作为一名全栈开发者,在构建现代应用程序时,我们几乎每天都在与认证(Authentication)和授权(Authorization)打交道。而在这个领域,OAuth 2.0 和 OpenID Connect (OIDC) 是我们绕不开的两个核心协议。然而,许多开发者,即便是经验丰富的开发者,也常常对这两个协议中的两个关键概念感到困惑:ID 令牌 (ID Token) 和 访问令牌 (Access Token)。它们看起来相似,都像是一串随机字符,都与用户身份和权限相关,但它们的用途、结构和生命周期却截然不同。错误地使用它们,可能会导致严重的安全漏洞或系统设计缺陷。
你是否也曾有过这样的疑问:为什么有了访问令牌,还需要ID令牌?它们究竟各自承载了什么信息?客户端(例如你的单页面应用)应该如何正确地处理它们?资源服务器(你的后端API)又该关心哪一个?这篇深度解析文章,将站在全栈开发者的实战视角,彻底剖析 ID 令牌与访问令牌的本质区别,并结合丰富的代码示例和实际应用场景,让你从此不再对它们感到模糊。我们将不仅仅停留在“是什么”的层面,更会深入探讨“为什么”和“怎么用”,帮助你构建更安全、更健壮的系统。
访问令牌 (Access Token): 通往资源的授权钥匙
让我们从更常见的访问令牌开始。如果你接触过任何需要调用第三方API(如GitHub API, Google Maps API)的开发,你一定对它不陌生。访问令牌是理解 OAuth 2.0 框架的核心,它的唯一使命就是——授权 (Authorization)。
什么是授权?
在技术语境下,授权指的是“允许某个实体(如一个应用)代表另一个实体(如一个用户)去执行某些特定操作或访问某些特定资源”。请注意这里的关键词:“代表”、“特定操作”。OAuth 2.0 的设计初衷,就是为了解决“委托授权”问题。例如,你允许一个第三方应用“X-Diagram”访问你的GitHub仓库列表来为你生成代码结构图,但你绝不希望它能删除你的仓库。这时,X-Diagram就需要一个代表你的、但权限受限的凭证,这个凭证就是访问令牌。
访问令牌的本质是一个凭证,它向资源服务器(Resource Server,例如 GitHub API)证明,持有该令牌的客户端(Client,例如 X-Diagram 应用)已经被资源所有者(Resource Owner,即你本人)授权,可以访问指定的资源或执行指定的操作(由 `scope` 定义)。
OAuth 2.0 核心思想
访问令牌的特点与用途
- 目的单一: 它的唯一目的是用于访问受保护的资源。客户端在向资源服务器发起请求时,必须在请求头中携带这个令牌,通常是使用 `Authorization: Bearer
` 的形式。 - 面向资源服务器: 访问令牌的“消费者”或“验证者”是资源服务器。客户端本身不应该,也通常无法解析访问令牌的内容。对客户端来说,它就是一个不透明的字符串,只需获取并妥善保管,然后在每次API请求时附加上即可。
- 生命周期通常较短: 出于安全考虑,访问令牌的有效期通常很短,可能是几分钟到几小时。这大大降低了令牌泄露后被恶意利用的风险。为了提供更好的用户体验,OAuth 2.0 引入了刷新令牌(Refresh Token)机制,用于在访问令牌过期后,无需用户重新登录即可获取新的访问令牌。
- 承载授权信息: 令牌本身(或者其在服务器端的记录)包含了授权的关键信息,例如:
- 被授权的客户端ID。
- 授权该操作的用户ID。
- 授权的范围(Scopes),如 `read:user`, `write:repo`。
- 过期时间。
访问令牌的两种主要格式
OAuth 2.0 规范本身并没有强制规定访问令牌的具体格式。这使得实现上具有很大的灵活性。实践中,主要有两种格式:
- 不透明令牌 (Opaque Tokens):
这是一种最简单的格式。令牌本身是一串没有特定结构、无法直接解析的随机字符串,例如 `v1.a2b3c4d5...`。当资源服务器收到这样的令牌时,它无法直接验证。它必须通过一个内部的、安全的通道,去询问授权服务器(Authorization Server):“这个令牌有效吗?它代表谁?有什么权限?” 这个过程称为令牌内省(Token Introspection)。
优点:
- 安全性高: 令牌本身不包含任何敏感信息,即使泄露,攻击者也无法直接获取用户信息或权限。
- 可随时撤销: 授权服务器可以在任何时候将某个令牌标记为无效,这种撤销会立即对所有资源服务器生效。
缺点:
- 性能开销: 每次API请求都需要资源服务器与授权服务器进行一次网络通信来验证令牌,这会增加延迟和系统复杂性,尤其是在微服务架构中。
- JSON Web Tokens (JWT):
JWT 是一种更现代的、自包含的令牌格式。它由三部分组成(头部、载荷、签名),使用点 `.` 分隔,并经过 Base64Url 编码。资源服务器收到 JWT 格式的访问令牌后,只要拥有授权服务器的公钥(或对称密钥),就可以在本地独立完成对令牌的验证,无需再次请求授权服务器。
一个典型的 JWT 载荷(Payload)可能包含如下信息:
{ "iss": "https://auth.example.com/", // 签发者 "sub": "user-123", // 主题,通常是用户ID "aud": "https://api.example.com", // 受众,即给哪个API用的 "iat": 1678886400, // 签发时间 "exp": 1678890000, // 过期时间 "client_id": "app-abc", // 客户端ID "scope": "read:data write:data" // 授权范围 }优点:
- 无状态与高性能: 资源服务器可以本地验证令牌,无需依赖授权服务器。这在分布式系统和微服务架构中极具优势,减少了服务间的耦合和网络延迟。
缺点:
- 撤销困难: 由于令牌是自包含的,一旦签发,在它过期之前就一直有效。要实现即时撤销,就需要引入复杂的机制,如维护一个黑名单,但这又违背了JWT无状态的初衷。
- 体积较大: 相比不透明令牌,JWT包含更多信息,因此体积更大,可能会对请求大小产生轻微影响。
如何使用访问令牌
在客户端代码中,使用访问令牌调用受保护的API是一个非常标准化的操作。以下是一个使用 JavaScript `fetch` API 的例子:
async function getUserProfile(accessToken) {
try {
const response = await fetch('https://api.example.com/v1/user/profile', {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`, // 核心在这里
'Content-Type': 'application/json'
}
});
if (!response.ok) {
// 如果是401 Unauthorized,可能意味着accessToken过期了
if (response.status === 401) {
console.error('Access token is invalid or expired. Time to refresh?');
// 在这里可以触发刷新令牌的逻辑
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('User profile:', data);
return data;
} catch (error) {
console.error('Failed to fetch user profile:', error);
}
}
// 假设你已经从授权服务器获取了访问令牌
const myAccessToken = 'eyJhbGciOiJSUzI1NiIs...';
getUserProfile(myAccessToken);
总结一下,访问令牌是关于“能做什么”的凭证。它的世界里只有客户端、资源服务器和被授予的权限。它回答了资源服务器的问题:“这个请求有权执行此操作吗?”
ID 令牌 (ID Token): 用户身份的证明文件
现在,让我们转向 ID 令牌。ID 令牌的出现,是为了解决 OAuth 2.0 本身的一个“短板”:OAuth 2.0 是一个授权框架,它本身并不关心用户的身份认证 (Authentication)。它能告诉你“某个应用可以代表用户A访问数据”,但它没有一个标准化的方式来告诉这个应用“当前这个用户确实是用户A”。
为了解决这个问题,OpenID Connect (OIDC) 诞生了。OIDC 并不是一个全新的协议,它只是在 OAuth 2.0 之上构建的一个薄薄的身份层。而这个身份层的核心产物,就是 ID 令牌。
ID 令牌的本质是一个安全令牌(具体来说,是一个JWT),它包含了关于认证事件的声明(Claims)。它向客户端证明用户的身份,并提供了关于用户认证过程的基本信息(例如用户何时、以何种方式完成认证)。
OpenID Connect 核心规范
ID 令牌的特点与用途
- 目的明确: 它的唯一目的是向客户端传递用户的身份信息。客户端收到 ID 令牌后,可以安全地认为用户已经通过了身份验证。
- 面向客户端: 与访问令牌不同,ID 令牌的“消费者”或“验证者”是客户端应用程序。客户端需要解析并验证 ID 令牌的签名和内容,从而获取用户的身份信息,例如用户ID、姓名、邮箱等。
- 格式固定: OIDC 规范强制规定,ID 令牌必须是 JWT 格式。这使得客户端可以用标准化的方式来解析和验证它。
- 绝不用于API授权: 这是一个至关重要的原则。永远不要将 ID 令牌作为 Bearer Token 发送给资源服务器(API)用于授权。API应该只信任访问令牌。ID 令牌是给客户端看的,不是给API看的。
- 承载身份信息: ID 令牌的 JWT 载荷包含了一系列标准化的声明(Claims),用于描述用户身份和认证事件。
ID 令牌的核心声明 (Standard Claims)
一个典型的 ID 令牌 JWT 载荷可能如下所示:
{
"iss": "https://auth.example.com", // Issuer: 签发者,即授权服务器的URL
"sub": "248289761001", // Subject: 主题,用户的唯一标识符,这是最重要的字段
"aud": "s6BhdRkqt3", // Audience: 受众,即接收该ID令牌的客户端ID
"exp": 1311281970, // Expiration Time: 过期时间
"iat": 1311280970, // Issued At Time: 签发时间
"auth_time": 1311280969, // Authentication Time: 用户完成认证的时间
"nonce": "n-0S6_WzA2Mj", // Nonce: 客户端在请求时生成的一个随机值,用于防止重放攻击
"name": "Jane Doe", // 用户的全名
"given_name": "Jane", // 用户的名
"family_name": "Doe", // 用户的姓
"email": "jane.doe@example.com", // 用户的邮箱
"picture": "https://example.com/janedoe/me.jpg" // 用户的头像URL
}
这里的每一个字段都有其精确的含义:
iss: 必须与授权服务器的发现文档(Discovery Document)中声明的 issuer 完全一致。sub: 这是用户的唯一ID。客户端应该用这个ID来关联本地的用户账户,而不是用邮箱或用户名,因为后者可能会改变。aud: 必须包含客户端自己的 `client_id`。这确保了该 ID 令牌是签发给你的应用的,而不是其他应用。exp和iat: 用于验证令牌的时效性,防止使用过期的令牌。nonce: 这是一个关键的安全机制。客户端在发起认证请求时,应在会话(session)中保存一个随机生成的 `nonce` 值,并将其包含在请求中。收到 ID 令牌后,必须校验其载荷中的 `nonce` 是否与之前保存的一致。这能有效防止重放攻击。
客户端如何验证 ID 令牌
客户端收到 ID 令牌后,绝不能盲目相信其中的内容。必须执行严格的验证步骤,这通常由 OIDC 客户端库来完成,但作为开发者,理解其原理至关重要:
- 解码JWT: 将 Base64Url 编码的 JWT 字符串解码成头部(Header)、载荷(Payload)和签名(Signature)三部分。
- 验证签名:
- 从 JWT 头部获取签名算法(如 `RS256`)。
- 从授权服务器的 JWKS (JSON Web Key Set) 端点获取用于签名的公钥。JWKS 端点的地址通常在授权服务器的发现文档(`.well-known/openid-configuration`)中可以找到。
- 使用获取的公钥和签名算法,验证 JWT 的签名是否有效。这是最关键的一步,确保了 ID 令牌没有被篡改过。
- 验证载荷中的声明:
- Issuer (`iss`): 校验 `iss` 声明是否与预期的授权服务器URL一致。
- Audience (`aud`): 校验 `aud` 声明是否包含自己的客户端ID。
- Expiration (`exp`): 校验当前时间是否在 `exp` 声明的时间之前。
- Nonce (`nonce`): 校验 `nonce` 声明是否与发起请求时保存在会话中的值一致。
只有当所有这些验证步骤都通过后,客户端才能安全地使用 ID 令牌中的信息,例如,在界面上显示“欢迎,Jane Doe!”,或者根据 `sub` 声明在本地数据库中创建或关联用户账户。
下面是一个使用 `jwt-decode` 和 `jose` 库在 Node.js 中进行简化的验证流程示例:
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
// 1. 获取授权服务器的 JWKS (通常在应用启动时获取并缓存)
const client = jwksClient({
jwksUri: 'https://dev-example.okta.com/oauth2/default/v1/keys'
});
function getKey(header, callback) {
client.getSigningKey(header.kid, function(err, key) {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
async function validateIdToken(idToken, clientId, issuer) {
try {
// 2. 验证签名和标准声明 (exp, aud, iss)
const decodedToken = await new Promise((resolve, reject) => {
jwt.verify(idToken, getKey, {
audience: clientId,
issuer: issuer,
algorithms: ['RS256']
}, (err, decoded) => {
if (err) {
return reject(err);
}
resolve(decoded);
});
});
// 3. (伪代码) 验证 nonce
// const savedNonce = getNonceFromUserSession();
// if (decodedToken.nonce !== savedNonce) {
// throw new Error('Nonce validation failed!');
// }
console.log('ID Token is valid!');
console.log('User Sub (ID):', decodedToken.sub);
console.log('User Email:', decodedToken.email);
return decodedToken;
} catch (error) {
console.error('ID Token validation failed:', error.message);
return null;
}
}
// --- 使用 ---
const myIdToken = 'eyJhbGciOiJSUzI1...'; // 从OIDC流程中获取的ID令牌
const MY_CLIENT_ID = 's6BhdRkqt3';
const MY_ISSUER = 'https://dev-example.okta.com/oauth2/default';
validateIdToken(myIdToken, MY_CLIENT_ID, MY_ISSUER);
总结一下,ID 令牌是关于“你是谁”的证明。它的世界里只有授权服务器、客户端和通过认证的用户。它回答了客户端的问题:“刚刚登录的这个用户是谁?”
核心对比:ID 令牌 vs. 访问令牌
现在我们已经深入了解了两者各自的细节,是时候将它们并排放在一起,进行一次清晰的对比了。这将是我们理解它们本质区别的关键。
| 特性 | ID 令牌 (ID Token) | 访问令牌 (Access Token) |
|---|---|---|
| 核心目的 | 认证 (Authentication) - 确认用户是谁。 | 授权 (Authorization) - 允许应用访问特定资源。 |
| 所属协议 | OpenID Connect (OIDC) | OAuth 2.0 |
| 格式 | 必须是 JWT (JSON Web Token)。 | 无强制规定,可以是不透明字符串 (Opaque) 或 JWT。 |
| 主要消费者/验证者 | 客户端应用程序 (Client Application)。 | 资源服务器 (Resource Server / API)。 |
| 内容 | 包含用户的身份信息(声明 Claims),如 `sub` (用户ID), `name`, `email`,以及认证事件的信息,如 `auth_time`, `nonce`。 | 包含授权相关信息,如 `scope` (权限范围), `client_id`。如果是不透明的,内容对客户端不可见。 |
| 传输方式 | 通常在认证流程结束后,通过授权服务器的令牌端点(Token Endpoint)或前端重定向返回给客户端。 | 客户端在每次请求受保护的API时,通过 `Authorization: Bearer |
| 是否应发送给API? | 绝对不应该。API不应该(也无法)信任ID令牌来进行授权。 | 是的。 这是它存在的唯一理由。 |
| 生命周期 | 通常与访问令牌的生命周期相似或更长,用于在客户端维持用户的登录会话状态。 | 通常较短(分钟到小时级别),以降低泄露风险。可通过刷新令牌续期。 |
| 一个简单的类比 | 身份证/护照。它证明了你是谁,包含了你的个人信息,由权威机构(授权服务器)签发。你用它来向应用(客户端)证明自己的身份。 | 演唱会门票/房间钥匙。它不关心你是谁,只关心你是否有权进入某个特定场地(资源服务器)的特定区域(scope)。 |
实战场景分析:它们如何协同工作
理论知识固然重要,但真正的理解来自于实践。让我们通过几个常见的场景,看看 ID 令牌和访问令牌是如何在一个完整的流程中协同工作的。
场景一:使用“Google 登录”的社交登录流程
这是最经典的 OIDC 流程。假设你正在开发一个名为 "MyWebApp" 的Web应用。
1. 发起请求 (Client -> User-Agent -> Authorization Server): * 用户点击 "Login with Google" 按钮。 * MyWebApp 将用户重定向到 Google 的授权服务器。请求URL中会包含关键参数: * `response_type=code`:表示我们使用授权码流程。 * `client_id=mywebapp-client-id`:MyWebApp 的客户端ID。 * `redirect_uri=https://mywebapp.com/callback`:认证成功后返回的地址。 * `scope=openid profile email api:read`:这是关键!`openid` 是一个特殊的 scope,它告诉 Google 我们要执行 OIDC 流程,请返回一个 ID 令牌。`profile` 和 `email` 请求获取用户的基本资料和邮箱信息(这些信息会包含在ID令牌中)。`api:read` 是一个自定义的 scope,用于请求访问 MyWebApp 自己的后端API的权限。 * `nonce=random_string_123`:一个用于防止重放攻击的随机字符串。 * `state=random_state_abc`:一个用于防止CSRF攻击的随机字符串。 2. 用户认证和授权 (User -> Authorization Server): * 用户在 Google 的页面上输入用户名和密码。 * Google 向用户展示授权页面:“MyWebApp 正在请求访问您的姓名、邮箱和基本资料。是否允许?” * 用户点击“允许”。 3. 返回授权码 (Authorization Server -> User-Agent -> Client): * Google 将用户重定向回 `https://mywebapp.com/callback`,URL中附带了 `code` 和 `state`。 * MyWebApp 的后端服务器收到请求,首先验证 `state` 值是否与发起请求前存储在 session 中的一致。 4. 交换令牌 (Client's Backend -> Authorization Server): * MyWebApp 的后端服务器使用上一步获取的 `code`,连同自己的 `client_id` 和 `client_secret`,向 Google 的令牌端点发起一个后台请求。 * 这是最核心的一步。因为我们在 `scope` 中请求了 `openid`,Google 的令牌端点会返回一个 JSON 对象,其中包含: * `access_token`: "ABC..." (一个访问令牌) * `id_token`: "eyJ..." (一个ID 令牌) * `refresh_token`: "XYZ..." (一个刷新令牌) * `expires_in`: 3600 5. 令牌的处理和使用 (Client): * 处理 ID 令牌: MyWebApp 的后端对 `id_token` 进行完整的验证(签名、iss, aud, exp, nonce等)。验证通过后,它解析出 `sub`, `name`, `email` 等信息。现在,MyWebApp 确信了用户的身份。它可以根据 `sub` 在数据库中查找或创建用户记录,并将用户信息存入会话(session),标志着用户已登录。前端现在可以显示“欢迎, [name]!”。 * 处理访问令牌: MyWebApp 将 `access_token` 安全地存储起来(例如,与用户的会话关联)。当 MyWebApp 的前端需要调用它自己的后端 API(例如 `https://api.mywebapp.com/data`)时,后端服务就可以使用这个 `access_token`。但在这个场景中,这个访问令牌是 Google 签发的,通常用于访问 Google 的 API(比如 Google Calendar API)。如果 MyWebApp 要访问自己的 API,它通常会自己签发一个访问令牌(见下一个场景)。在这个流程中,ID 令牌完成了认证任务,访问令牌则准备好了执行授权任务。
场景二:SPA (单页面应用) 与自有后端 API 通信
假设你有一个 React/Vue/Angular 开发的 SPA,它需要与你用 Node.js/Java/Go 开发的后端 API 进行安全通信。
1. 登录流程: * SPA 采用与场景一类似的 OIDC 授权码流程(通常是带有 PKCE 的授权码流程,因为 SPA 是公共客户端,不能安全存储 `client_secret`)。 * 用户通过授权服务器(例如 Auth0, Okta, 或者自建的 Keycloak)登录。 * 登录成功后,授权服务器通过重定向将 `code` 返回给 SPA。 * SPA 使用 `code` 和 `code_verifier` (PKCE的一部分) 向授权服务器的令牌端点请求令牌。 * 授权服务器返回 `id_token` 和 `access_token`。 2. SPA 的职责: * SPA 接收到令牌。它首先验证 `id_token`,确认用户身份。 * 验证成功后,SPA 可以解析 `id_token` 获取用户信息(如用户名、头像)用于界面展示,并以此建立前端的登录状态。 * SPA 将 `access_token` 安全地存储在内存中(这是目前推荐的最安全的方式)。绝对不要存储在 `localStorage` 中,因为它容易受到 XSS 攻击。 3. 调用 API: * 当 SPA 需要从后端获取数据时(例如,`fetch('/api/orders')`),它会在请求的 `Authorization` 头中附加上存储的 `access_token`。 * `fetch('/api/orders', { headers: { 'Authorization': 'Bearer ' + accessToken } })` 4. 后端 API 的职责: * 后端 API(资源服务器)收到请求。 * 它从 `Authorization` 头中提取出 `access_token`。 * API 服务器必须验证这个 `access_token`。如果令牌是 JWT 格式,API 会: * 验证签名(使用授权服务器的公钥)。 * 验证 `iss`, `aud` (audience 必须是该 API 的标识符)。 * 验证 `exp` 是否过期。 * 检查 `scope` 声明,确认该令牌是否有权访问 `/api/orders` 这个端点。 * 如果令牌是不透明格式,API 则需要调用授权服务器的内省端点(Introspection Endpoint)来验证令牌并获取其包含的授权信息。 * 只有在令牌验证通过且权限足够的情况下,API 才会执行业务逻辑并返回数据。在这个场景中,职责划分非常清晰:
- `id_token` 是给 SPA 用的,用来管理前端用户会话。
- `access_token` 是给 后端 API 用的,用来保护 API 资源。
安全考量与最佳实践
仅仅理解概念是不够的,作为开发者,我们需要知道如何在实践中安全地使用它们。
- 不要混淆用途: 这是最重要的原则。ID 令牌用于认证,访问令牌用于授权。永远不要用 ID 令牌去访问 API。
- 令牌存储:
- Web 后端 (传统服务器端渲染应用): 将令牌存储在服务端的加密会话(Session)中是最安全的方式。
- SPA (单页面应用): 最佳实践是将令牌存储在 JavaScript 变量中(内存中)。避免使用 `localStorage` 或 `sessionStorage`,因为它们无法防御 XSS 攻击。如果页面刷新后需要保持登录状态,可以使用一个长效的、`HttpOnly`、`Secure`、`SameSite=Strict` 的 cookie 来存储刷新令牌(Refresh Token),然后用它来静默获取新的访问令牌。
- 移动应用 (Mobile App): 应使用操作系统提供的安全存储机制,如 iOS 的 Keychain 或 Android 的 Keystore。
- 严格验证: 客户端必须严格验证 ID 令牌的每一项声明。资源服务器(API)必须严格验证访问令牌的签名、受众、签发者和过期时间。不要遗漏任何一步。
- 使用 `state` 和 `nonce`: 在 OIDC 流程中,`state` 参数用于防止 CSRF 攻击,`nonce` 参数用于防止重放攻击。必须使用并验证它们。
- 为 API 设置正确的 `audience`: 当你使用 JWT 格式的访问令牌时,要确保为每个资源服务器(或一组逻辑上相关的API)定义一个唯一的 `audience` (受众) 值。API 在验证令牌时,必须检查 `aud` 声明是否与自己的标识符匹配。这可以防止一个为 API A 签发的令牌被误用于访问 API B。
- 权限最小化原则: 在请求授权时,`scope` 应遵循权限最小化原则。只请求当前操作所必需的权限,不要一次性请求所有可能的权限。
结论:各司其职,相得益彰
经过这次深入的剖析,我们应该能清晰地看到,ID 令牌和访问令牌虽然诞生于同一个认证授权流程,但它们的设计目标、服务对象和生命周期使命却截然不同。
访问令牌是 OAuth 2.0 的核心,它是一把授权的钥匙,沉默而有力。它的世界里只有权限和资源,它告诉资源服务器:“这个请求可以做什么”。它面向机器,是服务间安全通信的基石。
ID 令牌是 OpenID Connect 的灵魂,它是一份用户的数字身份证明,详尽而可信。它的世界里只有用户身份和认证的上下文,它告诉客户端:“这个用户是谁”。它面向客户端应用,是构建可信用户会话的起点。
作为全栈开发者,彻底理解这两者的区别,不仅仅是掌握一个技术知识点,更是构建安全、可靠、可扩展系统的基本功。当你下一次设计登录系统、规划 API 安全策略或集成第三方服务时,请记住:
用 ID 令牌来识人(认证),用访问令牌来办事(授权)。
让它们在你的应用架构中各司其职,协同工作,你就能构建出一个既拥有流畅用户体验又具备坚实安全保障的现代化应用。
Post a Comment