Skip to content

认证授权: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 由三个部分组成,中间用 . 分隔:

  1. Header (头部):声明类型和加密算法。

    json
    { "alg": "HS256", "typ": "JWT" }

    Base64 编码后构成第一部分。

  2. Payload (载荷):存放有效信息(Claims)。包含三类声明:

    • 注册声明 (Registered claims):预定义字段,如 iss (签发者), exp (过期时间), sub (用户ID)。
    • 公共声明 (Public claims):可以随意添加,一般添加用户信息。
    • 私有声明 (Private claims):服务端和客户端共同定义的业务字段。

    警告:Payload 是 Base64 编码的明文,严禁存放密码、银行卡号等敏感数据!

  3. Signature (签名):这是整个 Token 安全的基石。

    javascript
    var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);
    var signature = HMACSHA256(encodedString, 'server_secret_key');

    Secret 必须保存在服务端,一旦泄露,客户端即可自我签发 Token。

2.2 核心优缺点分析 ​

优点:

  • 跨语言支持:JSON 通用性极强,JAVA, .NET, Node.js 等均可无缝对接。
  • 自包含:Token 本身包含了业务所需的非敏感信息,减少了数据库查询。
  • 性能高:由于不需要服务端存储,极大减轻了服务器压力。

缺点:

  • 不可撤销性:一旦签发,在有效期内无法强行失效(除非引入黑名单机制)。
  • 有效期权衡:设太长不安全,设太短影响用户体验。
  • 数据大小:包含 Claims 越多,Token 越长,增加网络开销。
方案优点缺点安全风险
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 流程:

  1. Client Credentials (客户端凭据):
    • 流程:App -> IDS4 (获取 Token) -> API。
    • 场景:后台服务间的通讯,无用户交互。
  2. Authorization Code + PKCE (授权码模式):
    • 流程:用户在浏览器登录 -> 获取 Code -> 客户端用 Code 换 Token。
    • 场景:Web 应用、移动端 App。这是目前最推荐的安全模式。
  3. 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. 生产环境安全红线与排障 ​

  1. 证书管理:生产环境严禁使用 AddDeveloperSigningCredential()。必须使用 .pfx 证书文件,否则服务重启会导致所有已签发的 Token 无效。
  2. 强制 HTTPS:Token 在 HTTP 下完全是“裸奔”的,中间人攻击可以轻易窃取 Token 并冒充用户。
  3. NuGet 源排障:
    • 问题:NETSDK1004: 找不到资产文件 project.assets.json。
    • 解决:vs->nuget包设置->本地源取消。确保能够从官方源还原 IDS4 的核心包。
  4. CORS 跨域:在服务端必须正确配置 Access-Control-Allow-Origin。IDS4 对不同类型的客户端有严格的跨域策略检查。

4. 单点登录 (SSO) 与 刷新机制 ​

4.1 SSO 认证闭环 ​

  1. 未登录访问:用户访问应用 A,发现无 Token,重定向至 IDS4 统一登录页。
  2. 认证过程:用户在 IDS4 输入凭据,IDS4 在自己的域名下写入登录 Cookie。
  3. 分发 Token:IDS4 重定向回应用 A,并签发应用 A 的 Token。
  4. 共享状态:当用户访问应用 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) ​

  1. Token Revocation (令牌撤销):
    • 当用户点击“注销”或修改密码时,IDS4 允许通过 /connect/revocation 端点使 Refresh Token 立即失效。
    • 这对于丢失设备或账号被盗的情况至关重要。
  2. Token Introspection (令牌内省):
    • 对于某些高安全级别的 API,它不信任自校验,而是每次都拿着 Token 去 IDS4 询问:“这个 Token 还是有效的吗?”。
    • IDS4 提供了 /connect/introspect 端点支持此功能。

4.4 移动端 (iOS/Android) 集成要点 ​

  1. 安全存储:移动端严禁将 Token 存入普通文件。应使用 iOS 的 Keychain 或 Android 的 EncryptedSharedPreferences。
  2. PKCE 协议:在移动端使用授权码模式时,必须启用 PKCE (Proof Key for Code Exchange),以防止授权码被恶意拦截。
  3. 系统浏览器登录:为了安全,登录过程应在系统自带浏览器或 ASWebAuthenticationSession 中完成,而不是应用内的 WebView。

5. 身份验证流程详解 (Sequential Steps) ​

为了更好地理解认证过程,以下是典型的 Authorization Code + PKCE 流程:

  1. Client -> IDS4:发起登录请求,生成 Code Challenge。
  2. IDS4 -> User:展示登录页面。
  3. User -> IDS4:提交用户名密码。
  4. IDS4 -> Client:重定向回客户端,并携带 Authorization Code。
  5. Client -> IDS4:在后台发送 Code + Code Verifier 请求 Token。
  6. IDS4 -> Client:验证通过,签发 ID Token、Access Token 和 Refresh Token。

6. 开发者避坑指南与深度排障 ​

5.1 常见错误场景 ​

  1. Invalid Client / Invalid Scope:
    • 原因:请求中的 ClientId 或 Scope 在数据库中未定义,或者 Client 的 AllowedScopes 列表中没有包含请求的 Scope。
    • 解决:核对 IDS4 配置代码与客户端请求参数是否完全一致。
  2. The signature is invalid:
    • 原因:API 端使用的 Secret 与 IDS4 签发时不匹配;或者 IDS4 重启后证书丢失导致签名密钥变更。
    • 解决:确保 API 端的 Authority 配置正确,且 IDS4 已持久化签名证书。
  3. 401 Unauthorized:
    • 原因:Token 已过期;或者 API 端的中间件配置了 RequireHttpsMetadata = true 而你使用的是 HTTP。
    • 解决:检查 Token 的 exp 字段;调试环境可设置 RequireHttpsMetadata = false。

5.2 进阶避坑建议 ​

  1. 越权检查 (IDOR):不要仅通过 Token 是否有效来判断权限。必须在服务端检查当前用户是否具有操作该资源的权限。
  2. 敏感信息过滤:Payload 中的数据是明文的!绝对不要把密码、身份证号等信息塞进去。
  3. 时钟偏差:如果服务器时间不准,可能导致 Token 刚签发就过期。建议在校验时留出 5 分钟的 ClockSkew。
  4. HttpOnly 保护:在 Web 应用中,将 Token 存入设置了 HttpOnly 的 Cookie 是防御 XSS 攻击的有效手段。
  5. 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 过期后再移除旧证书。


8. 参考资料 ​

方舟 · Ark