构建一个仅在本地机器上运行的Web应用程序是一回事,而构建一个安全、能够连接真实数据库并且能让互联网上的任何人使用的Web应用程序则是完全不同的挑战。为此需要使用另一套工具。
大多数生产环境中的Web应用程序都有一些共同的需求:它们需要存储和检索数据,通过API暴露这些数据,要求用户在执行敏感操作之前进行身份验证,同时还需要将应用程序部署在可靠且响应速度快的环境中。
过去,要满足所有这些需求,通常需要管理服务器、配置数据库、处理身份验证相关基础设施,以及准备托管环境——而这些往往都是单独进行的、手动完成的任务。
AWS显著改变了这种模式。通过本教程中会使用的一系列服务(Lambda、DynamoDB、API Gateway、Cognito和CloudFront),你可以无需管理任何一台服务器,就能构建并部署一个功能完备、安全且全球分布式的应用程序。
每项服务都负责特定的任务:
-
DynamoDB用于存储数据
-
Lambda根据需求执行业务逻辑
-
API Gateway将函数以REST API的形式提供出来
-
Cognito负责用户身份验证
-
CloudFront通过HTTPS在全球范围内分发前端应用程序
AWS CDK(云开发工具包)将这些服务全部整合在一起,允许你用TypeScript代码来定义它们。你无需通过AWS控制台手动配置每一项资源,只需在一个文件中描述整个基础设施结构,然后通过一条命令即可完成部署。
完成本教程的学习后,你将能够构建出一个功能完备的供应商管理平台。用户可以注册、登录,然后创建、查看或删除供应商信息,所有数据都会安全地存储在AWS DynamoDB中,而所有的访问请求都会经过Amazon Cognito的身份验证机制进行保护。
你将构建什么
在本教程中,你将构建一个由两个页面组成的Web应用程序,经过身份验证的用户可以使用这个应用程序来:
-
添加新的供应商信息(包括名称、类别和联系邮箱)
-
实时查看所有保存的供应商信息
-
从列表中删除某个供应商
-
安全地登录或退出系统
前端应用程序是用Next.js构建的,后端服务则完全运行在AWS平台上:DynamoDB负责数据存储,Lambda函数处理业务逻辑,API Gateway提供REST API接口,Cognito负责身份验证,而CloudFront则通过HTTPS在全球范围内分发应用程序内容。
目录
适用人群
本教程适用于那些已经掌握基础JavaScript和React知识,但尚未使用过AWS的开发者。您不需要具备任何后端开发、云计算或DevOps方面的经验。在开始使用任何AWS相关功能之前,我都会先为大家详细解释这些概念。
先决条件
在开始学习之前,请确保您已经安装并准备好了以下工具:
-
Node.js 18或更高版本:点击此处下载
-
npm:随Node.js一同提供
-
代码编辑器:我推荐使用VS Code
-
终端:macOS、Linux或Windows系统上的任意终端(在Windows系统中建议使用WSL)
-
AWS账户:您将在第一部分中创建一个AWS账户。虽然需要信用卡信息,但本教程中的所有内容都可以在免费 tier范围内完成。
-
对React和TypeScript有基本了解
系统架构概述
在编写任何代码之前,先来了解一下这些组件是如何协同工作的。
当用户在React应用程序中点击“添加供应商”按钮时:
-
前端会从浏览器会话中读取用户的JWT认证令牌
-
然后通过
POST请求将令牌发送到API Gateway,同时会在请求头中包含该令牌 -
API Gateway会使用Cognito来验证该令牌的有效性。如果令牌无效或丢失,它会立即返回401错误并拒绝请求
-
如果令牌有效,API Gateway会将请求转发给createVendor Lambda函数
-
Lambda函数会将新添加的供应商信息存储到DynamoDB中
-
DynamoDB会确认数据写入成功,然后Lambda函数会返回成功响应
-
前端会重新获取供应商列表并更新用户界面
读取或删除供应商信息时,流程类似,只是使用的Lambda函数和HTTP方法会有所不同。

应用程序的部署方式:您的React应用程序会被打包成静态网站文件,上传到S3存储桶中,然后通过CloudFront在全球范围内提供服务。后端基础设施(包括Lambda函数、API Gateway、DynamoDB和Cognito)则是使用AWS CDK以TypeScript语言进行定义的,只需执行一条命令即可完成部署。
第一部分:设置您的AWS账户及所需工具
在开始编写任何应用程序代码之前,您需要准备三样东西:一个AWS账户、适合自己使用的开发工具,以及能够让这些工具代表您与AWS进行通信的认证信息。
1.1 创建您的AWS账户
如果您还没有AWS账户:
-
点击“创建AWS账户”按钮
-
按照注册提示完成操作,并添加一种支付方式
-
注册成功后,请登录AWS管理控制台
AWS提供了免费 tier,涵盖了本教程中使用的所有服务。在按照教程操作的过程中,正常使用这些服务是不会产生任何费用的。
1.2 安装AWS CLI和CDK
AWS CLI是一种命令行工具,它允许你通过终端与AWS进行交互:查看资源信息、配置凭证等等。
AWS CDK(云开发工具包)则是用于使用TypeScript代码来定义你的后端架构(包括数据库、Lambda函数和API)的工具。你无需通过AWS控制台逐一创建各种资源,只需在TypeScript文件中描述所需的内容,CDK就会为你完成相应的构建工作。
请同时安装以下两个工具:
# 在macOS上安装AWS CLI
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
# 对于Linux系统,请参考:https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html
# 对于Windows系统,请参考:https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-windows.html
# 全局安装AWS CDK
npm install -g aws-cdk
请确认这两个工具已经成功安装:
aws --version
cdk --version
如果这两个命令都能显示出相应的版本号,那就说明你已经准备好了继续下一步操作。
1.3 配置你的AWS凭证(IAM账户)
这一步非常重要。你的终端需要一组凭证——比如用户名和密码——才能代表你在AWS中执行各种操作。
可以把你的根账户(也就是你注册时使用的账户)看作是整个AWS账户的“主钥匙”。但在日常开发工作中,绝对不应该使用根账户进行操作。相反,你应该创建一个独立的IAM用户,并为其配置单独的凭证。如果这些凭证被泄露了,你可以直接删除它们,而不会影响到根账户的安全。
步骤1:创建一个IAM用户
-
登录AWS控制台,在顶部的搜索栏中输入“IAM”进行搜索
-
在左侧的侧边栏中,点击“Users”,然后选择“Create user”
-
将用户名称设置为
cdk-dev。不要勾选“Provide user access to the AWS Management Console”——你只需要终端访问权限,不需要控制台访问权限 -
在权限设置页面中,选择“Attach policies directly”选项

