Keycloak集成ASP.NET Core MVC时角色声明无法生效问题
Keycloak角色无法在ASP.NET Core MVC中读取的问题
生产环境已部署Keycloak服务器,ASP.NET Core MVC应用能正常重定向完成Keycloak认证,但始终无法读取Token中的角色信息。Keycloak返回的Token里明确包含roles字段,尝试了自定义角色转换,但principal.identity中始终获取不到roles属性。
Startup.cs 配置
builder.Services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie(options => { options.LoginPath = "/Account/Login"; }) .AddOpenIdConnect(options => { options.Authority = "https://MyServer/auth/realms/ATG"; options.MetadataAddress = "https://MyServer/realms/ATG/.well-known/openid-configuration"; options.ClientId = "ATG.ad.yaskawa.com"; options.ClientSecret = "tpdyzbDOADYdsCUaoFz9bTJNqRsOsrcQ"; options.ResponseType = "code"; options.SaveTokens = true; options.Scope.Add("openid"); options.CallbackPath = "/signin-oidc"; // Update callback path options.SignedOutCallbackPath = "/signout-callback-oidc"; // Update signout callback path options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "preferred_username", RoleClaimType = "roles" }; }); builder.Services.AddTransient<IClaimsTransformation, CustomRoleClaimsTransformation>(); //Fix Telerik camelCase to PascalCase builder.Services.AddControllersWithViews().AddJsonOptions(options => options.JsonSerializerOptions.PropertyNamingPolicy = null); ; ConfigurationManager configuration = builder.Configuration; IdentityModelEventSource.ShowPII = true; var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseMigrationsEndPoint(); } else { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.MapRazorPages(); app.Run();
Keycloak返回的Token
{ "exp": 1720548651, "iat": 1720548351, "auth_time": 1720548351, "jti": "b9aca179-91c9-4528-834e-29f5d33e0308", "iss": "https://MyServer/realms/ATG", "aud": "account", "sub": "1ab9a926-d2a9-4941-9a01-ffe07d500abd", "typ": "Bearer", "azp": "ATG.ad.yaskawa.com", "sid": "fd0c995d-3ec7-4bec-8a0c-18449b393ee8", "acr": "1", "scope": "email profile", "email_verified": true, "roles": [ "Admin", "view-profile" ], "name": "Eric Obermuller", "preferred_username": "myEmail@yaskawa.com", "given_name": "Eric", "family_name": "O", "email": "myEmail@yaskawa.com" }
自定义角色转换类
public class CustomRoleClaimsTransformation : IClaimsTransformation { public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { var identity = (ClaimsIdentity)principal.Identity; // Ensure roles are being extracted and mapped correctly var roles = identity.FindAll("roles").Select(c => c.Value).ToList(); foreach (var role in roles) { identity.AddClaim(new Claim(ClaimTypes.Role, role)); } return Task.FromResult(principal); } }
问题排查与解决步骤
1. 补充请求Scope
从返回的Token可以看到,实际请求的Scope是email profile,并没有包含角色相关的Scope。需要在OpenIdConnect配置中添加roles或者客户端对应的Scope:
options.Scope.Add("roles"); // 如果是客户端级别的角色,还需要添加客户端ID作为Scope options.Scope.Add("ATG.ad.yaskawa.com");
2. 修正Authority路径一致性
当前Authority配置为https://MyServer/auth/realms/ATG,但MetadataAddress是https://MyServer/realms/ATG/.well-known/openid-configuration,两者路径不一致(多了/auth),可能导致元数据解析异常。统一路径:
options.Authority = "https://MyServer/realms/ATG"; // 无需手动设置MetadataAddress,会自动从Authority生成
3. 在Token验证事件中手动提取角色
如果默认的TokenValidationParameters无法正确解析数组类型的roles Claim,可以通过OpenIdConnect的OnTokenValidated事件手动提取并添加:
options.Events = new OpenIdConnectEvents { OnTokenValidated = context => { if (context.SecurityToken is JwtSecurityToken jwtToken) { // 直接从JWT Token中获取roles数组 var roleClaims = jwtToken.Claims.Where(c => c.Type == "roles"); var identity = (ClaimsIdentity)context.Principal.Identity; foreach (var roleClaim in roleClaims) { identity.AddClaim(new Claim(ClaimTypes.Role, roleClaim.Value)); } } return Task.CompletedTask; } };
4. 检查ClaimsTransformation的执行前提
在自定义转换类中,先打印所有Claim确认roles是否存在:
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { var identity = (ClaimsIdentity)principal.Identity; // 打印所有Claim,排查是否有roles字段 foreach (var claim in identity.Claims) { Console.WriteLine($"{claim.Type}: {claim.Value}"); } var roles = identity.FindAll("roles").Select(c => c.Value).ToList(); foreach (var role in roles) { identity.AddClaim(new Claim(ClaimTypes.Role, role)); } return Task.FromResult(principal); }
5. 验证Keycloak客户端配置
登录Keycloak后台检查客户端设置:
- 确认客户端
Access Type为confidential(与使用ClientSecret的配置匹配) - 开启
Full Scope Allowed,确保角色能被包含在Token中 - 确认用户已被正确分配对应角色
内容的提问来源于stack exchange,提问作者Selthien
相关产品推荐
相关产品推荐

