IdentityServer4认证授权之JWT方案

前言

回头看,轻舟已过万重山,

向前看,前路漫漫亦灿灿;

抬头看,满天星光甚浪漫,

低头看,满路荆棘已过半。

认证和授权

接着查看扩展方法AddApplicationServices的实现,他的第一个方法是builder.AddDefaultAuthentication(),我们跟踪AddDefaultAuthentication的具体实现

image-20251012224514335

1.读取配置

AddDefaultAuthentication这部分代码主要是:

  • 提取IServiceCollectionIConfigurationManager

  • 读取appsettings.jsonIdentity的配置(如果appsettings.json没有配置 Identity 节,则认为无需认证,但是购物车服务通常是需要认证的)

image-20251012224803236

appsettings.jsonIdentity的配置参数(这2个参数我们下面详细看一下):

  "Identity": {
    //"Url": "http://identity",
    "Audience": "basket"
  },

image-20251012225146632

2.删除JWT Claim自动映射(sub)

sub 是 JWT(JSON Web Token)中一个非常核心的标准字段,它的含义是: sub(Subject)表示令牌的主体,也就是这个 Token 所代表的用户或实体的唯一标识。

这行代码的意思是从 JWT 处理器的默认入站声明映射表中移除了对 "sub"(subject)这个 JWT 声明名的自动映射规则,使得 JWT 中的 sub 在转换为 .NET Claim 时仍保留名称 "sub",而不会被自动重命名为 ClaimTypes.NameIdentifier(或其他 .NET 标准 claim 名)

image-20251012225657118

假设 JWT payload 有:

{
  "sub": "12345",
  "name": "alice"
}

如果不移除映射

var id = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

移除映射

var id = User.FindFirst("sub")?.Value;

为什么存在映射(Inbound Claim Mapping(入站声明映射))?

在 .NET 里,历史上框架为了兼容不同 token 格式,会把一些常见的 JWT claim 名(如 sub, aud, iss, jti 等)自动映射为 .NET 内置的 ClaimTypes.* 字符串(例如 ClaimTypes.NameIdentifier)。

这种映射有时很方便(你可以直接用 ClaimTypes.NameIdentifier 读取用户标识),但也会带来隐式行为和混淆。

因此许多项目选择取消部分映射以保留 JWT 原生声明名(例如 sub),便于遵循 OpenID Connect 标准或与其他系统一致。

为什么要移除映射(Inbound Claim Mapping(入站声明映射))?

保持与 OpenID Connect / JWT 标准 的一致性(直接使用 subaud 等原名)。

避免隐式重命名导致的歧义或冲突(例如同时存在 subnameidentifier)。

更适合微服务/多语言互操作场景:不同语言/库通常直接使用原始 claim 名。

3.注册认证服务

services.AddAuthentication() 是 ASP.NET Core 注册认证服务的入口方法。它本身不指定具体认证方案,只做基础设施初始化。

注册认证框架的核心服务

  • 管理认证方案(Scheme):IAuthenticationSchemeProvider
  • 管理认证处理器(Handler):IAuthenticationHandlerProvider

image-20251012232847743

注册辅助服务

  • 数据保护(Cookie 加密、Token 加密)
  • Web 编码器(Base64 / URL 编码)
  • 系统时间提供者(验证 Token 过期)

image-20251012232703724

返回 AuthenticationBuilder

  • 用于链式注册具体的认证方案(JWT、Cookie、OAuth2 等)

AuthenticationBuilder 提供的常用方法:

方法 作用
AddJwtBearer(Action<JwtBearerOptions>) 注册 JWT Bearer 方案
AddCookie(Action<CookieAuthenticationOptions>) 注册 Cookie 方案
AddOAuth(Action<OAuthOptions>) 注册 OAuth2 授权方案
AddOpenIdConnect(Action<OpenIdConnectOptions>) 注册 OpenID Connect
AddScheme<TOptions, THandler>(string scheme, Action<TOptions>) 注册自定义认证方案