- 搜索“AdministratorAccess”选项,并将其旁边的复选框选中
关于权限设置需要注意的一点是:在正式的生产环境中,你会使用限制更严格的策略。但对于本教程来说,由于CDK需要创建多种类型的AWS资源,因此必须启用Administrator访问权限。
6. 点击页面底部的“Create user”按钮完成操作
步骤2:生成访问密钥
-
从“用户”列表中点击你新创建的
cdk-dev账户。 -
进入“安全凭证”选项卡。
-
向下滚动到“访问密钥”部分,然后点击“创建访问密钥”。
-
选择“命令行接口(CLI)”,勾选确认框,然后点击“下一步”。
-
最后再次点击“创建访问密钥”。
重要提示:现在就将访问密钥ID和秘密访问密钥都复制下来。关闭此页面后,您将再也无法看到秘密访问密钥了。请将这两个值保存在密码管理工具或安全笔记中。

第三步:将终端连接到AWS
在您的终端中运行以下命令:
aws configure
系统会要求您输入四个值:
AWS访问密钥ID: [请输入您的访问密钥ID]
AWS秘密访问密钥: [请输入您的秘密访问密钥]
默认区域名称: us-east-1
默认输出格式: json
在本教程中,请使用us-east-1作为区域。完成这一步后,您后续运行的所有CDK和AWS CLI命令都会自动使用这些凭据。
第二部分:设置项目结构
您将采用单仓库结构——一个顶层文件夹中包含两个子项目:frontend用于存放React应用程序代码,backend用于存放AWS基础设施相关代码。这两个项目会独立部署,但会共存于同一个系统中。
2.1 创建工作区
mkdir vendor-tracker && cd vendor-tracker
mkdir backend frontend
2.2 初始化前端项目(Next.js)
进入frontend文件夹,然后运行以下命令:
cd frontend
npx create-next-app@latest .
按照提示选择相应的选项:
-
TypeScript –>> 是
-
ESLint –>> 是
-
Tailwind CSS –>> 是
-
src/目录 –>> 否
-
应用路由器 –>> 是
-
导入别名 –>> 否
2.3 初始化后端项目(CDK)
进入backend文件夹,然后运行以下命令:
cd ../backend
cdk init app --language typescript
这样就会生成一个基本的CDK项目。其中最重要的文件是backend/lib/backend-stack.ts,您需要在这个文件中用TypeScript代码定义所有的AWS基础设施相关配置。
另外,请安装esbuild,因为CDK会使用它来打包您的Lambda函数:
npm install --save-dev esbuild
2.4 在编写代码之前了解CDK的工作原理
CDK可能与您以前使用过的大多数工具有所不同。它的运作方式如下:
通常情况下,您需要通过AWS控制台来创建各种资源:在这里创建表格,在那里配置Lambda函数。而CDK允许您使用TypeScript代码来完成这些操作。
当你运行 `cdk deploy` 时,CDK会读取你的 TypeScript 文件,将其转换为 AWS CloudFormation 模板(这是一种用于描述基础设施的 AWS 内部格式),然后提交给 AWS。AWS 会根据这些模板创建你所定义的所有资源。
在本教程中,你会遇到一些术语:
-
Stack:指你定义的所有 AWS 资源的集合。你的 `
BackendStack` 类就代表这样一个 Stack。 -
Construct:在 Stack 中创建的每一个单独的 AWS 资源(例如表格、Lambda 函数或 API)都被称为 Construct。
-
Deploy:运行 `
cdk deploy` 会将你的 TypeScript 定义发送给 AWS,从而创建或更新相应的实际资源。
你主要需要操作的文件是 `backend/lib/backend-stack.ts`。可以把它看作是你整个后端系统的蓝图。
最终的项目结构会如下所示:
vendor-tracker/
├── backend/
│ ├── lambda/
│ │ ├── createVendor.ts
│ │ ├── getVendors.ts
│ │ └── deleteVendor.ts
│ ├── lib/
│ │ └── backend-stack.ts
│ └── package.json
└── frontend/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ └── providers.tsx
├── lib/
│ └── api.ts
├── types/
│ └── vendor.ts
└── .env.local
第三部分:定义数据库(DynamoDB)
DynamoDB 是 AWS 提供的 NoSQL 数据库。它可以被看作是一种快速且可扩展的云中键值存储系统。在 DynamoDB 表中,每一条记录都必须拥有一个唯一的标识符,这个标识符被称为 分区键。对于你的供应商信息表来说,这个分区键就是 `vendorId`。
打开文件 `backend/lib/backend-stack.ts`,并将其中的全部内容替换为以下代码:
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
export class BackendStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// 1. DynamoDB 表
const vendorTable = new dynamodb.Table(this, 'VendorTable', {
partitionKey: {
name: 'vendorId',
type: dynamodb.AttributeType.STRING,
},
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY, // 仅适用于开发环境
});
}
}
每行代码的作用:
-
partitionKey指定了 `vendorId` 是每条记录的唯一标识符。没有任何两个供应商可以拥有相同的vendorId。 -
PAY_PER_REQUEST表示只有当数据真正被读取或写入时,才会产生费用。如果表格处于空闲状态,则不会产生任何费用,因此这种配置非常适合用于学习用途。 -
RemovalPolicy.DESTROY意味着当你运行 `cdk destroy` 时,该表格将会被删除。在实际的生产环境中,不建议使用这种设置。
第4部分:编写Lambda函数
Lambda函数其实就是你的服务器,但与传统服务器不同,它只有在被调用时才会运行。AWS会根据需求启动它,运行你的代码,然后将其关闭。你只需要为实际运行代码的时间支付费用。
你需要编写三个Lambda函数:
-
createVendor.ts:向DynamoDB中添加新的供应商信息 -
getVendors.ts:从DynamoDB中检索所有供应商信息 -
deleteVendor.ts:根据ID从DynamoDB中删除供应商信息
在backend目录下创建一个新文件夹:
mkdir backend/lambda

