关注留言点赞,带你了解最流行的软件开发知识与最新科技行业趋势。
使用无服务器进行构建的好处之一是,您可以以一种可组合的方式设计事物。这意味着您可以将逻辑与其他志同道合的逻辑紧密结合在一起,然后让事物与其他组件保持松散耦合,以便事物易于更改而不会太脆弱。构建 API 时,您通常需要某种授权方来验证所提供的令牌。在本文中,我将逐步介绍如何使用 Golang 构建自定义 API 网关授权方。
使用 Golang 的 API 网关授权方
作为参考,这是我想向您展示的架构图。
使用 Golang 的 API 网关授权方
![]()
以上实现的是以下内容
定义一个 API 网关来管理我们资源的有效负载
使用 Lamabda 处理授权
针对 Cognito 用户池验证令牌
利用具有自定义设置 TTL 的缓存来节省计算
最后,如果一切顺利,允许访问受保护的资源也将能够提供对声明上下文的覆盖
这篇文章还有另一半,我将向您展示如何使用 Lambdas 和 DyanamoDB 扩展我们将使用的 JWT。如果您对此感到好奇,这里的文章将向您展示这是如何完成的
遍历代码
CDK 从 Cognito 开始
要使用 Cognito 进行验证,我们首先需要构建一个 Cognito 实例以及一个能够登录的客户端。
定义 UserPool 如下所示。没有什么需要额外解释的,所以让我们继续讨论客户端。
this._pool = new cognito.UserPool(this, "SamplePool", {
userPoolName: "SamplePool",
selfSignUpEnabled: false,
signInAliases: {
email: true,
username: true,
preferredUsername: true,
},
autoVerify: {
email: false,
},
standardAttributes: {
email: {
required: true,
mutable: true,
},
},
customAttributes: {
isAdmin: new cognito.StringAttribute({ mutable: true }),
},
passwordPolicy: {
minLength: 8,
requireLowercase: true,
requireDigits: true,
requireUppercase: true,
requireSymbols: true,
},
accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
removalPolicy: cdk.RemovalPolicy.DESTROY,
将客户端添加到 UserPool 也很简单。这么多的选择,但我下面的是相当香草。使用此客户端,您可以通过一种方式与用户一起登录并针对它进行其他应用程序开发。正如您稍后将在本文中看到的那样,我只是使用 Postman 将所有这些整合在一起。
this._pool.addClient("sample-client", {
userPoolClientName: "sample-client",
authFlows: {
adminUserPassword: true,
custom: true,
userPassword: true,
userSrp: false,
},
idTokenValidity: Duration.minutes(60),
refreshTokenValidity: Duration.days(30),
accessTokenValidity: Duration.minutes(60),
建立授权者
现在“自定义”使用 Golang 构建自定义 API 网关授权器。Authorizer 只不过是一个 Lambda 函数。因此,如果您愿意,这可以是从另一个堆栈导入的。但为简单起见,我将所有内容都包含在这一套基础设施中。如果您想更深入地了解 CDK 和 GoFunction,这里有一篇文章可以帮助您
CDK 中的函数定义。
export class AuthorizerFunction extends Construct {
private readonly _func: GoFunction;
constructor(scope: Construct, id: string, poolId: string) {
super(scope, id);
this._func = new GoFunction(this, "AuthorizerFunc", {
entry: path.join(__dirname, `../../../src/authorizer`),
functionName: "authorizer-func",
timeout: Duration.seconds(30),
environment: {
USER_POOL_ID: poolId,
},
get function(): GoFunction {
return this._func;
正如我上面提到的,一个简单的 GoFunction 实现。唯一需要注意的有趣的事情是 USER_POOL_ID 的环境变量。让我们来看看为什么这很重要。
Golang中的函数实现
对于这个使用 Golang 构建自定义 API 网关授权方的示例,我将验证 JWT 并添加一些额外的上下文。您的实施可能会有很大不同,这也是我喜欢这种方法的原因。您可以根据需要拥有多个不同的授权者,并且您的受保护资源不知道调用堆栈中在它们之上发生的事情。
我想向您展示的第一件事是如何为众所周知的 Cognito 端点建立密钥集。我在init()函数中这样做是因为我知道它会在 Lambda 初始化时运行一次,然后我将输出“缓存”在一个变量中,该变量将在 Lambda 调用中保持自身。不是冷启动,而是调用。
func init() {
log.SetFormatter(&log.JSONFormatter{
PrettyPrint: false,
log.SetLevel(log.DebugLevel)
region := "us-west-2"
poolId := os.Getenv("USER_POOL_ID")
var err error
jwksUrl := fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json", region, poolId)
keySet, err = jwk.Fetch(context.TODO(), jwksUrl)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"url": jwksUrl,
}).Fatal("error getting keyset")
jwksUrl上面的变量记录在AWS 开发人员指南中。我使用 来"github.com/lestrrat-go/jwx/jwt"表示KeySet我将使用的 来验证令牌的真实性和过期时间。还记得USER_POOL_ID上面CDK中的变量吗?这就是它发挥作用的地方。构建众所周知的端点需要 UserPoolId
此过程的下一部分是执行验证。我不打算在本文中详细说明这是如何发生的,但本质上图书馆将:
验证令牌的结构
验证签名密钥与密钥使用的算法匹配
验证过期以及令牌尚未过期
这就是使用库的好处 :) 下面是调用它的方法。
bounds := len(event.AuthorizationToken)
token := event.AuthorizationToken[7:bounds]
parsedToken, err := jwt.Parse(
[]byte(token),
jwt.WithKeySet(keySet),
jwt.WithValidate(true),
如果上述任何一项失败,的输出jwt.Parse将返回一个。error这意味着在这种情况下,您可以发出拒绝。像这样:
return events.APIGatewayCustomAuthorizerResponse{
PrincipalID: "",
PolicyDocument: events.APIGatewayCustomAuthorizerPolicy{
Version: "2012-10-17",
Statement: []events.IAMPolicyStatement{
Action: []string{"execute-api:Invoke"},
Effect: "Deny", // Here is the rejection
Resource: []string{"*"},
},
},
},
UsageIdentifierKey: "",
}, nil
请注意,我没有返回错误。这只是拒绝访问。403 响应不是错误,那么为什么要返回一个呢?
在一切正常的情况下,只需返回许可即可。
return events.APIGatewayCustomAuthorizerResponse{
PrincipalID: "",
PolicyDocument: events.APIGatewayCustomAuthorizerPolicy{
Version: "2012-10-17",
Statement: []events.IAMPolicyStatement{
Action: []string{"execute-api:Invoke"},
Effect: "Allow", // Return Allow
Resource: []string{"*"},
},
},
},
Context: DumpClaims(parsedToken),
UsageIdentifierKey: "",
}, nil
我还想强调该DumpClaims功能。那有什么作用?
Lambda 授权器的一项很酷的事情是,您可以将作为“上下文”发送的内容扩展到下游方。如果您想将令牌的一部分带到预定的目的地怎么办?该请求将向 JWT 发送公开的详细信息,但不会传递私人声明或您扩展的内容。也许是客户 ID?也许是一些角色?
func DumpClaims(token jwt.Token) map[string]interface{} {
m := make(map[string]interface{})
m["customKey"] = "SomeValueHere"
return m
对于本文,很简单,我只是customKey在上下文中添加一个。我将很快向您展示它是如何出现的。
CDK 受保护的资源
使用 Golang 构建自定义 API 网关授权器的乐趣已经结束了一半。那只是意味着另一半即将开始!现在我们已经有了授权者,我们该怎么办?当然,在它后面放一个受保护的资源!
constructor(scope: Construct, id: string, func: IFunction) {
super(scope, id);
const authorizer = new TokenAuthorizer(this, "TokenAuthorizer", {
authorizerName: "BearTokenAuthorizer",
handler: func,
this._api = new RestApi(this, "RestApi", {
description: "Sample API",
restApiName: "Sample API",
deployOptions: {
stageName: `main`,
},
defaultMethodOptions: {
authorizer: authorizer,
},
那就是 API 网关 CDK 代码。请注意,defaultMethodOptions我正在添加一个“授权者”。这只是一个IFunction. 这又可能是一个导入,或者在我们的例子中,它是我们刚刚构建的 Authorizer。
现在有了 API,我们可以创建资源。
constructor(scope: Construct, id: string, api: RestApi) {
super(scope, id);
this._func = new GoFunction(this, `ProtectedResource`, {
entry: path.join(__dirname, `../../../src/protected-resource`),
functionName: `protected-resource-func`,
timeout: Duration.seconds(30),
api.root.addMethod(
"GET",
new LambdaIntegration(this._func, {
proxy: true,
对于我们的示例,我正在使用 Lambda 代理集成并将其定义在“根”级别。所以我们可以期待“/”路径上的 GET 请求。
这个端点的实际处理程序也是一个简单的演示。
func handler(ctx context.Context, event events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) {
success := &Response{
Message: "Congrats! A Payload",
CustomKey: event.RequestContext.Authorizer["customKey"].(string),
b, _ := json.Marshal(success)
return &events.APIGatewayProxyResponse{
Body: string(b),
StatusCode: 200,
Headers: map[string]string{
"Content-Type": "application/json",
},
}, nil
customKey注意和 的使用event.RequestContext.Authorizer["customKey"].(string)。这个 event.RequestContext.Authorizer 包含一个 `map[string]interface{},您可以利用它来发挥自己的优势。
用例是无穷无尽的,但我经常将它用于我扩展的客户详细信息和用户角色以及个人资料数据。
把它们放在一起
让我们将自定义 API 网关授权方的输出与 Golang 放在一起。为此,这里是一起测试这一切的场景。
第一件事
在引导帐户中:
base
cdk deploy
创建已知用户
部署基础架构后,您应该拥有
2个lamda
授权人
保护资源
API网关
附有 Authoirzer 的 ProtectedResource 的一个端点
授权人
部署阶段
一个已知的用户池
这是您的 UserPool 的外观
![]()
用户池. 注意用户池 ID(出于某些原因我已经清除了我的 ID)。您需要复制该 ID,因为稍后会很重要。
![]()
现在的客户名单
客户名单
该表中的 ClientID 也很重要。同样,我的已经清理干净了,但请注意你的。
最后,创建一个用户并将其标记为已验证。
![]()
创建用户
记下他们的密码,因为我们将在一分钟内使用密码流程登录
游览 API 网关
对于我们的主要受保护资源,这是它的创建方式
![]()
受保护的资源
Authorization 字段指向我们在本文开头定义的 BearerTokenAuthorizer。
然后在 API 网关上定义授权者。请记住,如果您使用本文中定义的基本路径映射并共享授权方,则需要为每个 API 网关附加它。
![]()
授权方 API 网关
执行请求
我们终于准备好运行这个东西了。
但首先,让我们获得一个令牌。还记得我说过要捕获 UserPool 中的 ClientID 吗?现在是时候把它拿出来了。
获取令牌请求
![]()
这个的输出将是你的三个令牌。
访问令牌
身份证
刷新令牌
请随意在下一个请求中使用 ID 或 Access。
发出请求很简单。
失败请求
首先,让我们看看 Bad Token 会发生什么
![]()
错误的请求
您在 CloudWatch 中的日志应如下所示
失败 Cloudwatch
![]()
请求成功
现在为了成功!
![]()
您在 CloudWatch 中的日志应如下所示
失败 Cloudwatch
![]()
你做到了!
使用示例事件在本地测试
如果我不包括您也可以对授权方进行一些本地测试,那将是我的疏忽。这可以通过两种方式发生
一些单元测试
使用测试事件文件
运行本地文件
如果您在此堆栈上本地执行,您将在目录中cdk synth得到一个。您可以像这样运行存储库中包含的测试文件MainStack.template.jsoncdk.out
bash
sam local invoke AuthorizerFunc -t cdk.out/MainStack.template.json --event src/authorizer/test-events/e-1.json --env-vars environment.json --skip-pull-image
总结
那是一篇包含很多细节的长文章,但在使用无服务器技术构建安全且可扩展的 API 时,这种模式非常有用。通过使用 Golang 添加自定义 API 网关授权器,您可以在堆栈的高层捕获此授权逻辑,从而节省下游资源,使其不必处理此重复代码。此外,但利用事件上下文到您的下游 Lambda,您可以使用您可能已自定义的 PrivateClaims。