IdentityServer4认证授权之JWT方案
前言
回头看,轻舟已过万重山,
向前看,前路漫漫亦灿灿;
抬头看,满天星光甚浪漫,
低头看,满路荆棘已过半。
认证和授权
接着查看扩展方法AddApplicationServices的实现,他的第一个方法是builder.AddDefaultAuthentication(),我们跟踪AddDefaultAuthentication的具体实现

1.读取配置
AddDefaultAuthentication这部分代码主要是:
-
提取
IServiceCollection和IConfigurationManager -
读取
appsettings.json中Identity的配置(如果appsettings.json没有配置Identity节,则认为无需认证,但是购物车服务通常是需要认证的)

appsettings.json中Identity的配置参数(这2个参数我们下面详细看一下):
"Identity": {
//"Url": "http://identity",
"Audience": "basket"
},

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 名)。

假设 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 标准 的一致性(直接使用
sub、aud等原名)。避免隐式重命名导致的歧义或冲突(例如同时存在
sub与nameidentifier)。更适合微服务/多语言互操作场景:不同语言/库通常直接使用原始 claim 名。
3.注册认证服务
services.AddAuthentication() 是 ASP.NET Core 注册认证服务的入口方法。它本身不指定具体认证方案,只做基础设施初始化。
注册认证框架的核心服务
- 管理认证方案(Scheme):
IAuthenticationSchemeProvider - 管理认证处理器(Handler):
IAuthenticationHandlerProvider

注册辅助服务
- 数据保护(Cookie 加密、Token 加密)
- Web 编码器(Base64 / URL 编码)
- 系统时间提供者(验证 Token 过期)

返回 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)

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 接收、认证成功等 |

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 等) |

元数据接口
打开开发环境认证服务,之前的Identity.API我们已经调通了,这里可以测试一下
我们在eShop\src\Identity.API目录下执行以下命令,开启认证服务
dotnet run --urls https://localhost:5243/

然后访问认证服务,IdentityServer4界面上默认就可以打开元数据接口
自己也可以根据认证服务的地址+/.well-known/openid-configuration打开
https://localhost:5243/.well-known/openid-configuration

3.配置元数据接口地址
eshop中其实没有配置Identity:Url,我们既然学到这里了就先给配上,如果不行可以去掉或者再想办法
我们把IdentityServer4元数据接口地址配置到Identity:Url

为什么
eshop没有配置Identity:Url?在本地开发环境中:
- 微服务和 IdentityServer 都在本地运行,或者用 Docker 容器模拟。
- 为了方便调试,示例项目允许 只校验 Audience,而跳过访问远程 Authority。
- 避免每个开发者都必须配置本地 IdentityServer 地址,降低配置门槛。
eShop 样例项目只配置 Audience,是为了 本地开发和学习方便。
如果正式项目不配置 Authority,会发生什么?
无法自动获取元数据
- 框架不会访问
.well-known/openid-configuration- 无法获取签发者 (
issuer)、公钥 (jwks_uri) 和 Token EndpointToken 验证不完整
- 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 验证中配置 Audience 为 basket,表示该 token 仅能被购物车服务接收和使用,其他服务无法使用该 token,从而保证 token 的使用范围和安全性。

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 访问。

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。
ValidIssuers:IEnumerable<string>,允许列出多个受信任的 issuer(适合多 IdP / 多域名场景)。
为什么必须校验
iss?即便签名正确,仍需确认这个 token 是由受信任的签发方签发的(防止别的系统或被攻陷的 IdP 的 token 被接受)。
iss校验是信任链的一部分。

7.验证令牌目标Audience(ValidateAudience)
概念
ValidateAudience 是 JWT 验证参数中的一个布尔属性,用来控制 是否校验 token 的 aud(Audience)声明。
- aud(Audience)是 JWT 中的标准字段,表示 这个 token 面向哪个受众/资源。
ValidateAudience = true时,中间件会检查 token 中的aud是否匹配你配置的值(ValidAudience或ValidAudiences)。ValidateAudience = false时,会跳过aud校验。
ValidateAudience 决定你是否严格要求 token 是为你的 API 或资源发的。
示例
我们这里是购物车服务,如果token中的aud有basket就代表为购物车服务发的
JWT payload 示例:
{
"iss": "https://localhost:5243",
"aud": "basket",
"sub": "123",
"exp": 1710000000
}
开发环境我们也验证一下这个Audience

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 | 授权策略缓存 | 提高性能,避免重复生成策略对象 |

查看AddAuthorizationCore的实现:注册了 授权系统的核心服务、策略提供者、Handler 机制、策略评估器和上下文工厂,提供基础授权能力,但不包含任何自定义策略或业务逻辑。
| 注册组件 | 类型 | 作用 |
|---|---|---|
IAuthorizationService |
核心服务 | 执行策略授权,调用 Handler 判断 Requirement |
IAuthorizationPolicyProvider |
策略提供者 | 根据策略名称返回 AuthorizationPolicy |
IAuthorizationHandlerProvider |
Handler 提供者 | 提供所有注册的 Handler |
IAuthorizationEvaluator |
策略评估器 | 负责遍历 Requirement 并调用 Handler,得出授权结果 |
IAuthorizationHandlerContextFactory |
上下文工厂 | 构建 AuthorizationHandlerContext 给 Handler 使用 |
IAuthorizationHandler(PassThrough) |
默认 Handler | 空实现,保证至少有一个 Handler |
然后查看DefaultAuthorizationPolicyProvider,这个服务提供策略

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

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

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

📌 创作不易,感谢支持!
每一篇内容都凝聚了心血与热情,如果我的内容对您有帮助,欢迎请我喝杯咖啡☕,您的支持是我持续分享的最大动力!
💬 加入交流群(QQ群):576434538

浙公网安备 33010602011771号