关于AWS SDK的说明
这三个Lambda函数都使用了AWS SDK v3(@aws-sdk/client-dynamodb和@aws-sdk/lib-dynamodb)。这是当前的标准版本。虽然也存在较旧版本的SDK(aws-sdk),但它已经被弃用,也不会被包含在Node.js 18的Lambda运行环境中,而你使用的正是这个版本。因此,请在整个开发过程中始终使用v3版本。
4.1 创建供应商信息处理Lambda函数
创建backend/lambda/createVendor.ts文件:
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "crypto";
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
export const handler = async (event: any) => {
try {
const body = JSON.parse(event.body);
const item = {
vendorId: randomUUID(), // 生成一个唯一且不会重复的ID
name: body.name,
category: body.category,
contactEmail: body.contactEmail,
createdAt: new Date().toISOString(),
};
await docClient.send(
new PutCommand({
TableName: process.env.TABLE_NAME!, // 从环境变量中读取表名
Item: item,
})
);
return {
statusCode: 201,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type,Authorization",
"Access-Control-Allow-Methods": "OPTIONS,POST,GET,DELETE",
},
body: JSON.stringify({ message: "供应商信息已创建", vendorId: item.vendorId }),
};
} catch (error) {
console.error("创建供应商信息时出现错误:", error);
return {
statusCode: 500,
headers: { "Access-Control-Allow-Origin": "*" },
body: JSON.stringify({ error: "无法创建供应商信息" }),
};
}
};
各部分的功能说明:
-
randomUUID()使用Node.js内置的crypto模块生成一个全球唯一的ID。这个方法不需要额外安装任何包,而且比Date.now()更可靠,因为后者在同一毫秒内收到两个请求时可能会生成重复的ID。 -
process.env.table_NAME从环境变量中读取DynamoDB表的名称。你可以在CDK堆栈中设置这个值,这样就可以避免在Lambda代码中硬编码表名了。 -
headers块对于实现CORS(跨源资源共享)是必需的。如果没有Access-Control-Allow-Origin头,浏览器会拒绝来自不同域名的响应;如果没有Access-Control-Allow-Headers头,后来为Cognito添加的Authorization头也会在浏览器的预检阶段被拒绝。
4.2 获取供应商信息的相关Lambda函数
创建文件backend/lambda/getVendors.ts:
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
export const handler = async () => {
try {
const response = await docClient.send(
new ScanCommand({
TableName: process.env.TABLE_NAME!,
})
);
return {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type,Authorization",
"Content-Type": "application/json",
},
body: JSON.stringify(response.Items ?? []);
};
} catch (error) {
console.error("获取供应商信息时出现错误:", error);
return {
statusCode: 500,
headers: { "Access-Control-Allow-Origin": "*" },
body: JSON.stringify({ error: "无法获取供应商信息" }),
};
}
};
各部分的功能:
-
ScanCommand用于读取表中的所有数据,并将它们以数组的形式返回。对于一个学习项目来说,这种做法是可以接受的;但在一个包含数百万条记录的生产环境中,应该使用更加精确的QueryCommand来避免在每次请求时都读取整个表格。 -
response.Items ?? []如果表中没有任何数据,就会返回一个空数组,这样前端在遇到这种情况时就不会出现错误。
4.3 删除供应商信息的相关Lambda函数
创建文件backend/lambda/deleteVendor.ts:
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, DeleteCommand } from "@aws-sdk/lib-dynamodb";
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
export const handler = async (event: any) => {
try {
const body = JSON.parse(event.body);
const { vendorId } = body;
if (!vendorId) {
return {
statusCode: 400,
headers: { "Access-Control-Allow-Origin": "*" },
body: JSON.stringify({ error: "需要提供vendorId" }),
};
}
await docClient.send(
new DeleteCommand({
TableName: process.env.TABLE_NAME!,
Key: { vendorId },
})
);
return {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type,Authorization",
"Access-Control-Allow-Methods": "OPTIONS,POST,GET,DELETE",
},
body: JSON.stringify({ message: "供应商信息已成功删除" }),
};
} catch (error) {
console.error("删除供应商信息时出现错误:", error);
return {
statusCode: 500,
headers: { "Access-Control-Allow-Origin": "*" },
body: JSON.stringify({ error: "无法删除供应商信息" }),
};
}
};
各部分的功能:
-
DeleteCommand用于删除那些vendorId与您提供的值相匹配的条目。如果该条目不存在,DynamoDB不会返回错误信息,而会直接忽略这一操作。 -
在代码的开头添加了
400错误处理机制,这样当调用者忘记提供vendorId时,系统会直接返回明确的错误信息,而不会让DynamoDB抛出令人困惑的内部错误。
第5部分:使用API Gateway构建API
API Gateway为您的Lambda函数提供了公共访问地址。如果没有它,浏览器就无法触发Lambda函数。可以把API Gateway看作是后端的“前门”:它接收HTTP请求,检查调用者是否具有访问权限,然后将请求路由到相应的Lambda函数,并将函数的响应返回给调用者。
现在您需要在backend/lib/background-stack.ts文件中将所有这些组件连接起来。
5.1 将Lambda函数和API Gateway添加到后台架构中
请用以下完整的代码替换backend/lib/background-stack.ts文件中的所有内容:
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
export class BackendStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// 1. DynamoDB表
const vendorTable = new dynamodb.Table(this, 'VendorTable', {
partitionKey: {
name: 'vendorId',
type: dynamodb.AttributeType.STRING,
},
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// 2. Lambda函数
const lambdaEnv = { TABLE_NAME: vendorTable.tableName };
const createVendorLambda = new NodejsFunction(this, 'CreateVendorHandler', {
entry: 'lambda/createVendor.ts',
handler: 'handler',
environment: lambdaEnv,
});
const getVendorsLambda = new NodejsFunction(this, 'GetVendorsHandler', {
entry: 'lambda/getVendors.ts',
handler: 'handler',
environment: lambdaEnv,
});
const deleteVendorLambda = new NodejsFunction(this, 'DeleteVendorHandler', {
entry: 'lambda/deleteVendor.ts',
handler: 'handler',
environment: lambdaEnv,
});
// 3. 权限设置(最小权限原则)
vendorTable.grantWriteData(createVendorLambda);
vendorTable.grantReadData(getVendorsLambda);
vendorTable.grantWriteData(deleteVendorLambda);
// 4. API Gateway
const api = new apigateway.RestApi(this, 'VendorApi', {
restApiName: 'Vendor Service',
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
allowMethods: apigateway.Cors.ALL_METHODS,
allowHeaders: ['Content-Type', 'Authorization'],
},
});
const vendors = api.root.addResource('vendors');
vendors.addMethod('POST', new apigateway.LambdaIntegration(createVendorLambda));
vendors.addMethod('GET', new apigateway.LambdaIntegration(getVendorsLambda));
vendors.addMethod('DELETE', new apigateway.LambdaIntegration(deleteVendorLambda));
// 5. 输出结果
new cdk.CfnOutput(this, 'ApiEndpoint', {
value: api.url,
});
}
}
各部分的功能:
NodejsFunction是一种特殊的CDK构建工具,它会使用esbuild自动将你的Lambda代码及其所有依赖项打包成一个文件,然后再将其上传到AWS。这就是为什么在第二部分中你需要安装esbuild的原因。
请始终使用NodejsFunction,而不是基本的lambda.Function构造。基本版本需要你手动进行代码打包操作,这样在运行时就容易出现“模块未找到”的错误。
权限设置(最小权限原则):在AWS中,默认情况下没有任何资源能够与其他资源进行交互。除非你明确授予权限,否则Lambda函数无法访问DynamoDB、S3或其他任何服务。
这就是所谓的“最小权限原则”:系统中的每个组件只被赋予它所需要的权限,而不会获得额外的权限。grantWriteData允许Lambda函数写入和删除数据,grantReadData则允许Lambda函数读取数据。为每个函数分别设置权限,就能确保getVendors Lambda函数永远不会意外地删除数据。
CfnOutput会在cdk deploy命令执行完成后在终端中显示相应的结果。你可以使用ApiEndpoint这个URL来配置前端应用。
第6部分:将后端部署到AWS
你的基础设施已经完全通过代码定义好了,现在你将把它们部署到AWS上,并获得一个可使用的API地址。
6.1 初始化你的AWS环境
在首次使用CDK进行部署之前,AWS需要在你的账户中创建一个用于存储Lambda代码包及其他资源的S3桶。这个设置步骤被称为“初始化环境”,每个AWS账户在每个区域都只需要执行一次。
在backend文件夹内运行以下命令:
cdk bootstrap
重要提示:初始化环境操作是针对特定区域进行的。如果你更换了AWS区域,就需要在该新区域内再次执行cdk bootstrap命令。
6.2 进行部署
运行以下命令:
cdk deploy
CDK会显示即将创建的所有资源的详细信息,并请求你确认是否继续。输入y并按回车键即可。
部署完成后,你会在终端中看到 Outputs部分,其中会包含API地址:
Outputs:
BackendStackApiEndpoint = https://abcdef123.execute-api.us-east-1.amazonaws.com/prod/
请将这个URL保存下来,后续构建前端应用时会需要用到它。
6.3 故障排除:如何查看AWS错误日志
在实际部署过程中,很少会有事情一次就能顺利完成。如果部署后出现了问题,以下方法可以帮助你找到具体的错误信息。
错误代码:502 Bad Gateway
502错误表示API网关接收到了你的请求,但Lambda函数在响应之前就已经崩溃了。最常见的原因就是缺少某个环境变量——例如,如果TABLE_NAME这个参数传入的值不正确,Lambda函数就无法找到相应的表。
要找到实际的错误信息,请使用CloudWatch Logs:
-
登录AWS控制台,然后搜索CloudWatch。
-
在左侧边栏中,点击“Logs” -> “Log groups”。