这里没有指定默认方案 (DefaultAuthenticateScheme 等),框架会使用第一个注册的方案作为默认。就是下面的JWT Bearer 认证方案(AddJwtBearer

image-20251012232224255

4.注册JWT Bearer认证方案

4.1注册AddJwtBearer

在 ASP.NET Core 中注册一个 JWT Bearer 认证方案

返回值AuthenticationBuilder允许继续链式注册其他认证方案。

核心功能

  • 1.告诉 ASP.NET Core:接下来处理的认证类型是 JWT Bearer Token
  • 2.配置 Token 的验证逻辑,例如签名验证、Issuer 验证、Audience 验证等

常用属性

属性 作用
Authority JWT 签发者的地址(Identity Server 或其他 IdP),用于获取元数据(公钥、端点等)
Audience 期望 Token 的受众(aud Claim),通常是 API 名称
RequireHttpsMetadata 是否强制通过 HTTPS 获取 Authority 的元数据
TokenValidationParameters 用于进一步定制 Token 校验规则(Issuer、Audience、签名算法等)
Events 可自定义认证事件,例如验证失败、Token 接收、认证成功等

image-20251012233805614

4.2认证服务器地址(Authority)

1.概念

在 JWT Bearer 认证中,Authority 表示 可信身份提供者(Identity Provider, IdP)的地址,即 谁签发了 JWT Token。(简单理解:认证服务器地址)

  • 它通常是 IdentityServer、Azure AD、Okta 等认证服务器的 URL
  • ASP.NET Core 通过它来获取认证元数据,包括:
    • 签名公钥(用于验证 JWT 签名)
    • 发行者信息(Issuer)
    • 授权端点、Token 端点等

元数据内容

字段 说明
issuer 发行者地址,标识 IdentityServer 实例
authorization_endpoint 授权端点 URL,用于获取授权码或令牌
token_endpoint 令牌端点 URL,用于交换 access token
userinfo_endpoint 用户信息端点 URL
jwks_uri 公钥信息,用于验证 JWT 签名
end_session_endpoint 注销端点 URL
response_types_supported 支持的响应类型(code, token 等)
subject_types_supported 支持的主体类型(public, pairwise)
scopes_supported 支持的作用域(openid, profile, email 等)

image-20251013004657584

元数据接口

打开开发环境认证服务,之前的Identity.API我们已经调通了,这里可以测试一下

我们在eShop\src\Identity.API目录下执行以下命令,开启认证服务

dotnet run --urls https://localhost:5243/

image-20251013003530449

然后访问认证服务,IdentityServer4界面上默认就可以打开元数据接口

自己也可以根据认证服务的地址+/.well-known/openid-configuration打开

https://localhost:5243/.well-known/openid-configuration

image-20251013003819461

3.配置元数据接口地址

eshop中其实没有配置Identity:Url,我们既然学到这里了就先给配上,如果不行可以去掉或者再想办法

我们把IdentityServer4元数据接口地址配置到Identity:Url

image-20251013211801023

为什么eshop没有配置Identity:Url

在本地开发环境中:

  • 微服务和 IdentityServer 都在本地运行,或者用 Docker 容器模拟。
  • 为了方便调试,示例项目允许 只校验 Audience,而跳过访问远程 Authority。
  • 避免每个开发者都必须配置本地 IdentityServer 地址,降低配置门槛。

eShop 样例项目只配置 Audience,是为了 本地开发和学习方便

如果正式项目不配置 Authority,会发生什么?

无法自动获取元数据

  • 框架不会访问 .well-known/openid-configuration
  • 无法获取签发者 (issuer)、公钥 (jwks_uri) 和 Token Endpoint

Token 验证不完整

  • JWT 只会校验 Audience(或依赖手动配置的 TokenValidationParameters)
  • 可能导致签发者不可信,Token 有被伪造的风险

安全性降低

  • 认证机制不完整,攻击者可能利用伪造 Token 访问 API
  • 在生产环境中非常不安全

4.3令牌目标(Audience)

Audience(aud)是 JWT 的“目标服务/资源”,用于指明哪些服务可以接收这个 token,并防止 token 被非目标服务滥用。

Audience 的作用

  • 避免 token 被非目标 API 或服务使用。JWT 被签发时指定 aud,验证时检查是否匹配。
  • 微服务系统中,不同服务有不同 audience。
  • 配合 JWT 验证,在ASP.NET Core 中验证 token

当前服务是购物车服务(Basket.API),因此在 JWT 验证中配置 Audiencebasket,表示该 token 仅能被购物车服务接收和使用,其他服务无法使用该 token,从而保证 token 的使用范围和安全性。

image-20251013011038773

5.HTTPS元数据验证(RequireHttpsMetadata)

RequireHttpsMetadata 控制 JWT/OIDC 中间件从 元数据地址(metadata address)或 Authority 拉取 OpenID Connect 元数据时,是否必须使用 HTTPS。默认值为 true,也就是说中间件会拒绝通过 http:// 拉取元数据。

RequireHttpsMetadata 的作用是 强制框架在获取 Identity Provider 元数据时必须使用 HTTPS 传输,以防止元数据被篡改导致验证链被攻击。

只在 开发 / 本地测试 场景下临时允许(例如本地调试、某些模拟器回环地址、内部测试环境等),并且你能接受通信在非 TLS 下的风险。绝对不要在生产环境或对外暴露的环境关闭它。社区和官方文档都建议仅在开发临时关闭。

更安全的做法是:为测试环境也启用 HTTPS(即使用自签名证书),或在极短期内通过 BackchannelHttpHandler 临时接受自签名证书 —— 不要在外网/生产上使用绕过证书验证的做法。

我们的Identity.API启动本身就是HTTPS,所以本地开发环境也可以打开这个配置。(先打开试试,不行再看)

为什么要有RequireHttpsMetadata(安全动机)?

  • OpenID Connect/JWKS 等元数据包含签名公钥、端点地址等敏感信息。如果通过 HTTP 明文传输,容易被中间人(MITM)篡改或替换公钥,导致签名验证失效或被伪造 token。

  • 因此默认强制 HTTPS 是为了防止证书/中间人攻击,确保获取的元数据来自受信任的授权服务器。

为什么 eShop 的 Identity.API 在本地启动时就能使用 HTTPS(如 https://localhost:5243 )?

  • ASP.NET Core 自带开发证书机制:从 .NET Core 2.1 开始,ASP.NET Core 默认支持本地 HTTPS。安装 SDK 后系统会自动生成并信任一个 “开发用自签名证书(localhost 证书)”。
  • Kestrel 自动加载证书Properties/launchSettings.json 里定义了"applicationUrl": "https://localhost:5243;http://localhost:5242",Kestrel 启动时会自动绑定该开发证书到 5243 端口,因此本地即可通过 HTTPS 访问。

image-20251013210503088

6.验证iss签发者(ValidIssuers)

iss(issuer)是 JWT(JSON Web Token)中的标准声明(claim)字段,用来表示 哪个实体/身份提供者签发了这个 token。示例:"iss": "https://localhost:5243"

TokenValidationParameters.ValidIssuers 是一个允许的 issuer 字符串集合。当 ValidateIssuer = true 时,框架会检查 token 的 iss 是否精确匹配集合中的某一项。匹配才算通过 issuer 校验。

ValidIssuer:单一字符串,表示只允许一个 issuer。

ValidIssuersIEnumerable<string>,允许列出多个受信任的 issuer(适合多 IdP / 多域名场景)。

为什么必须校验 iss

即便签名正确,仍需确认这个 token 是由受信任的签发方签发的(防止别的系统或被攻陷的 IdP 的 token 被接受)。iss 校验是信任链的一部分。

image-20251013211216206

7.验证令牌目标Audience(ValidateAudience)

概念

ValidateAudienceJWT 验证参数中的一个布尔属性,用来控制 是否校验 token 的 aud(Audience)声明

  • aud(Audience)是 JWT 中的标准字段,表示 这个 token 面向哪个受众/资源
  • ValidateAudience = true 时,中间件会检查 token 中的 aud 是否匹配你配置的值(ValidAudienceValidAudiences)。
  • ValidateAudience = false 时,会跳过 aud 校验。

ValidateAudience 决定你是否严格要求 token 是为你的 API 或资源发的。

示例

我们这里是购物车服务,如果token中的audbasket就代表为购物车服务发的

JWT payload 示例:

{
  "iss": "https://localhost:5243",
  "aud": "basket",
  "sub": "123",
  "exp": 1710000000
}

开发环境我们也验证一下这个Audience

image-20251013212912499

8.注册授权AddAuthorization

8.1概念

services.AddAuthorization()ASP.NET Core DI 容器中的授权服务注册方法

  • 作用:在 依赖注入容器中注册授权服务,使你的应用可以使用 [Authorize] 属性、策略、角色等功能。
  • 注意:这只注册授权服务本身,不负责身份验证(Authentication)。
  • 通常配合 services.AddAuthentication() 使用
    • Authentication :确认用户身份
    • Authorization :确认用户是否有权限访问资源
  • 默认策略:
    • 如果 [Authorize] 没有指定角色或策略,默认策略就是:用户必须已认证 (context.User.Identity.IsAuthenticated == true)
    • 如果 [AllowAnonymous],则跳过授权

8.2授权策略

这部分只是扩展,认证授权之前有看过,好久不用忘了好多,重新在复习一下

授权策略(Policy):是一组规则,用来判断用户是否有权限访问某个资源。

[Authorize(Policy = "PolicyName")] 会触发策略验证。

ASP.NET Core 的授权是发生在 身份验证(Authentication)之后

AddAuthorization 的作用:在 DI 容器中注册授权服务,使策略、角色、Claim 验证生效。

总结

  • AddAuthorization() 注册授权服务。
  • 授权策略分为:认证、角色、Claim、自定义、默认策略。
  • 策略可以组合使用,实现从基础到复杂的权限控制。
  • [Authorize][AllowAnonymous]、策略、角色、Claim、Handler 配合,形成完整权限体系。

.8.1.1基于认证的策略(RequireAuthenticatedUser)

仅要求用户已认证,默认策略就是这个。

场景:普通登录用户访问 API。

eshop使用的就是这种

// 注册策略:声明AuthenticatedUser策略,认证即可
options.AddPolicy("AuthenticatedUser", policy =>
{
    policy.RequireAuthenticatedUser();
});

// 使用策略
[Authorize(Policy = "AuthenticatedUser")]
public IActionResult GetData() => Ok();

8.1.2基于角色的策略(RequireRole)

用户必须拥有指定角色。

场景:后台管理、权限分级。

// 注册策略:声明AdminOnly策略,需要认证角色
options.AddPolicy("AdminOnly", policy =>
{
    policy.RequireRole("Admin");            // 单角色
    // policy.RequireRole("Admin", "Super"); // 多角色
});

// 使用策略
[Authorize(Policy = "AdminOnly")]
public IActionResult DeleteUser() => Ok();

8.1.3基于Claim的策略(RequireClaim)

用户必须拥有指定 Claim(类型和值)。

场景:精细化权限控制,比如部门、项目或地域访问。

// 注册策略:声明HRDepartment策略,需要认证Claim中必须有`Department=HR`
options.AddPolicy("HRDepartment", policy =>
{
    policy.RequireClaim("Department", "HR");
});

// 使用策略
[Authorize(Policy = "HRDepartment")]
public IActionResult GetEmployeeList() => Ok();

8.1.4自定义策略(Requirement + Handler)

适合复杂逻辑、动态权限。

场景:复杂业务条件授权。

// 1、自定义 Requirement,表示用户需要拥有 "CanEditDocument" 权限
public class CanEditDocumentRequirement : IAuthorizationRequirement
{
    // Requirement 可以为空,Handler 会实现逻辑
}


// 2、处理 CanEditDocumentRequirement 的逻辑
public class CanEditDocumentHandler : AuthorizationHandler<CanEditDocumentRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,           // 当前用户和策略上下文
        CanEditDocumentRequirement requirement)       // 当前 Requirement
    {
        // 检查用户是否有名为 "CanEditDocument" 的 Claim
        if (context.User.HasClaim(c => c.Type == "CanEditDocument"))
        {
            // 授权成功
            context.Succeed(requirement);
        }

        // 如果没有满足条件,则不调用 Succeed,授权会失败
        return Task.CompletedTask;
    }
}

