随着目前 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 的更多解释,请点击此页面
本文用到的代码点击这里获取<<<<
步骤
创建 cognito 用户池 user pool
输入 user pool 名称(如 cognito-user-pool-for-iot),review defaults, 并根据需求做自定义修改(如可以修改 necessary attributes,密码长度和字符的要求等),此 demo 均利用默认值。
创建并配置应用客户端 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,在下一步骤中会用到。
搭建 API Gateway 资源
如果还没有 API 资源,新建 rest-new API, 命名资源后,添加方法
在本例当中,我们添加一个 get method。点击 get 方法后,进入 API Gateway 的配置页面。
在 Integration Request 当中,选择 intergration type 为 lambda,配置自己的 lambda 函数名
注:此 lambda 函数为最终执行逻辑的业务层级的 lambda 函数,而不是 authorizer
增加 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。
![](https://static001.infoq.cn/resource/image/53/37/53ce660419837d2fd7c7dbc2befc4637.png)
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否则会报跨域无法访问的错误
![](https://static001.infoq.cn/resource/image/3c/6f/3cd1061f16d4137b1f3ac57283a8516f.png)
在header处添加Authorization和Access-Control-Allow-Origin标头
![](https://static001.infoq.cn/resource/image/16/fa/160266e500ec2c66267c23d5bb04eafa.png)
6. 部署API
在以上流程没有问题后,点击部署API
![](https://static001.infoq.cn/resource/image/96/a2/9629f43b69b424fe657f9a18f053b5a2.png)
一定要注意,在每一次更新API的配置或者设置之后,一定要重新部署API,否则不会生效。
7. 测试
用postman等API访问工具直接访问此get请求时,或方法(1)带authorization header但非group1信息时,方法(2)token不对时,均会显示无法访问 ![](https://static001.infoq.cn/resource/image/56/9d/561ac6fc2fffbd6b033a1db18db1029d.png)
只有header带group1时,可以实现访问
![](https://static001.infoq.cn/resource/image/95/33/95b6638fc75db1abad7a59eade7a8033.png)
8. 结合前端cognito测试
可在前端注册用户,添加group,并将用户添加到某个group当中。
![](https://static001.infoq.cn/resource/image/70/de/70aa627f174ca44894ce4ca76f29fbde.png)
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:///logout?response_type=code&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。
![](https://static001.infoq.cn/resource/image/8d/17/8d7e1307133b06e52020df462ffb4917.png)
## 资源销毁
1. 删除API Gateway
2. 删除lambda函数
3. 删除cognito user pool
## 参考资料
https://aws.amazon.com/cn/blogs/compute/introducing-custom-authorizers-in-amazon-api-gateway/
https://docs.aws.amazon.com/zh_cn/cognito/latest/developerguide/using-amazon-cognito-user-identity-pools-javascript-examples.html
https://docs.aws.amazon.com/zh_cn/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html
** 作者介绍:**
李天歌
AWS解决方案架构师
张贝贝
AWS 解决方案架构师
**本文转载自AWS博客。**
**原文链接:**
https://amazonaws-china.com/cn/blogs/china/cognito-group-api-gateway-lambda/
复制代码
评论