Appearance
认证授权:IdentityServer4 与 JWT 基础实践
在现代分布式系统和微服务架构中,传统的基于 Session 的认证机制已难以满足需求。基于 Token(特别是 JWT)的认证与授权方案已成为行业标准。IdentityServer4 作为 .NET 生态中的佼佼者,提供了工业级的安全能力。
1. 认证机制的演进:从 Session 到 Token
1.1 基于 Session 的认证机制
- 原理:HTTP 是无状态的。服务器验证用户后,在内存中创建 Session,并将 Session ID 通过 Cookie 返回给浏览器。
- 局限性:
- 扩展性差:在集群环境下,需要复杂的 Session 共享(如 Redis 存储),否则用户请求落到不同机器会导致掉线。
- 资源开销:每个认证用户都要在服务端记录 Session,随着在线用户增加,服务端内存压力巨大。
- CSRF 风险:因为是基于 Cookie 的,容易受到跨站请求伪造攻击。
1.2 基于 Token (JWT) 的认证机制
- 原理:服务端认证成功后,将用户信息封装成一个加密字符串(Token)返回。客户端后续请求在 Header 中携带 Token,服务端通过算法自校验,无需查库。
- 核心对比表:
| 特性 | Session 认证 | JWT (Token) 认证 |
|---|---|---|
| 存储位置 | 服务端内存或数据库 | 客户端 (Storage/Cookie) |
| 可扩展性 | 难 (需解决 Session 同步) | 易 (服务端无状态,天然集群) |
| 跨域支持 | 较差 (受限于 Cookie 策略) | 极好 (Header 传输,无视域名) |
| 单点登录 | 复杂 | 简单 (开销小) |
| 安全性 | 容易受到 CSRF 攻击 | 需防范 Token 泄露 (XSS) |
2. JWT (JSON Web Token) 深度解析
JWT 是一种基于 JSON 的开放标准 (RFC 7519),设计目标是紧凑且安全。
2.1 结构组成
JWT 由三个部分组成,中间用 . 分隔:
Header (头部):声明类型和加密算法。
json{ "alg": "HS256", "typ": "JWT" }Base64 编码后构成第一部分。
Payload (载荷):存放有效信息(Claims)。包含三类声明:
- 注册声明 (Registered claims):预定义字段,如
iss(签发者),exp(过期时间),sub(用户ID)。 - 公共声明 (Public claims):可以随意添加,一般添加用户信息。
- 私有声明 (Private claims):服务端和客户端共同定义的业务字段。
警告:Payload 是 Base64 编码的明文,严禁存放密码、银行卡号等敏感数据!
- 注册声明 (Registered claims):预定义字段,如
Signature (签名):这是整个 Token 安全的基石。
javascriptvar encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload); var signature = HMACSHA256(encodedString, 'server_secret_key');Secret 必须保存在服务端,一旦泄露,客户端即可自我签发 Token。
2.2 核心优缺点分析
优点:
- 跨语言支持:JSON 通用性极强,JAVA, .NET, Node.js 等均可无缝对接。
- 自包含:Token 本身包含了业务所需的非敏感信息,减少了数据库查询。
- 性能高:由于不需要服务端存储,极大减轻了服务器压力。
缺点:
- 不可撤销性:一旦签发,在有效期内无法强行失效(除非引入黑名单机制)。
- 有效期权衡:设太长不安全,设太短影响用户体验。
- 数据大小:包含 Claims 越多,Token 越长,增加网络开销。
2.3 安全存储之争:LocalStorage vs Cookie
| 方案 | 优点 | 缺点 | 安全风险 |
|---|---|---|---|
| LocalStorage | 存储空间大,操作方便 | 易被脚本读取 | XSS 攻击:脚本可以直接拿到 Token 并发送给攻击者服务器。 |
| HttpOnly Cookie | 脚本无法读取,安全性高 | 空间有限 (4KB),需处理跨域问题 | CSRF 攻击:虽然 Token 能防御 CSRF,但仍需配合 SameSite 属性。 |
建议:对于高安全性应用,优先选择 HttpOnly + Secure + SameSite=Lax 的 Cookie 存储 Token。
3. IdentityServer4 (IDS4) 核心实践
IdentityServer4 是为 ASP.NET Core 设计特有的 OpenID Connect 和 OAuth 2.0 框架。
3.1 核心资源与 Token 类型
使用 IDS4 时,需要区分两种核心 Token:
- ID Token:包含用户的身份信息(如姓名、头像)。主要供客户端(前端)使用,用于展示用户信息。遵循 OpenID Connect 标准。
- Access Token:用于访问受保护的 API 资源。主要供服务端使用。
资源定义区分:
- Identity Scopes:对应 ID Token 中的 Claims。
- API Scopes:对应 Access Token 中允许访问的范围。
3.2 方案选型:IDS4 vs 托管服务 (Auth0/Okta)
| 维度 | IdentityServer4 (自托管) | 托管服务 (Auth0/Okta/Azure AD) |
|---|---|---|
| 成本 | 免费开源 (软件成本为0) | 按活跃用户计费 (后期昂贵) |
| 掌控力 | 100% 掌控代码和数据 | 受限于厂商功能和合规性 |
| 维护成本 | 高 (需运维服务器、数据库、证书) | 极低 (SaaS 服务) |
| 合规性 | 数据可完全留在内网 | 数据存储在第三方云端 |
4. 身份验证流程详解 (Sequential Steps)
在 IDS4 中,我们通过代码定义资源和客户端。以下是一个标准的内存配置示例:
csharp
public static class Config {
// 1. 定义身份资源(用户基本信息)
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[] {
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
// 2. 定义 API 资源(受保护的接口)
public static IEnumerable<ApiResource> ApiResources =>
new ApiResource[] {
new ApiResource("api1", "我的第一个 API")
};
// 3. 定义客户端(允许访问的应用)
public static IEnumerable<Client> Clients =>
new Client[] {
new Client {
ClientId = "client",
// 使用客户端凭据模式
AllowedGrantTypes = GrantTypes.ClientCredentials,
// 加密密钥
ClientSecrets = { new Secret("secret".Sha256()) },
// 允许访问的范围
AllowedScopes = { "api1" }
}
};
}3.3 授权模式深度解析 (Flows)
根据应用场景不同,IDS4 支持多种 OAuth 2.0 流程:
- Client Credentials (客户端凭据):
- 流程:App -> IDS4 (获取 Token) -> API。
- 场景:后台服务间的通讯,无用户交互。
- Authorization Code + PKCE (授权码模式):
- 流程:用户在浏览器登录 -> 获取 Code -> 客户端用 Code 换 Token。
- 场景:Web 应用、移动端 App。这是目前最推荐的安全模式。
- Device Flow (设备模式):
- 场景:电视、物联网设备等输入受限的终端。
3.4 数据库持久化方案 (Entity Framework)
生产环境必须持久化数据。IDS4 提供了 IdentityServer4.EntityFramework 扩展包:
csharp
public void ConfigureServices(IServiceCollection services) {
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityServer()
.AddConfigurationStore(options => {
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options => {
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// 启用自动清理过期 Token
options.EnableTokenCleanup = true;
});
}4. 生产环境安全红线与排障
- 证书管理:生产环境严禁使用
AddDeveloperSigningCredential()。必须使用.pfx证书文件,否则服务重启会导致所有已签发的 Token 无效。 - 强制 HTTPS:Token 在 HTTP 下完全是“裸奔”的,中间人攻击可以轻易窃取 Token 并冒充用户。
- NuGet 源排障:
- 问题:
NETSDK1004: 找不到资产文件 project.assets.json。 - 解决:vs->nuget包设置->本地源取消。确保能够从官方源还原 IDS4 的核心包。
- 问题:
- CORS 跨域:在服务端必须正确配置
Access-Control-Allow-Origin。IDS4 对不同类型的客户端有严格的跨域策略检查。
4. 单点登录 (SSO) 与 刷新机制
4.1 SSO 认证闭环
- 未登录访问:用户访问应用 A,发现无 Token,重定向至 IDS4 统一登录页。
- 认证过程:用户在 IDS4 输入凭据,IDS4 在自己的域名下写入登录 Cookie。
- 分发 Token:IDS4 重定向回应用 A,并签发应用 A 的 Token。
- 共享状态:当用户访问应用 B 时,应用 B 再次跳转 IDS4,IDS4 发现用户已存在登录 Cookie,直接重定向回应用 B 并签发新 Token,实现“一处登录,处处通行”。
4.2 Refresh Token (刷新令牌)
为了安全性,JWT 的 exp 通常设得很短(如 1 小时)。为了避免用户频繁登录,需要引入 Refresh Token。
- 机制:在签发 JWT 的同时,签发一个长效的 Refresh Token(存入数据库)。
- 流程:当 JWT 过期,客户端使用 Refresh Token 向 IDS4 请求新 JWT。如果 Refresh Token 也过期或被注销,用户才需要重新登录。
4.3 令牌撤销与内省 (Revocation & Introspection)
- Token Revocation (令牌撤销):
- 当用户点击“注销”或修改密码时,IDS4 允许通过
/connect/revocation端点使 Refresh Token 立即失效。 - 这对于丢失设备或账号被盗的情况至关重要。
- 当用户点击“注销”或修改密码时,IDS4 允许通过
- Token Introspection (令牌内省):
- 对于某些高安全级别的 API,它不信任自校验,而是每次都拿着 Token 去 IDS4 询问:“这个 Token 还是有效的吗?”。
- IDS4 提供了
/connect/introspect端点支持此功能。
4.4 移动端 (iOS/Android) 集成要点
- 安全存储:移动端严禁将 Token 存入普通文件。应使用 iOS 的 Keychain 或 Android 的 EncryptedSharedPreferences。
- PKCE 协议:在移动端使用授权码模式时,必须启用 PKCE (Proof Key for Code Exchange),以防止授权码被恶意拦截。
- 系统浏览器登录:为了安全,登录过程应在系统自带浏览器或
ASWebAuthenticationSession中完成,而不是应用内的WebView。
5. 身份验证流程详解 (Sequential Steps)
为了更好地理解认证过程,以下是典型的 Authorization Code + PKCE 流程:
- Client -> IDS4:发起登录请求,生成
Code Challenge。 - IDS4 -> User:展示登录页面。
- User -> IDS4:提交用户名密码。
- IDS4 -> Client:重定向回客户端,并携带
Authorization Code。 - Client -> IDS4:在后台发送
Code+Code Verifier请求 Token。 - IDS4 -> Client:验证通过,签发
ID Token、Access Token和Refresh Token。
6. 开发者避坑指南与深度排障
5.1 常见错误场景
- Invalid Client / Invalid Scope:
- 原因:请求中的
ClientId或Scope在数据库中未定义,或者Client的AllowedScopes列表中没有包含请求的 Scope。 - 解决:核对 IDS4 配置代码与客户端请求参数是否完全一致。
- 原因:请求中的
- The signature is invalid:
- 原因:API 端使用的
Secret与 IDS4 签发时不匹配;或者 IDS4 重启后证书丢失导致签名密钥变更。 - 解决:确保 API 端的
Authority配置正确,且 IDS4 已持久化签名证书。
- 原因:API 端使用的
- 401 Unauthorized:
- 原因:Token 已过期;或者 API 端的中间件配置了
RequireHttpsMetadata = true而你使用的是 HTTP。 - 解决:检查 Token 的
exp字段;调试环境可设置RequireHttpsMetadata = false。
- 原因:Token 已过期;或者 API 端的中间件配置了
5.2 进阶避坑建议
- 越权检查 (IDOR):不要仅通过 Token 是否有效来判断权限。必须在服务端检查当前用户是否具有操作该资源的权限。
- 敏感信息过滤:Payload 中的数据是明文的!绝对不要把密码、身份证号等信息塞进去。
- 时钟偏差:如果服务器时间不准,可能导致 Token 刚签发就过期。建议在校验时留出 5 分钟的
ClockSkew。 - HttpOnly 保护:在 Web 应用中,将 Token 存入设置了
HttpOnly的 Cookie 是防御 XSS 攻击的有效手段。 - CORS 跨域深度配置:IDS4 内部维护了一个
ClientCorsOrigins表。如果前端 SPA 跨域请求失败,除了配置 ASP.NET Core 的 CORS 中间件,还需要在 IDS4 的 Client 配置中添加AllowedCorsOrigins。
6. 生产环境上线检查清单 (Checklist)
- [ ] 签名证书:已替换为有效的
.pfx或Store证书,非开发证书。 - [ ] HTTPS:IDS4 节点及所有 API 节点已强制开启 HTTPS。
- [ ] Token 有效期:
AccessTokenLifetime已根据业务安全性调整(建议 < 2小时)。 - [ ] 持久化:配置和授权数据已从内存迁移至 SQL Server/MySQL。
- [ ] 日志监控:已配置 Serilog 等工具记录 IDS4 的认证失败和异常日志。
- [ ] CORS 策略:已限制允许的 Origin 域名列表,非
*。
7. 认证授权常见问题 (FAQ)
Q: JWT 是否可以手动注销? A: JWT 本身是无状态的,一旦签发无法收回。常见的做法是引入“黑名单”机制,将注销的 Token ID (jti) 存入 Redis 并设置过期时间,在 API 端验证时查一下黑名单。
Q: 为什么我的 Token 解析出来是乱码? A: 请确认是否误用了加密 (JWE) 而非签名 (JWS)。通常 JWT 仅是 Base64 编码,如果你在 IDS4 中配置了加密令牌,则需要对应的解密密钥。
Q: 生产环境如何处理证书更新? A: IDS4 支持 AddSigningCredential 挂载多个证书。在更新时,先挂载新证书并保持旧证书作为验证项,待所有旧 Token 过期后再移除旧证书。
