亚马逊AWS官方博客
认证授权专题(四) : 利用Cognito Group信息管理多套API Gateway+lambda环境
随着目前Serverless微服务的应用越来越广泛,如何安全有效的管理多套serverless环境构建自己的应用系统逐渐成为一个大家关心的问题。
此文针对客户使用多套API Gateway + Lambda的场景,介绍了如何利用Cognito来实现访问权限的管理与区分。Cognito用户池中不同group信息的用户可以访问不同的微服务环境。如果一个用户同时属于多个group,则当前用户可以访问多套环境。终端用户将没有权限访问自己并不属于的group的API资源。
架构图
总述
Cognito为AWS提供的mobile端访问控制工具。利用Cognito User Pool,可以方便的实现web应用用户的注册,登录,登出等功能。Cognito提供的用户池自带多种属性可以供管理者选择,其中包括group,即组别信息。一旦拿到group的值,我们就可以利用该属性去做一些权限的判定与区分。本文利用两种方式来实现此过程,两者均利用lambda自定义Authorization来实现。每套serverless环境,需要一个API Gateway,两个lambda函数。
- 一种是前端解析cognito生成的 JWT token,将用户的cognito:group信息直接传给API Gateway,API Gateway Authorization Lambda通过对group信息的条件判断决定是否allow访问,仅允许本组成员访问本组资源,若为其他组成员,拒绝访问(您也可以将此值换成其他的attribute);
- 另一种方式是将cognito生成的JWT token传递给后端,后端通过decode解析整个token,将解析后的值来做判断,并且可以将解析后的值进一步传递给后端的lambda来做正常的业务逻辑。
两种方法对比下来,前者更为简单,而后者则更为灵活。
有关于Cognito生成的JWT Token的更多解释,请点击此页面
步骤
1. 创建cognito用户池user pool
输入user pool名称(如cognito-user-pool-for-iot),review defaults, 并根据需求做自定义修改(如可以修改necessary attributes,密码长度和字符的要求等),此demo均利用默认值。
2. 创建并配置应用客户端app client
选择应用客户端,取消generate client secret的选项
在左侧APP-Integration项目下,需要我们修改的有2个地方,一是APP client setting,修改callback URL以及scope token作用范围,二是自定义domain name(需要全region唯一)
注意:localhost:8000仅在测试环境中使用。实际生产环境,这里的callback不支持http协议,请修改为https的网址。请勿写入http://ip等形式。
记下userPoolID和app Client ID,在下一步骤中会用到。
3. 搭建API Gateway资源
如果还没有API资源,新建rest-new API, 命名资源后,添加方法
在本例当中,我们添加一个get method。点击get方法后,进入API Gateway的配置页面。
在Integration Request当中,选择intergration type为lambda,配置自己的lambda函数名
注:此lambda函数为最终执行逻辑的业务层级的lambda函数,而不是authorizer
4. 增加Lambda自定义认证方式
添加新的authorizer,选择用来做认证的lambda函数,event payload选择token,invoke role留空。不建议开启caching,以防止因存在缓存而出现测试结果混乱的可能。
除了token以外,事实上,API Gateway authorizer也支持用request的方式(如query string)来传递此值,本文对该话题不做进一步展开。
在authorizer lambda函数的设置当中,我们可以任意自定义规则。
在条件判断完毕后,允许的policy example如下:
allow_response = {
"principalId": "random",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": "Allow",
"Resource":"<API-Gateway-method-arn>"
}
]
},
#需要传递给后端lambda的值
"context": {
"key": "test",
"numKey": 1
}
}
注意:的完整格式为arn:aws:execute-api:{regionId}:{accountId}:{appId}/{stage}/{httpVerb}/[{resource}/[{child-resources}]].
例如arn:aws:execute-api:ap-northeast-1:1234567799:xxxxxxxx/beta/GET/(如果有resource传参接着写{resource})
deny的example policy如下:
deny_response= {
"principalId": event['authorizationToken'],
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": "deny",
"Resource":"<API-Gateway-method-arn>"
}
]
}
}
在本例当中,我们有两种方式可实现此判断,一种是直接传递解析过后的value,另外一种是传递jwt token,由authorization lambda自己做解析拿到payload。以下为具体实现。
(0)创建lambda函数
进入lambda控制台, 选择右上角按钮“创建函数”, 命名函数名称,选择python2.7作为语言,并且给lambda函数一个角色(role)。如果lambda函数有对AWS其他产品或服务的调用,则需确保lambda的角色有访问其他产品服务的权限,否则会报403 permission denied。有关于更多role的解释和说明,请参考这里.
(1)我们校验前端传递过来的group信息并做判断。如果该user的group value为本组资源,则允许访问allow,否则deny.lambda python2.7参考代码如下, 也可以点击这里下载
import json
def lambda_handler(event, context):
#获取group信息
groups= event['authorizationToken'].split(",")
#如果只属于一个group,且为本组,则允许访问
if (len(groups) ==1):
if (event['authorizationToken'] == 'group1'):
response = allow_response
#否则deny
else:
response = deny_response
return response
else:
#如果有多个group信息,只要有一组是本组资源,允许访问;
for group in groups:
#print(group)
if (group == 'group1'):
response = allow_response
return response
#循环完毕,没有本组信息,deny
response = deny_response
return response
配置完毕后可以点击test测试是否返回正确的policy,如
{
"type":"TOKEN",
"authorizationToken":"{caller-supplied-token}",
"methodArn":"arn:aws:execute-api:{regionId}:{accountId}:{appId}/{stage}/{httpVerb}/[{resource}/[{child-resources}]]"
}
(2)前端将完整的JWT token传递给后端
cognito有三种token,分别为idToken, access Token以及refresh token。本demo当中附带的index.html通过前端的方式直观的展示了这三种token解析出来的payload。我们可以看到,idtoken与accesstoken解析出的claims包含cognito:groups,username,email等attribute。
idToken与accessToken均由三个部分组成,header,payload,以及signature。格式是这样的11111111111.22222222222.33333333333
在本文当中,我们只对payload做验证。实际生产也可以增加对signature的验证。有关于signature验证的解释,请参考这里.
python示例如下
full_token=idToken.split('.')
b64_string=full_token[1] #payload token
b64_string += "=" * ((4 - len(b64_string) % 4) % 4) #ugh
print(base64.b64decode(b64_string)) #解析后的完整json
这样我们就拿到了解析后的对应的json值。lambda可自行进行if逻辑判断或者传参给后端。
5. enable跨域功能
在我们的demo中,我们会用localhost访问此API gateway,因此需要enable CORS否则会报跨域无法访问的错误
在header处添加Authorization和Access-Control-Allow-Origin标头
6. 部署API
在以上流程没有问题后,点击部署API
一定要注意,在每一次更新API的配置或者设置之后,一定要重新部署API,否则不会生效。
7. 测试
用postman等API访问工具直接访问此get请求时,或方法(1)带authorization header但非group1信息时,方法(2)token不对时,均会显示无法访问
只有header带group1时,可以实现访问
8. 结合前端cognito测试
可在前端注册用户,添加group,并将用户添加到某个group当中。
cognito JS核心代码如下,在代码可以demo用户登录获取token,解析token的过程。此demo的完整代码在这里下载,有关于cognito JS更多示例以及use case,可以点击此页面查看。
$.ajax({
url: "https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/beta",
type: "GET",
beforeSend: function(xhr){
xhr.setRequestHeader('Authorization', cognito_groups) ;
xhr.setRequestHeader('Access-Control-Allow-Origin','*');
}
});
需要修改的字段如下:
根据两种方法的不同,为API endpoint的header当中发送的值可以是解析后的cognito_group,也可以是idtoken或是accessToken
修改cognito app client配置以及API endpoint
function initCognitoSDK() {
AWS.config.region ='ap-northeast-1';
var authData = {
ClientId : '<app-client-id>', // Your APP client id here
AppWebDomain : '<your-custom-domain-name', // Exclude the "https://" part.
//TokenScopesArray : ['openid','email'], // like ['openid','email','phone']...
TokenScopesArray : ['openid'],
RedirectUriSignIn : 'http://localhost:8000',
//RedirectUriSignIn:"https://sdb53tv9o0.execute-api.ap-northeast-1.amazonaws.com/beta",
RedirectUriSignOut : 'http://localhost:8000',
UserPoolId : '<user-pool-id>',
AdvancedSecurityDataCollectionFlag : false
};
var login = {};
var auth = new AmazonCognitoIdentity.CognitoAuth(authData);
// You can also set state parameter
// auth.setState(<state parameter>);
auth.userhandler = {
onSuccess: function (result) {
//根据传递的值不同,这里可以为解析后的cognito_group,也可以是idtoken或是accessToken
var cognito_groups= showSignedIn(result);
$.ajax({
url: "https://<API-address>/<stage>/", //替换为自己的API Gateway endpoint
type: "GET",
beforeSend: function(xhr){
xhr.setRequestHeader('Authorization', cognito_groups) ;
xhr.setRequestHeader('Access-Control-Allow-Origin','*');
},
success: function(data) {
console.log(data);
}
});
},
onFailure: function (err) {
console.log('error',err);
}
};
return auth;
}
修改跳转地址
function userButton(auth) {
var state = document.getElementById('signInButton').innerHTML;
if (state === "Sign Out") {
document.getElementById("signInButton").href="https://<your-custom-domain-name>/logout?response_type=code&client_id=<app-client-id>&logout_uri=http://localhost:8000";
document.getElementById("signInButton").innerHTML = "Sign In";
auth.signOut();
showSignedOut();
} else {
//session_info = auth.getSession();
//console.log(session_info);
document.getElementById("signInButton").href="https://<your-custom-domain-name>/login?response_type=code&client_id=<app-client-id>&redirect_uri=http://localhost:8000";
}
}
可以打开浏览器–tools–developer tools,通过console的log查看API是否调用成功,或者通过networking tab查看API的response情况(200/403).
会发现只有当前user属于group1时,才允许访问,否则都为deny。
资源销毁
- 删除API Gateway
- 删除lambda函数
- 删除cognito user pool