-
找到名为
/aws/lambda/BackendStack-CreateVendorHandler...的日志组。 -
点击最新的日志流。
-
阅读错误信息,它会清楚地告诉你出了什么问题。
以下是两种常见的错误信息及其解决方法:
-
Runtime.ImportModuleError:你的Lambda函数找不到某个模块。请确保在CDK堆栈中使用了NodejsFunction(而不是lambda.Function)。NodejsFunction会自动打包依赖项;而lambdaFUNCTION则不会。 -
AccessDeniedException:你的Lambda函数试图访问DynamoDB,但没有相应的权限。请检查你的堆栈中是否为该Lambda函数配置了正确的grantWriteData或grantReadData权限。
第7部分:构建React前端界面
你的后端服务已经上线了。现在你需要构建与它交互的React用户界面。
7.1 定义“供应商”类型
在编写任何API或组件代码之前,首先需要在TypeScript中定义“供应商”这个概念。这样可以在整个前端代码中确保类型安全。
创建文件frontend/types/vendor.ts:
export interface Vendor {
vendorId?: string; // 创建时可选——Lambda函数会自动生成这个ID
name: string;
category: string;
contactEmail: string;
createdAt?: string;
}
之所以将vendorId?标记为可选,是因为在创建新的“供应商”对象时,此时还没有ID;而createVendor Lambda函数会生成这个ID。当通过API读取“供应商”信息时,vendorId字段一定是存在的。
7.2 创建API服务层
不要在React组件中直接编写fetch调用语句,而是将所有的API逻辑集中放在一个文件中。这种设计模式被称为服务层。这样做可以让组件结构更加清晰,并且便于统一修改所有API调用相关的代码。
首先,在frontend文件夹下创建一个.env.local文件,用于存储API地址:
# frontend/.env.local
NEXT_PUBLIC_API_URL=https://abcdef123.execute-api.us-east-1.amazonaws.com/prod
请将其中的URL替换为cdk deploy命令输出结果中得到的ApiEndpoint值。Next.js要求在环境变量前加上NEXT_PUBLIC_前缀,这样才能让浏览器能够识别这个环境变量。
你可能会想:为什么不直接将URL硬编码到代码中呢?如果你将API地址直接粘贴到代码中并上传到GitHub,那么这个地址就会被公开。虽然单独的API地址并不会暴露你的数据(Cognito会对此进行保护),但将URL和敏感信息放在源代码控制之外仍然是良好的实践。请始终使用`.env.local`文件,并将其添加到`.gitignore`列表中。
确保`.env.local`文件在`.gitignore`列表中:
echo ".env.local" >> frontend/.gitignore
现在创建`frontend/lib/api.ts`文件:
import { Vendor } from '@/types/vendor';
const BASE_URL = process.env.NEXT_PUBLIC_API_URL!;
export const getVendors = async (): Promise => {
const response = await fetch(`${BASE_URL}/vendors`);
if (!response.ok) throw new Error('获取供应商信息失败');
return response.json();
};
export const createVendor = async (vendor: Omit): Promise => {
const response = await fetch(`${BASE_URL}/vendors`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(vendor),
});
if (!response.ok) throw new Error('创建供应商信息失败');
};
export const deleteVendor = async (vendorId: string): Promise => {
const response = await fetch(`${BASE_URL}/vendors`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vendorId }),
});
if (!response.ok) throw new Error('删除供应商信息失败');
};
各部分的功能说明:
-
Omit表示createVendor函数可以接受没有ID或创建时间戳的供应商对象——这些信息是由服务器端生成的。 -
if (!response.ok) throw new Error(...)这一代码确保了任何HTTP错误(4xx或5xx码)都会以JavaScript错误的形式在组件中显示出来,这样你就可以向用户展示有意义的提示信息,而不会让程序默默地出现故障。
在第八部分中,你会对这些函数进行修改,以便在其中加入Cognito认证令牌。
7.3 构建主页面
现在来创建主页面组件。这个组件包含一个用于添加供应商信息的表单,以及一个用于显示当前所有供应商信息的列表。
将`frontend/app/page.tsx`文件的内容替换为以下内容:
供应商管理工具
你的供应商信息存储在AWS DynamoDB中。
{error && (
错误信息:{error}
)}
{/* ── 添加供应商表单 ── */}
添加新供应商
{/* ── 供应商列表 ── */}
当前供应商列表
{vendors.length === 0 ? (
目前还没有供应商信息。请使用表单添加新的供应商。
) : (
vendors.map(v => (
名称:{v.name}
类别:{v.category} · 联系邮箱:{v.contactEmail}
))}
)
);
}
该组件的关键点:
-
在代码顶部使用的 `
'use client'` 是 Next.js 的一个指令。它告诉 Next.js,该组件使用了浏览器 API(`useState`、`useEffect` 以及事件处理程序),因此必须在浏览器中运行,而不能在服务器端进行预渲染。 -
在 `handleSubmit` 函数中使用的 `
e.preventDefault()` 可以阻止浏览器的默认表单提交行为,因为这种行为会导致页面完全重新加载,进而使 React 中的状态数据丢失。 -
每次调用 `createVendor` 或 `deleteVendor` 方法后,都会再次执行 `loadVendors()` 函数。这个函数会从 DynamoDB 中重新获取最新数据,这样用户界面显示的内容就会与数据库中存储的实际数据保持一致。
7.4 在本地测试应用程序
启动你的 Next.js 开发服务器:
cd frontend
npm run dev
在浏览器中访问 http://localhost:3000,你应该会看到由两个面板组成的界面。试着添加一个供应商信息,确认它是否真的出现在列表中。