// 3、注册 Handler,在Program.cs 或 Startup.cs 中
services.AddSingleton<IAuthorizationHandler, CanEditDocumentHandler>(); 

// 4、注册授权服务,并定义策略 CanEditDocumentPolicy 
services.AddAuthorization(options =>
{
    // 定义一个名为 "CanEditDocumentPolicy" 的策略 添加策略条件
    options.AddPolicy("CanEditDocumentPolicy", policy =>
        policy.Requirements.Add(new CanEditDocumentRequirement()));
});

// 5、只用该策略,只有拥有 "CanEditDocument" Claim 的用户可以访问
    [HttpPost("edit")]
    [Authorize(Policy = "CanEditDocumentPolicy")]
    public IActionResult EditDocument()
    {
        return Ok("可以编辑文档");
    }

8.1.5默认策略(FallbackPolicy)

没有 [Authorize] 的请求也会使用这个策略。

[AllowAnonymous] 可覆盖 FallbackPolicy

场景:API 默认安全,部分端点允许匿名访问。

options.FallbackPolicy = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .Build();

8.1.6组合策略(角色+Claim)

可以组合角色 + Claim。

只有同时满足所有条件,授权才通过。

options.AddPolicy("AdminInHR", policy =>
{
    policy.RequireRole("Admin");
    policy.RequireClaim("Department", "HR");
});

