AWS 无服务器入门 - 身份验证
TL;DR
在本系列中,我将尝试讲解 AWS 上无服务器的基础知识,以便您构建自己的无服务器应用程序。在上一篇文章中,我们一起学习了如何创建 Lambda 函数、Rest API、数据库和文件存储。在本文中,我们将学习如何使用 Cognito 为 API 添加身份验证、处理登录以及创建受保护的 API 路由。
⬇️ 我会定期发布无服务器内容,如果你想了解更多 ⬇️
快速公告:我还在开发一个名为🛡 sls-mentor 🛡的库。它汇集了 30 条无服务器最佳实践,这些实践会在您的 AWS 无服务器项目中自动检查(无论使用哪种框架)。它是免费开源的,欢迎随时查看!
Cognito 简介
Cognito 是 AWS 的身份验证服务。它允许您创建用户池,其中包含用户信息(用户名、电子邮件、密码等),并以安全可靠的方式存储。连接到这些用户池后,您可以创建用户池客户端,这些客户端是使用用户池对用户进行身份验证的应用程序。例如,您可以为 Web 应用程序创建一个用户池,为移动应用程序创建另一个用户池。每个应用程序都有自己的应用客户端,并且能够使用相同的用户池对用户进行身份验证。
可以使用此模式轻松描述,其中用户投票是中心元素,用户可以通过前端应用程序或 lambda 函数进行连接。
今天的目标 - 创建受授权器保护的 API 路由
今天,我们将创建一个简单的 API 路由,并由授权器进行保护。该授权器将是一个 Cognito 用户池,允许我们在用户访问 API 路由之前对其进行身份验证。我们还将创建一个注册、确认和登录路由,允许用户进行身份验证并获取 JWT 令牌,该令牌将用于在受保护的路由上进行身份验证。其架构如下:
为了构建这个基础设施,我将使用 AWS CDK 和 TypeScript。我已经在本系列的上一篇文章中使用过这种部署方法,如果你需要复习一下,可以随时查看一下😉。
创建用户池和用户池客户端
使用 AWS CDK,可以直观地创建用户池和应用程序客户端。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class MyFirstStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const userPool = new cdk.aws_cognito.UserPool(this, 'myFirstUserPool', {
selfSignUpEnabled: true,
autoVerify: {
email: true,
},
});
const userPoolClient = new cdk.aws_cognito.UserPoolClient(this, 'myFirstUserPoolClient', {
userPool,
authFlows: {
userPassword: true,
},
});
}
}
分为两个步骤:我创建一个用户池,其中包含允许用户自行注册并在其地址上接收确认电子邮件的选项。然后,我将这个用户池连接到一个用户池客户端,该客户端将用于对用户进行身份验证。我启用userPassword
身份验证流程,这将允许用户使用用户名和密码进行身份验证。
注册、确认电子邮件地址并登录
在本教程中,我将尽量保持简单。我将创建三个身份验证 API 路由:注册路由允许用户创建帐户并通过电子邮件接收验证码;确认路由允许用户确认帐户;登录路由允许用户进行身份验证并获取 JWT 令牌。
让我们首先配置与这 3 条路由对应的 3 个 lambda 函数。
// ... Previous code ...
// Provision a signup lambda function
const signup = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'signup', {
entry: path.join(__dirname, 'signup', 'handler.ts'),
handler: 'handler',
environment: {
USER_POOL_CLIENT_ID: userPoolClient.userPoolClientId,
},
});
// Give the lambda function the permission to sign up users
signup.addToRolePolicy(
new cdk.aws_iam.PolicyStatement({
actions: ['cognito-idp:SignUp'],
resources: [userPool.userPoolArn],
}),
);
// Provision a signup lambda function
const confirm = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'confirm', {
entry: path.join(__dirname, 'confirm', 'handler.ts'),
handler: 'handler',
environment: {
USER_POOL_CLIENT_ID: userPoolClient.userPoolClientId,
},
});
// Give the lambda function the permission to sign up users
confirm.addToRolePolicy(
new cdk.aws_iam.PolicyStatement({
actions: ['cognito-idp:ConfirmSignUp'],
resources: [userPool.userPoolArn],
}),
);
// Provision a signin lambda function
const signin = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'signin', {
entry: path.join(__dirname, 'signin', 'handler.ts'),
handler: 'handler',
environment: {
USER_POOL_CLIENT_ID: userPoolClient.userPoolClientId,
},
});
// GIve the lambda function the permission to sign in users
signin.addToRolePolicy(
new cdk.aws_iam.PolicyStatement({
actions: ['cognito-idp:InitiateAuth'],
resources: [userPool.userPoolArn],
}),
);
在此代码片段中,我创建了三个 lambda 函数,一个用于注册路由,一个用于确认路由,一个用于登录路由。我还向它们传递了用户池客户端 ID,用于对用户进行身份验证。
最后,我向 Lambda 函数添加所需的 IAM 权限,以允许它们与用户池交互。第一个 Lambda 函数需要该cognito-idp:SignUp
权限,第二个 Lambda 函数需要该权限cognito-idp:ConfirmSignUp
,第三个 Lambda 函数也需要该cognito-idp:InitiateAuth
权限。
下一步,在这些 Lambda 函数中创建代码。我们从注册函数开始。
import { CognitoIdentityProviderClient, SignUpCommand } from '@aws-sdk/client-cognito-identity-provider';
const client = new CognitoIdentityProviderClient({});
export const handler = async (event: { body: string }): Promise<{ statusCode: number; body: string }> => {
const { username, password, email } = JSON.parse(event.body) as {
username?: string;
password?: string;
email?: string;
};
if (username === undefined || password === undefined || email === undefined) {
return Promise.resolve({ statusCode: 400, body: 'Missing username, email or password' });
}
const userPoolClientId = process.env.USER_POOL_CLIENT_ID;
await client.send(
new SignUpCommand({
ClientId: userPoolClientId,
Username: username,
Password: password,
UserAttributes: [
{
Name: 'email',
Value: email,
},
],
}),
);
return { statusCode: 200, body: 'User created' };
};
在此代码片段中,我使用 SDK 向用户池发送了一个 SignUp 命令。我从之前在代码的配置部分中指定的环境变量中获取了 clientId。
确认功能非常相似。
import { CognitoIdentityProviderClient, ConfirmSignUpCommand } from '@aws-sdk/client-cognito-identity-provider';
const client = new CognitoIdentityProviderClient({});
export const handler = async (event: { body: string }): Promise<{ statusCode: number; body: string }> => {
const { username, code } = JSON.parse(event.body) as { username?: string; code?: string };
if (username === undefined || code === undefined) {
return Promise.resolve({ statusCode: 400, body: 'Missing username or confirmation code' });
}
const userPoolClientId = process.env.USER_POOL_CLIENT_ID;
await client.send(
new ConfirmSignUpCommand({
ClientId: userPoolClientId,
Username: username,
ConfirmationCode: code,
}),
);
return { statusCode: 200, body: 'User confirmed' };
};
该处理程序需要收到注册用户通过电子邮件收到的确认码。然后,它会向用户池触发 ConfirmSignUp 命令。
最后,登录功能。
import { CognitoIdentityProviderClient, InitiateAuthCommand } from '@aws-sdk/client-cognito-identity-provider';
const client = new CognitoIdentityProviderClient({});
export const handler = async (event: { body: string }): Promise<{ statusCode: number; body: string }> => {
const { username, password } = JSON.parse(event.body) as { username?: string; password?: string };
if (username === undefined || password === undefined) {
return Promise.resolve({ statusCode: 400, body: 'Missing username or password' });
}
const userPoolClientId = process.env.USER_POOL_CLIENT_ID;
const result = await client.send(
new InitiateAuthCommand({
AuthFlow: 'USER_PASSWORD_AUTH',
ClientId: userPoolClientId,
AuthParameters: {
USERNAME: username,
PASSWORD: password,
},
}),
);
const idToken = result.AuthenticationResult?.IdToken;
if (idToken === undefined) {
return Promise.resolve({ statusCode: 401, body: 'Authentication failed' });
}
return { statusCode: 200, body: idToken };
};
此函数需要输入用户名和密码。然后,它会向用户池触发 InitiateAuth 命令。此命令将返回一个 ID 令牌,即 JWT 令牌。用户稍后可以使用此令牌进行身份验证。
创建 API 和受保护的路由
现在 lambda 函数已创建,我可以创建 API 和受保护的路由了。我将使用 AWS CDK 中的 API 网关构造。
// ... previous code ...
// Create a new API
const myFirstApi = new cdk.aws_apigateway.RestApi(this, 'myFirstApi', {});
// Add routes to the API
myFirstApi.root.addResource('sign-up').addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(signup));
myFirstApi.root.addResource('sign-in').addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(signin));
myFirstApi.root.addResource('confirm').addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(confirm));
此代码片段创建了一个新的 API,并为其添加了三个路由。每个路由都链接到一个 lambda 函数。API 网关构造将自动创建所需的权限,以允许 API 触发 lambda 函数。
现在,让我们基于 Cognito 用户池创建一个授权器,并将其分配给我们想要保护的新路线。
// ... previous code ...
// Create an authorizer based on the user pool
const authorizer = new cdk.aws_apigateway.CognitoUserPoolsAuthorizer(this, 'myFirstAuthorizer', {
cognitoUserPools: [userPool],
identitySource: 'method.request.header.Authorization',
});
const secretLambda = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'secret', {
entry: path.join(__dirname, 'secret', 'handler.ts'),
handler: 'handler',
});
// Create a new secret route, triggering the secret Lambda, and protected by the authorizer
myFirstApi.root.addResource('secret').addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(secret), {
authorizer,
authorizationType: cdk.aws_apigateway.AuthorizationType.COGNITO,
});
最后,秘密 lambda 函数的(简单)代码。
export const handler = async (): Promise<{ statusCode: number; body: string }> => {
return Promise.resolve({ statusCode: 200, body: 'CAUTION !!! THIS IS VERY SECRET' });
};
部署和测试应用程序
大功告成!最后一步,部署应用并测试。要部署应用,请运行以下命令。
npm run cdk deploy
您可以在 AWS 控制台的 API 网关部分找到该 API 的 URL。我在本系列的第一篇文章中对此进行了详细的解释,欢迎随时查看!
是时候测试一切了!首先,让我们注册,我指定用户名、电子邮件和密码。
一旦注册,我会收到一封包含确认码的电子邮件。
然后,我输入通过电子邮件收到的代码和我的用户名来确认注册。
最后,我可以使用我的用户名和密码登录。
此请求的结果是一个 JWT 令牌。我可以使用此令牌访问秘密路由,方法是将其传递到 Authorization 标头中,格式如下:“Bearer”
一切都按预期进行!
结论
本教程是对 Cognito 上无服务器身份验证的简单介绍。它是一款简单的入门工具,能够帮助您注册和验证用户身份。此外,它还提供许多其他功能,例如多因素身份验证、在您自己的域上托管 UI 身份验证等等。我可能会在以后的文章中讨论这些主题!
我计划每两个月更新一次这一系列文章。我已经介绍了如何创建简单的 Lambda 函数和 REST API,以及如何与 DynamoDB 数据库和 S3 存储桶进行交互。您可以在我的代码库中关注这些进展!我将介绍一些新的主题,例如创建事件驱动的应用程序、类型安全等等。如果您有任何建议,请随时联系我!
如果您能回复并分享这篇文章给您的朋友和同事,我将不胜感激。这将极大地帮助我扩大读者群。另外,别忘了订阅,以便及时收到下一篇文章的更新!
如果您想与我保持联系,请访问我的Twitter 账号。我经常发布或转发有关 AWS 和无服务器的有趣内容,欢迎关注我!
鏂囩珷鏉ユ簮锛�https://dev.to/slsbytheodo/learn-serverless-on-aws-authentication-with-cognito-19bo