验证与 AWS 的连接
打开 Chrome 开发工具(按 F12 键),然后点击“网络”选项卡。当你添加一个供应商信息时,应该会看到以下操作:
-
向你的 AWS API 地址发送一个
POST请求,请求会返回 201 状态码。 -
还会收到一个
GET请求,响应内容为更新后的供应商列表,状态码为 200。
你也可以通过打开 AWS 控制台,然后导航到 DynamoDB –> Tables –> VendorTable –>> Explore table items 来验证数据是否已被成功保存。你的供应商信息应该会出现在那里。
第 8 部分:使用 Amazon Cognito 添加身份认证功能
目前,你的 API 是完全开放的,任何找到该 API 地址的人都可以添加或删除供应商信息。你可以使用 Amazon Cognito 来解决这个问题。
Cognito 是 AWS 提供的身份认证服务。它负责管理用户池——这个用户池是一个存储了注册用户的用户名和密码的数据库。当用户登录时,Cognito 会生成一个 JWT(JSON Web Token):这种经过加密处理的字符串能够证明用户的身份。你的 API Gateway 在处理每个请求时都会检查这个令牌;如果没有有效的令牌,用户就无法访问相关资源。
什么是 JWT? JSON Web Token 是一种格式类似于 eyJhbGci... 的字符串。它包含了关于用户的加密信息,并且由 Cognito 使用密钥进行了签名处理。
API Gateway能够在每次请求时都不需要与Cognito进行通信即可验证签名,因此令令牌校验过程变得非常快速。可以将它想象成一种防篡改的标识:任何人都能看到上面写着的名称,但只有Cognito生成的签名才能使该标识具有法律效力。
8.1 将Cognito添加到CDK堆栈中
打开backend/lib/backend-stack.ts文件,并对其进行修改以加入Cognito相关配置。以下是修改后的完整代码:
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
export class BackendStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// ─── 1. DynamoDB表 ────────────────────────────────────────────────────
const vendorTable = new dynamodb.Table(this, 'VendorTable', {
partitionKey: { name: 'vendorId', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// ─── 2. Lambda函数 ──────────────────────────────────────────────────
const lambdaEnv = { TABLE_NAME: vendorTable.tableName };
const createVendorLambda = new NodejsFunction(this, 'CreateVendorHandler', {
entry: 'lambda/createVendor.ts',
handler: 'handler',
environment: lambdaEnv,
});
const getVendorsLambda = new NodejsFunction(this, 'GetVendorsHandler', {
entry: 'lambda/getVendors.ts',
handler: 'handler',
environment: lambdaEnv,
});
const deleteVendorLambda = new NodejsFunction(this, 'DeleteVendorHandler', {
entry: 'lambda/deleteVendor.ts',
handler: 'handler',
environment: lambdaEnv,
});
// ─── 3. 权限设置 ───────────────────────────────────────────────────────
vendorTable.grantWriteData(createVendorLambda);
vendorTable.grantReadData(getVendorsLambda);
vendorTable.grantWriteData(deleteVendorLambda);
// ─── 4. Cognito用户池 ─────────────────────────────────────────────────
const userPool = new cognito.UserPool(this, 'VendorUserPool', {
selfSignUpEnabled: true,
signInAliases: { email: true },
autoVerify: { email: true },
userVerification: {
emailStyle: cognito.VerificationEmailStyleCODE,
},
});
// 需要添加此配置以便托管Cognito的内部认证接口
userPool.addDomain('VendorUserPoolDomain', {
cognitoDomain: {
domainPrefix: `vendor-tracker-${this.account}`,
},
});
const userPoolClient = userPool.addClient('VendorAppClient');
// ─── 5. API Gateway与授权器 ──────────────────────────────────────────
const api = new apigateway.RestApi(this, 'VendorApi', {
restApiName: 'Vendor Service',
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
allowMethods: apigateway.Cors.ALL_METHODS,
allowHeaders: ['Content-Type', 'Authorization'],
},
});
const authorizer = new apigateway.CognitoUserPoolsAuthorizer(
this,
'VendorAuthorizer',
{ cognitoUserPools: [userPool] }
);
const authOptions = {
authorizer,
authorizationType: apigateway AuthorizationType.COGNITO,
};
const vendors = api.root.addResource('vendors');
vendors.addMethod('GET', new apigateway.LambdaIntegration(getVendorsLambda), authOptions);
vendors.addMethod('POST', new apigateway.LambdaIntegration(createVendorLambda), authOptions);
vendors.addMethod('DELETE', new apigateway.LambdaIntegration(deleteVendorLambda), authOptions);
// ─── 6. 输出结果 ───────────────────────────────────────────────────
new cdk.CfnOutput(this, 'ApiEndpoint', { value: api.url });
new cdk.CfnOutput(this, 'UserPoolId', { value: userPool.userPoolId });
new cdk.CfnOutput(this, 'UserPoolClientId', { value: userPoolClient.userPoolClientId });
}
}

