A

AWS 无服务器入门 - 身份验证

2025-06-10

AWS 无服务器入门 - 身份验证

TL;DR

在本系列中,我将尝试讲解 AWS 上无服务器的基础知识,以便您构建自己的无服务器应用程序。在上一篇文章中,我们一起学习了如何创建 Lambda 函数、Rest API、数据库和文件存储。在本文中,我们将学习如何使用 Cognito 为 API 添加身份验证、处理登录以及创建受保护的 API 路由。

⬇️ 我会定期发布无服务器内容,如果你想了解更多 ⬇️

在 Twitter 上关注我🚀

快速公告:我还在开发一个名为🛡 sls-mentor 🛡的库。它汇集了 30 条无服务器最佳实践,这些实践会在您的 AWS 无服务器项目中自动检查(无论使用哪种框架)。它是免费开源的,欢迎随时查看!

在 Github 上查找 sls-mentor⭐️

Cognito 简介

Cognito 是 AWS 的身份验证服务。它允许您创建用户池,其中包含用户信息(用户名、电子邮件、密码等),并以安全可靠的方式存储。连接到这些用户池后,您可以创建用户池客户端,这些客户端是使用用户池对用户进行身份验证的应用程序。例如,您可以为 Web 应用程序创建一个用户池,为移动应用程序创建另一个用户池。每个应用程序都有自己的应用客户端,并且能够使用相同的用户池对用户进行身份验证。

可以使用此模式轻松描述,其中用户投票是中心元素,用户可以通过前端应用程序或 lambda 函数进行连接。

Cognito 模式

今天的目标 - 创建受授权器保护的 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,
      },
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

分为两个步骤:我创建一个用户池,其中包含允许用户自行注册并在其地址上接收确认电子邮件的选项。然后,我将这个用户池连接到一个用户池客户端,该客户端将用于对用户进行身份验证。我启用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],
  }),
);
Enter fullscreen mode Exit fullscreen mode

在此代码片段中,我创建了三个 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' };
};
Enter fullscreen mode Exit fullscreen mode

在此代码片段中,我使用 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' };
};
Enter fullscreen mode Exit fullscreen mode

该处理程序需要收到注册用户通过电子邮件收到的确认码。然后,它会向用户池触发 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 };
};
Enter fullscreen mode Exit fullscreen mode

此函数需要输入用户名和密码。然后,它会向用户池触发 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));
Enter fullscreen mode Exit fullscreen mode

此代码片段创建了一个新的 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,
});
Enter fullscreen mode Exit fullscreen mode

最后,秘密 lambda 函数的(简单)代码。

export const handler = async (): Promise<{ statusCode: number; body: string }> => {
  return Promise.resolve({ statusCode: 200, body: 'CAUTION !!! THIS IS VERY SECRET' });
};
Enter fullscreen mode Exit fullscreen mode

部署和测试应用程序

大功告成!最后一步,部署应用并测试。要部署应用,请运行以下命令。

npm run cdk deploy
Enter fullscreen mode Exit fullscreen mode

您可以在 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
PREV
AWS 无服务器入门 - 电子邮件
NEXT
从零到英雄...像专业人士一样发送 AWS SES 电子邮件!