如何使用 JWT 和 API Gateway Lambda Authorizer 保护 API
JSON Web Token (JWT) 是一个开放标准 (RFC 7519),它定义了一种在两方之间传输信息(例如身份验证和授权事实)的方式:issuer 和_audience_。通信是安全的,因为发行的每个令牌都经过数字签名,因此消费者可以验证令牌是真实的还是伪造的。
在之前的故事中,我们深入讨论了它:它是如何构建的,它解决了什么问题,签名验证背后的理论是什么,最后,我们如何保护我们的资源。
图。 1 — 无服务器身份验证和授权的经典流程。
在这种情况下,想要访问受保护资源的客户端需要 JWT。没有它,同一个客户端需要联系_身份验证服务器_交换用户名和密码以获得全新的 JWT(从图 1 的步骤 1 到 3)。有了这个令牌,它最终可以通过 Amazon API Gateway 请求受保护的资源,该网关必须对其进行验证(从图 1 的步骤 4 到 6)。
在那篇文章中,我们介绍了理论,但没有提及真正的实现。
我们需要修理它!但首先......退后一步!
框架上下文
Amazon API Gateway 是一个具有许多功能的出色工具,但在其核心,它基本上是一个将传入的 HTTP 请求路由到负责的后端的组件。
Fig.2 — AWS 上的经典 API REST.
上图很好地代表了这个概念,因为我们有:
-
HTTP REST 端点:使用 Amazon API Gateway 实现(例如
GET /orders
); -
端点的后端:它可以是一个 AWS Lambda 函数(如图 2 所示)、一个容器化的微服务、一个负载均衡器、一个 HTTP 端点等;
那么,token 的验证呢?
据我们所见,cowboy 的逻辑建议将验证代码直接引入后端逻辑。就像是:
import lib.isAuthorized
function getOrders(token) {
if(isAuthorized(token))
{
// business logic goes here
}
}
我真的不喜欢将授权和业务代码混合在一起,但是,尤其是在单体应用程序中,这种方法本质上没有什么坏处。当我们有一个更“面向服务的架构”时,问题就开始了,比如微服务。如果每个服务都是一个带有授权码的整体,那么一旦我们发现一个错误,我们就必须重新部署整个系统来修复它。
Fig.3(不要在家里尝试这个)-与身份验证逻辑强耦合的微服务系统。
一种更精明的方法是建议从业务代码中消除授权逻辑,创建一个专用于它的新服务。我们需要修复授权逻辑的那一天,我们只需要重新部署一种服务类型。
Fig.4 — 带有用于身份验证逻辑的外部服务的微服务系统。
回到图 2 的示例,现在我们知道最好将 Amazon API Gateway 和 AWS Lambda 之间的直接通信分开,使用外部服务来处理授权逻辑。
引入 Lambda 授权器
Lambda Authorizer(以前称为 Custom Authorizer)是一种特殊类型的 Lambda 函数。它接受包含令牌的对象并返回 JSON 策略以允许或阻止 API 执行。像这样的东西:
{
"principalId": "apigateway.amazonaws.com",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Action": "execute-api:Invoke",
"Effect": "Allow",
"Resource": "arn:aws:execute-api:{REGION}:{ACCOUNT_ID}:{API_ID}/Prod/GET/"
}]
}
}
通过上述策略,API网关服务(即principalId
等于apigateway.amazonaws.com
)被允许(即Effect
等于Allow
)调用(即Action
等于execute-api:Invoke
)给定的API资源(例如Resource
等于toarn:aws:execute-api:{REGION}:{ACCOUNT_ID}:{API_ID}/Prod/GET/
)。
为了了解我们的函数必须返回什么策略,它需要验证令牌。按照惯例,授权函数接受一个参数(在 Lambda 方言中,它的名称是 Event Source),此后称为event
,它具有两个重要属性:
-
event.authorizationToken
— 我们要验证的令牌; -
event.methodArn
——触发授权方的API请求的ARN;
我们的第一个 Lambda 授权者
为了逐步介绍逻辑,这里我们提出一个非常简单(但不是那么安全)的授权器:
exports.handler = async (event) => {
if(event.authorizationToken === "OK")
return allowPolicy(event.methodArn);
return denyAllPolicy();
};
function denyAllPolicy(){
return {
"principalId": "*",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "*",
"Effect": "Deny",
"Resource": "*"
}
]
}
}
}
function allowPolicy(methodArn){
return {
"principalId": "apigateway.amazonaws.com",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": "Allow",
"Resource": methodArn
}
]
}
}
}
在上面的代码中,授权方评估令牌,如果它的值为OK
,它将返回一个让请求继续流动的策略。否则返回denyAllPolicy
表示的策略,中断流程。
创建我们的第一个 Lambda 授权者
作为_第一步_我们要自己构建 Lambda Authorizer,因此创建一个 Node.js 10.x Lambda 函数并将上述代码粘贴到编辑器中并保存(这里是教程)。 第二步(如果您已经有 API 可以使用,则可选)是创建我们想要保护的 API。为了尽可能快地进行,我们可以使用 AWS CodeStar(此处为教程)创建一个新的无服务器 Web 应用程序——无论使用何种语言。
CodeStar 构建完所有组件后,打开 Amazon API Gateway 控制台并搜索这个新 API(名称将类似于 CodeStar 项目的名称)并展开它。
Fig.6–我们API的授权者列表(当前为空)。
此时,我们可以跳转到“Authorizers”选项卡并点击“Create New Authorizer”。
图7——我们的第一个授权人的创建。
要创建一个新的 Authorizer,我们需要选择一个 name(例如lambda-authorizer-test
)和一个 type(例如 Lambda)。
**注意:**如果您已经使用 Amazon Cognito 用户池作为身份提供商,您应该选择 Cognito 作为授权方类型。如果您想阅读更多内容,请单击此处。
在 Lambda 函数字段中,我们必须提供将充当授权方的 Lambda 函数,并且我们已在本节的第一步中创建了该函数。
最后,我们必须解释我们的客户端将如何在 API 请求期间发送令牌。按照惯例,这是通过Authorization
HTTP 标头发送的,因此我们可以将其设置为 “Token Source”(图 7)。
对于这个例子,我们不想设置正则表达式来验证令牌的语法正确性,也不想使用缓存,所以我们只需点击_“创建”_。
图8-API网关需要一个角色来执行该功能,这里我们可以创建它。
由于 Amazon API Gateway 需要具有特定权限的角色才能调用授权方,因此如果出现上述弹出窗口,请单击“Grant & Create”_ 接受提示。
在 “Authorizers” 选项卡中,以前是空的(图 6),现在我们可以看到我们的 Authorizer。
图。 9–一旦我们创建了我们的授权人,我们就可以对其进行测试。
在我们继续之前,点击_“测试”_并检查它的逻辑。在下面的示例中,我们使用字符串 “NOT OK”,正如预期的那样,它返回 deny-all 策略。
图10——我们的授权人的测试结果。
将所有东西连接在一起
现在我们已经把所有东西都放在了正确的位置,我们终于可以拆分 Amazon API Gateway 和后端 Lambda 之间的绑定,在中间引入授权者。
在同一个 Amazon API Gateway 控制面板中单击_“资源”_ 菜单项,这将显示与此 API 关联的所有资源。选择我们希望保护的 HTTP 动词(如果我们按照教程我们只有根资源/
和GET
动词)。
Fig.11 — API 流程,从请求到集成。
单击_“方法请求”_,当我们准备好后,在“_Authorization”_下拉列表中选择之前创建的授权者(在本例中为_lambda-authorizer-test;_如果我们看不到它,应该足够了刷新页面),然后单击 ✔ 符号确认(图 11)。
Fig.11–这里我们可以选择我们要使用的授权人是什么。
部署并测试修改
至此,我们已经正确配置了授权方,但我们仍然需要部署 API 才能使事情正常进行。单击下拉列表_“操作”,然后选择“部署 API”。将出现一个弹出窗口,因此我们可以选择我们要部署 API 的目标阶段(例如_Stage),然后单击_“部署”_(如果您需要更多详细信息,here是 AWS 的官方文档) .
如果一切都按预期进行,我们将看到“调用 URL”。复制该 URL 并使用 Postman 之类的工具发出请求,这样我们就可以轻松地在请求中填充Authorization
标头(图 12)。
Fig.12–我们使用错误令牌调用受保护 API 的输出。
在这种情况下,我们收到了一个403 Unauthorized
错误和一条明确的消息:
用户无权通过显式拒绝访问此资源。
真正的授权人
如前所述,上述授权者并不那么安全。我们需要更智能的东西,它能够执行之前的故事中定义的验证步骤。这些是:
-
解码令牌,这样我们就可以得到 JWK 端点;
-
调用此端点并检索计算公钥的_exponent_ 和_modulus_(使用
kid
来识别要选择的正确密钥); -
计算 PEM;
-
针对 PEM 验证令牌;
如果在这些步骤中没有发生错误,将返回allowPolicy
。
这是一个伪代码:
function(event) {
try {
token = decode(event.authorizationToken)
jwk = getJwkByKid(token.kid)
pem = jwkToPem(jwk)
verify(token, pem)
return allowPolicy()
} catch (error) {
return denyAllPolicy()
}
}
// CODE HERE
https://github.com/marianoc84/lambda-authorizers-collections
即使我们仍然可以执行进一步的检查,这个算法也比以前看到的要安全得多。
如果你喜欢这篇文章,请支持我的工作!
总结
在本系列的第一篇文章中,我们介绍了 JSON Web Token (JWT)。我们研究了它的特点以及为什么它现在如此受欢迎。
相反,在这篇文章中,我们展示了 JWT 如何融入 AWS 生态系统,尤其是在 Amazon API Gateway 中。使用此功能,我们可以拆分 HTTP 端点(例如GET /orders
)与其后端(例如GetOrders
Lambda 函数)之间的通信,从而引入授权策略层。
我们强调了创建和测试我们的第一个和微不足道的授权者所需的步骤。如果您想查看完整示例的代码,请查看这个 GitHub 存储库。
更多推荐
所有评论(0)