发生了哪些变化:
-
CognitoUserPoolsAuthorizer会指示API Gateway在将任何请求传递给Lambda之前,先检查该请求是否包含有效的Cognito JWT令牌。如果令牌缺失或无效,API Gateway会立即以401 Unauthorized响应拒绝该请求,而根本不会触发你的Lambda函数。 -
authOptions这一配置会应用于GET、POST和DELETE这三种API方法,因此现在所有路由都受到了保护。 -
autoVerify: { email: true }这一设置会使得Cognito在用户通过验证码确认邮箱地址有效后,将该邮箱属性标记为已验证状态。不过系统并不会跳过发送验证邮件这一步骤,因为用户仍然会收到验证码。如果你在开发阶段希望跳过验证流程,可以在Cognito控制台中手动完成用户验证操作(具体方法详见第8.5节)。 -
在下一次部署之后,你的终端界面将会显示两个新的
CfnOutput参数值:UserPoolId和UserPoolClientId。你的前端应用程序需要这些信息才能与Cognito服务进行交互。
请部署更新后的代码栈:
cd backend
cdk deploy
部署完成后,终端输出中会包含以下三个值:
Outputs:
BackendStackApiEndpoint = https://abc123.execute-api.us-east-1.amazonaws.com/prod/
BackendStack.UserPoolId = us-east-1_xxxxxxxx
BackendStack.UserPoolClientId = xxxxxxxxxxxxxxxxxxxx
请将这三个值保存下来,后续步骤会需要用到它们。
8.2 安装并配置AWS Amplify
AWS Amplify是一个前端开发工具库,它可以帮你处理所有复杂的认证逻辑:它负责管理登录界面、在用户的浏览器中存储令牌、自动更新过期的令牌,并提供简单的API接口来获取当前用户的会话信息。
请将Amplify相关库文件安装到你的frontend文件夹中:
cd frontend
npm install aws-amplify @aws-amplify/ui-react
创建文件frontend/appproviders.tsx,该文件会使用你的Cognito配置信息来初始化Amplify。应用程序加载时,这个文件会自动执行一次:
'use client';
import { Amplify } from 'aws-amplify';
Amplify.configure(
{
Auth: {
Cognito: {
userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID!,
userPoolClientId: process.env.NEXT_public_USER_POOL_CLIENT_ID!,
},
},
},
{ ssr: true }
);
export function Providers({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
请将你的Cognito用户名和ID添加到frontend/.env.local文件中:
NEXT_PUBLIC_API_URL=https://abc123.execute-api.us-east-1.amazonaws.com/prod
NEXT_public_USER_POOL_ID=us-east-1_xxxxxxxx
NEXT_PUBLIC_USER_POOL_CLIENT_ID=xxxxxxxxxxxxxxxxxxxx
请将其中的值替换为执行`cdk deploy`命令后得到的结果。
8.3 将相关组件集成到应用布局中
这一步非常关键。在任何组件尝试使用认证功能之前,必须先初始化Amplify。如果跳过这一步,`fetchAuthSession()`方法会抛出“Amplify未配置”的错误,导致所有功能都无法正常使用。
打开`frontend/app/layout.tsx`文件,将其修改为用`Providers`组件包裹整个应用结构:
import type { Metadata } from 'next';
import './globals.css';
import { Providers } from './providers';
export const metadata: Metadata = {
title: '供应商管理工具',
description: '使用AWS来管理您的供应商信息。',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
);
}
通过用`
8.4 使用withAuthenticator保护用户界面
现在需要对`Home`组件进行相应的处理,这样未登录的用户看到的将会是登录页面,而不是控制面板。
将`frontend/app/page.tsx`文件的内容替换为以下更新后的版本:
'use client';
import { useState, useEffect } from 'react';
import { withAuthenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import { getVendors, createVendor, deleteVendor } from '@/lib/api';
import { Vendor } from '@/types/vendor';
// withAuthenticator会自动将`signOut`和`user`作为属性注入到函数中
function Home({ signOut, user }: { signOut?: () => void; user?: any }) {
const [vendors, setVendors] = useState:
const [form, setForm] = useState({ name: '', category: '', contactEmail: '' });
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const loadVendors = async () => {
try {
const data = await getVendors();
setVendors(data);
} catch {
setError('加载供应商信息失败。');
}
};
useEffect(() => {
loadVendors();
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
await createVendor(form);
setForm({ name: '', category: '', contactEmail: '' });
await loadVendors();
} catch {
setError('添加供应商信息失败。');
} finally {
setLoading(false);
}
};
const handleDelete = async (vendorId: string) => {
try {
await deleteVendor(vendorId);
await loadVendors();
} catch {
setError('删除供应商信息失败。');
}
};
return (
{/* ── 头部导航 ── */}
供应商管理工具
当前登录用户:{user?.signInDetails?.loginId}
{error && (
)}
添加新供应商
{/* ── 供应商列表 ── */}
当前供应商数量:{vendors.length}
目前还没有供应商信息。
) : (
vendors.map(v => (
供应商名称:{v.name}
类别:{v.category} · 联系邮箱:{v.contactEmail}
))}
)}
);
}
// 使用withAuthenticator包装Home组件后,未登录的用户将会看到Amplify提供的登录/注册界面,
// 而不是这个自定义的首页组件。
export default withAuthenticator(Home);

8.5 在API调用中传递认证令牌
由于API Gateway现在要求每个请求都必须包含JWT令牌,因此你的fetch调用需要在Authorization头部中添加该令牌。如果没有这个令牌,所有请求都会返回401 Unauthorized错误。
请更新frontend/lib/api.ts文件,在其中加入用于处理令牌的辅助函数,并修改相关的fetch调用代码:
import { fetchAuthSession } from 'aws-amplify/auth';
import { Vendor } from '@/types/vendor';
const BASE_URL = process.env.NEXT_PUBLIC_API_URL!;
// 从当前激活的Amplify会话中获取用户的JWT令牌
const getAuthToken = async (): Promise => {
const session = await fetchAuthSession();
const token = session.tokens?.idToken?.toString();
if (!token) throw new Error('没有激活的会话。请先登录。');
return token;
};
export const getVendors = async(): Promise => {
const token = await getAuthToken();
const response = await fetch(`${BASE_URL}/vendors`, {
headers: { Authorization: token },
});
if (!response.ok) throw new Error('获取供应商信息失败');
return response.json();
};
export const createVendor = async (
vendor: Omit>
): Promise => {
const token = await getAuthToken();
const response = await fetch(`${BASE_URL}/vendors`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: token,
},
body: JSON.stringify(vendor),
});
if (!response.ok) throw new Error('创建供应商信息失败');
};
export const deleteVendor = async (vendorId: string): Promise => {
const token = await getAuthToken();
const response = await fetch(`${BASE_URL}/vendors`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: token,
},
body: JSON.stringify({ vendorId }),
});
if (!response.ok) throw new Error('删除供应商信息失败');
};
getAuthToken的作用是:
fetchAuthSession()会从浏览器中读取当前登录用户的会话信息。在用户登录后,Amplify会将该会话信息存储在内存以及localStorage中。
session.tokens?.idToken就是API Gateway的Cognito授权系统所需要的数据。将这个令牌作为Authorization头部发送出去,就可以告诉API Gateway:“这个请求是由已认证的用户发出的。”
8.6 解决Cognito相关问题
注册后出现“未确认”用户状态的问题
当新用户通过Amplify UI进行注册时,Cognito会将该账户标记为未确认状态,直到用户验证自己的电子邮件地址。系统会向用户的邮箱发送验证码,用户输入验证码后,账户状态才会变为已确认,此时用户就可以登录了。
如果您在本地进行测试,并希望跳过电子邮件验证步骤,可以在AWS控制台中手动确认相关账户的状态:
-
打开AWS控制台,然后导航到Cognito页面
-
点击您的用户池(例如:
VendorUserPool...) -
选择“Users”选项卡
-
点击用户的电子邮件地址
-
打开下拉菜单,然后选择“Confirm account”


部署后出现“401未经授权”错误
如果遇到了401错误,请检查以下两点:
-
打开Chrome开发者工具,进入“网络”选项卡,点击出现错误的请求,查看请求头信息。你应该能看到一个包含长字符串的
Authorization头部字段。如果这个字段缺失,那么getAuthToken函数就会失败。请确认在providers.tsx文件中Amplify的配置是否正确,并且通过layout.tsx文件将其正确链接起来。 -
在您的CDK代码栈中,确保每个受保护的接口定义中都包含了
authorizationType: apigatewayAuthorizationType.COGNITO这一项。如果缺少这个配置,即使已经定义了授权机制,API Gateway也可能不会检查令牌。
第9部分:使用S3和CloudFront部署前端应用
您的应用程序在本地环境下可以正常运行。现在,您要将它部署到一个真正的HTTPS地址上,这样世界各地的任何人都可以访问这个应用。
部署策略:Next.js会将您的React应用程序生成为一组静态的HTML、CSS和JavaScript文件,这些文件会被上传到S3存储桶中(AWS提供的文件存储服务)。而CloudFront则作为内容分发网络,将这些文件分发到全球各地的服务器上,并通过HTTPS协议提供访问服务。
9.1 配置Next.js以进行静态资源导出
打开frontend/next.config.js(或next.config.mjs)文件,在其中添加output: 'export'这一配置项:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // 这种配置会生成静态的/out文件夹,而不是运行Node.js服务器
};
export default nextConfig;
关于“use client”配置与静态资源导出的关系:当设置output: 'export'时,Next.js会在编译阶段生成所有页面的静态文件。对于那些使用了仅适用于浏览器的API的组件(比如Amplify提供的withAuthenticator组件),必须在文件顶部添加use client指令。这样Next.js就会跳过对这些组件的服务器端渲染步骤,仅在浏览器环境中执行它们。
你已经在`page.tsx`文件中添加了`use client`这一代码。如果在未来遇到构建错误,提示“window未定义”之类的问题,请检查相关组件是否在文件顶部包含了`use client`这句话。
现在来构建前端代码:
cd frontend
npm run build
这样会生成一个名为`/out`的文件夹,其中包含你的整个网站的静态文件。请确认这个文件夹确实已经创建好了:
ls out
# 你应该能看到:index.html、_next/等文件。
9.2 将S3和CloudFront添加到CDK堆栈中
打开`backend/lib/backend-stack.ts`文件,然后添加相关的托管配置。以下是该文件的最终版本:
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
export class BackendStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super_scope, id, props);
// 1. DynamoDB表
const vendorTable = new dynamodb.Table(this, 'VendorTable', {
partitionKey: { name: 'vendorId', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// 2. Lambda函数
const lambdaEnv = { TABLE_NAME: vendorTable.tableName };
const createVendorLambda = new NodejsFunction(this, 'CreateVendorHandler', {
entry: 'lambda/createVendor.ts',
handler: 'handler',
environment: lambdaEnv,
});
const getVendorsLambda = new NodejsFunction(this, 'GetVendorsHandler', {
entry: 'lambda/getVendors.ts',
handler: 'handler',
environment: lambdaEnv,
});
const deleteVendorLambda = new NodejsFunction(this, 'DeleteVendorHandler', {
entry: 'lambda/deleteVendor.ts',
handler: 'handler',
environment: lambdaEnv,
});
// 3. 权限设置
vendorTable.grantWriteData(createVendorLambda);
vendorTable.grantReadData(getVendorsLambda);
vendorTable.grantWriteData(deleteVendorLambda);
// 4. Cognito用户池
const userPool = new cognito.UserPool(this, 'VendorUserPool', {
selfSignUpEnabled: true,
signInAliases: { email: true },
autoVerify: { email: true },
userVerification: {
emailStyle: cognito.VerificationEmailStyle.CODE,
},
});
userPool.addDomain('VendorUserPoolDomain', {
cognitoDomain: { domainPrefix: `vendor-tracker-${this.account}` },
});
const userPoolClient = userPool.addClient('VendorAppClient');
// 5. API网关与授权器
const api = new apigateway.RestApi(this, 'VendorApi', {
restApiName: 'Vendor Service',
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
allowMethods: apigateway.Cors.ALL_METHODS,
allowHeaders: ['Content-Type', 'Authorization'],
},
});
const authorizer = new apigateway.CognitoUserPoolsAuthorizer(
this,
'VendorAuthorizer',
{ cognitoUserPools: [userPool] }
);
const authOptions = {
authorizer,
authorizationType: apigateway AuthorizationType.COGNITO,
};
const vendors = api.root.addResource('vendors');
vendors.addMethod('GET', new apigateway.LambdaIntegration(getVendorsLambda), authOptions);
vendors.addMethod('POST', new apigateway.LambdaIntegration(createVendorLambda), authOptions);
vendors.addMethod('DELETE', new apigateway.LambdaIntegration(deleteVendorLambda), authOptions);
// 6. S3存储桶(用于存放前端文件)
const siteBucket = new s3.Bucket(this, 'VendorSiteBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// 7. CloudFront分发服务(提供HTTPS支持及内容分发)
const distribution = new cloudfront.Distribution(this, 'SiteDistribution', {
defaultBehavior: {
origin: new origins.S3Origin(siteBucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
defaultRootObject: 'index.html',
errorResponses: [
{
// 将所有404错误页面重定向到index.html,以便React能够处理路由逻辑
httpStatus: 404,
responseHttpStatus: 200,
responsePagePath: '/index.html',
},
],
});
// 8. 将前端文件部署到S3存储桶
new s3deploy.BucketDeployment(this, 'DeployWebsite', {
sources: [s3deploy.Source.asset('../frontend/out')]
», destinationBucket: siteBucket,
distribution,
distributionPaths: ['/*'], // 每次部署时都会清除CloudFront缓存
});
// 9. 输出结果
new cdk.CfnOutput(this, 'ApiEndpoint', { value: api.url });
new cdk.CfnOutput(this, 'UserPoolId', { value: userPool.userPoolId });
new cdk.CfnOutput(this, 'UserPoolClientId', { value: userPoolClient.userPoolClientId });
new cdk.CfnOutput(this, 'CloudFrontURL', {
value: `https://${distribution.distributionDomainName}`,
});
}
}
托管基础设施的作用:
-
S3存储桶用于存放您的静态HTML、CSS和JavaScript文件。这些文件是私有的,用户无法直接访问它们。
-
CloudFront是一种内容分发网络服务,它位于S3之前。它会为您提供一个HTTPS地址,并将您的文件缓存到全球各地的边缘节点上,因此无论用户身处何地,应用程序都能快速加载。
REDIRECT_TO_HTTPS功能会自动将所有HTTP请求转换为HTTPS请求。 -
当发生404错误时,错误响应会返回
index.html文件,而不是错误页面。这对于单页应用程序来说非常重要:如果用户直接访问像/vendors/123这样的路径,CloudFront找不到相应的文件,但返回index.html后,React应用程序就能正确处理路由请求。 -
distributionPaths: ['/*']这一设置会告诉CloudFront在每次部署后清除其所有的缓存数据,这样用户就能立即看到您应用程序的最新版本。 -
BucketDeployment是AWS CDK提供的一个功能,每当您运行cdk deploy命令时,它都会自动将frontend/out文件夹中的文件上传到S3存储桶中。
9.3 执行最终部署
首先,使用最新的环境变量构建前端代码:
cd frontend
npm run build
然后,从后端文件夹中部署所有内容:
cd ../backend
cdk deploy
部署完成后,从终端输出中复制CloudFrontURL地址:
Outputs:
BackendStack.CloudFrontURL = https://d1234abcd.cloudfront.net
在浏览器中打开这个地址,您的应用程序就已经正式上线了——它通过HTTPS协议提供服务,并且已经实现了全球范围内的分布部署。

