基于OpenIddict的角色授权问题:资源服务器端点403排查
我用OpenIddict搭建了独立授权服务器、资源服务器和SPA前端架构,基础授权功能正常:未登录调用端点返回401,登录后调用无角色限制的端点能正常获取数据,但添加了[Authorize(Roles = "ADMIN")]注解的资源服务器端点,在登录且持有包含ADMIN角色的access_token时仍返回403。以下是资源服务器的相关代码:
控制器代码
using Microsoft.AspNetCore.Mvc; using OpenIddict.Validation.AspNetCore; using System.Security.Claims; namespace TestProtectedApi.Controllers { [ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private readonly ILogger<WeatherForecastController> _logger; public WeatherForecastController(ILogger<WeatherForecastController> logger) { _logger = logger; } [Authorize(Roles = "ADMIN")] [HttpGet("GetWeatherForecast")] public IEnumerable<WeatherForecast> Get() { return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) .ToArray(); } [Authorize] [HttpGet("GetWeatherForecastNoRole")] public IEnumerable<WeatherForecast> GetNoRole() { return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) .ToArray(); } } }
资源服务器Program.cs配置
using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenIddict.Validation.AspNetCore; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddCors(options => { options.AddPolicy("AllowAllOrigins", builder => { builder .AllowCredentials() .WithOrigins( "http://localhost:4200", "http://localhost:4204", "https://localhost:7150") .SetIsOriginAllowedToAllowWildcardSubdomains() .AllowAnyHeader() .AllowAnyMethod(); }); }); builder.Services.AddOpenIddict() .AddValidation(options => { options.SetIssuer("https://localhost:7150/"); options.UseIntrospection() .SetClientId("api_server") .SetClientSecret("test"); options.UseSystemNetHttp(); options.UseAspNetCore(); }); builder.Services.AddCors(); builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); builder.Services.AddAuthorization(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { } app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.UseCors("AllowAllOrigins"); app.Run();
针对403问题,按以下步骤排查和修复:
修正中间件顺序
CORS中间件必须放在认证、授权和路由之前,否则无法正确处理跨域请求头。调整Program.cs中的中间件顺序:app.UseHttpsRedirection(); // 移到认证授权之前 app.UseCors("AllowAllOrigins"); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();映射角色声明类型
ASP.NET Core默认使用http://schemas.microsoft.com/ws/2008/06/identity/claims/role作为角色声明类型,而OpenIddict通常返回role(小写)声明。在OpenIddict验证配置中添加声明映射:builder.Services.AddOpenIddict() .AddValidation(options => { options.SetIssuer("https://localhost:7150/"); options.AddAudiences("api_server"); // 确保匹配token的audience options.UseIntrospection() .SetClientId("api_server") .SetClientSecret("test"); options.UseSystemNetHttp(); options.UseAspNetCore(); // 映射OpenIddict的role声明到ASP.NET Core的角色类型 options.AddClaim(OpenIddictConstants.Claims.Role, System.Security.Claims.ClaimTypes.Role); });验证Introspection端点返回的角色
检查授权服务器的Introspection端点响应,确保返回的claims字段包含role: "ADMIN"。可以用接口调试工具调用https://localhost:7150/connect/introspect,携带token、client_id和client_secret参数,查看返回结果。自定义授权策略(可选)
如果不想修改声明映射,可以直接在授权配置中指定使用role声明:builder.Services.AddAuthorization(options => { options.AddPolicy("AdminOnly", policy => { policy.RequireAuthenticatedUser(); policy.RequireClaim("role", "ADMIN"); }); });然后控制器中替换为:
[Authorize(Policy = "AdminOnly")] [HttpGet("GetWeatherForecast")]确认token受众匹配
确保access_token的aud字段包含api_server,否则资源服务器在验证时会过滤掉不相关的声明。
内容的提问来源于stack exchange,提问作者kevin van gils