8.3源码分析(AddAuthorization默认策略)

eshop没有配置默认策略,这里只是好奇,AI给了结果,但是AI也不是100%正确,就差看了一下源码,官方源码质量比较高的,我这里只是浅浅查看

跟踪AddAuthorization代码查看具体实现,核心是AddAuthorizationCore

步骤 注册内容 作用
AddAuthorizationCore IAuthorizationService + 核心 Handler 提供最基础的授权功能
AddAuthorizationPolicyEvaluator AuthorizationPolicyEvaluator 执行策略评估 [Authorize] 时调用
AuthorizationPolicyCache 授权策略缓存 提高性能,避免重复生成策略对象

image-20251014001249497

查看AddAuthorizationCore的实现:注册了 授权系统的核心服务、策略提供者、Handler 机制、策略评估器和上下文工厂,提供基础授权能力,但不包含任何自定义策略或业务逻辑。

注册组件 类型 作用
IAuthorizationService 核心服务 执行策略授权,调用 Handler 判断 Requirement
IAuthorizationPolicyProvider 策略提供者 根据策略名称返回 AuthorizationPolicy
IAuthorizationHandlerProvider Handler 提供者 提供所有注册的 Handler
IAuthorizationEvaluator 策略评估器 负责遍历 Requirement 并调用 Handler,得出授权结果
IAuthorizationHandlerContextFactory 上下文工厂 构建 AuthorizationHandlerContext 给 Handler 使用
IAuthorizationHandler(PassThrough) 默认 Handler 空实现,保证至少有一个 Handler

然后查看DefaultAuthorizationPolicyProvider,这个服务提供策略

image-20251014002722657

GetDefaultPolicyAsync就是返回默认的策略,首先从缓存获取,没有就获取_options.DefaultPolicy

image-20251014002846278

查看DefaultPolicy实现,new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()意思是必须是已认证用户(User.Identity.IsAuthenticated == true),没有额外的要求,比如角色或自定义要求。

image-20251014002921651

8.4eshop的授权策略

eshop就简单注册授权服务,保证 [Authorize] 能工作,默认行为就是“用户必须认证成功”

image-20251013235453834

📌 创作不易,感谢支持!

每一篇内容都凝聚了心血与热情,如果我的内容对您有帮助,欢迎请我喝杯咖啡☕,您的支持是我持续分享的最大动力!

💬 加入交流群(QQ群):576434538

微信打赏

posted @ 2025-12-05 23:39  peng_boke  阅读(55)  评论(0)    收藏  举报