您所构建的应用程序
现在,您已经拥有一个完整部署好的、具备生产环境级别的全栈应用程序。以下是您所构建的各个组件的功能概述:
| 层级 | 服务 | 功能 |
|---|---|---|
| 前端 | Next.js + CloudFront | 通过HTTPS在全球范围内提供React用户界面 |
| 认证 | Amazon Cognito + Amplify | 处理用户注册、登录以及JWT令牌的管理 |
| API接口 | API Gateway | 路由处理HTTP请求并验证认证令牌 |
| 逻辑处理 | AWS Lambda (×3) | 根据需求创建、读取或删除供应商相关信息 |
| 数据库 | DynamoDB | 无额外开销地存储供应商信息 |
| 存储服务 | S3 | 保存编译后的前端文件 |
| 基础设施 | AWS CDK | 以代码形式定义并部署上述所有组件 |
结论
你已经构建并部署了几乎所有云应用所依赖的基础架构模式:一个由数据库支持的安全API,并通过“基础设施即代码”的方式进行部署。你所完成的工作包括以下几点:
你搭建了一个专业的AWS开发环境,并配置了相应的IAM权限设置。你使用AWS CDK将整个后端基础设施定义成了TypeScript代码,这意味着你的数据库、API、Lambda函数以及认证系统都处于版本控制之下,而且可以通过一个命令即可完成部署。
你编写了三个Lambda函数,分别用于处理创建、读取和删除操作,每个函数都包含了适当的错误处理机制,并遵循了AWS SDK v3的标准架构。这些Lambda函数通过API Gateway与REST API相连,而所有的接口都使用了Amazon Cognito进行认证,因此只有已注册且经过验证的用户才能访问你的数据。
在前端层面,你使用Next.js构建了一个应用程序。该应用程序的服务层将API逻辑与UI组件清晰地分离开来,并通过AWS Amplify自动管理JWT令牌的生命周期。这样一来,用户无需编写任何与认证相关的UI代码,就能完成注册和登录流程。
最后,你将整个系统进行了部署:后端部分被部署到了AWS Lambda和DynamoDB上,而前端则作为静态网站通过CloudFront在全球范围内提供HTTPS服务。
本教程的完整源代码可以在GitHub上找到。你可以克隆这些代码,根据需要进行修改,然后将其作为自己项目的参考资料使用。

