如何在Identity Server 4登录后从客户端注入自定义Claims?
可行性说明与实现方案
完全可行,但客户端无法直接修改已颁发的令牌(令牌由Identity Server 4签名,篡改后会失效)。正确的做法是通过刷新令牌流程,将客户端侧选择的信息传递给ID4服务器,由服务器重新颁发包含新Claims的令牌。
实现步骤与示例代码
一、Identity Server 4 服务器端配置
1. 客户端配置(允许刷新令牌)
在ID4的客户端配置类(如Config.cs)中,确保客户端支持离线访问(刷新令牌):
public static IEnumerable<Client> GetClients() { return new List<Client> { new Client { ClientId = "web_app", ClientName = "Web应用客户端", AllowedGrantTypes = GrantTypes.Code, RequirePkce = true, ClientSecrets = { new Secret("your_client_secret".Sha256()) }, RedirectUris = { "https://your-client-domain/signin-oidc" }, PostLogoutRedirectUris = { "https://your-client-domain/signout-callback-oidc" }, AllowedScopes = { "openid", "profile", "api1", "offline_access" }, AllowOfflineAccess = true, // 开启刷新令牌支持 AccessTokenLifetime = 3600, // 按需设置令牌有效期 } }; }
2. 自定义ProfileService(添加客户端传递的Claims)
修改或实现IProfileService,读取客户端提交的参数并注入Claims:
public class CustomProfileService : IProfileService { private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory; private readonly UserManager<ApplicationUser> _userManager; public CustomProfileService(UserManager<ApplicationUser> userManager, IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory) { _userManager = userManager; _claimsFactory = claimsFactory; } public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var userId = context.Subject.GetSubjectId(); var user = await _userManager.FindByIdAsync(userId); if (user == null) throw new ArgumentException("用户不存在"); // 获取基础用户Claims var principal = await _claimsFactory.CreateAsync(user); var claims = principal.Claims.ToList(); // 读取客户端传递的额外参数 if (context.Request.Raw.TryGetValue("company", out var company)) claims.Add(new Claim("company", company)); if (context.Request.Raw.TryGetValue("position", out var position)) claims.Add(new Claim("position", position)); if (context.Request.Raw.TryGetValue("fiscal_year", out var fiscalYear)) claims.Add(new Claim("fiscal_year", fiscalYear)); context.IssuedClaims = claims; } public async Task IsActiveAsync(IsActiveContext context) { var userId = context.Subject.GetSubjectId(); var user = await _userManager.FindByIdAsync(userId); context.IsActive = user != null; } }
3. 注册ProfileService
在服务器启动类(Program.cs或Startup.cs)中注册自定义服务:
builder.Services.AddIdentityServer() .AddAspNetIdentity<ApplicationUser>() .AddProfileService<CustomProfileService>() // 其他ID4配置(如资源、客户端)...
二、客户端(ASP.NET Core Web应用)实现
1. 用户选择信息页面(示例视图)
创建用户选择公司、职位、财年的表单(SelectInfo.cshtml):
<form method="post" asp-action="UpdateUserClaims"> <div> <label>公司名称:</label> <input type="text" name="company" required /> </div> <div> <label>职位:</label> <input type="text" name="position" required /> </div> <div> <label>财年:</label> <input type="text" name="fiscalYear" required /> </div> <button type="submit">确认并更新身份信息</button> </form>
2. 刷新令牌并获取新Claims的Controller方法
public class AccountController : Controller { private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _config; public AccountController(IHttpClientFactory httpClientFactory, IConfiguration config) { _httpClientFactory = httpClientFactory; _config = config; } [HttpPost] public async Task<IActionResult> UpdateUserClaims(string company, string position, string fiscalYear) { // 从Cookie/Session中获取刷新令牌(需确保客户端配置中开启SaveTokens) var refreshToken = await HttpContext.GetTokenAsync("refresh_token"); if (string.IsNullOrEmpty(refreshToken)) return RedirectToAction("Login"); // 构造令牌刷新请求 var client = _httpClientFactory.CreateClient(); var tokenEndpoint = $"{_config["IdentityServer:Authority"]}/connect/token"; var requestContent = new FormUrlEncodedContent(new Dictionary<string, string> { ["grant_type"] = "refresh_token", ["client_id"] = "web_app", ["client_secret"] = "your_client_secret", ["refresh_token"] = refreshToken, ["scope"] = "openid profile api1 offline_access", ["company"] = company, ["position"] = position, ["fiscal_year"] = fiscalYear }); var response = await client.PostAsync(tokenEndpoint, requestContent); if (!response.IsSuccessStatusCode) return RedirectToAction("Login"); // 解析新令牌并更新客户端身份 var tokenResponse = await response.Content.ReadFromJsonAsync<TokenResponse>(); if (tokenResponse == null) return RedirectToAction("Login"); // 更新Cookie中的令牌 var authProperties = new AuthenticationProperties(); authProperties.StoreTokens(new[] { new AuthenticationToken { Name = "access_token", Value = tokenResponse.AccessToken }, new AuthenticationToken { Name = "refresh_token", Value = tokenResponse.RefreshToken } }); await HttpContext.SignInAsync("Cookies", User, authProperties); return RedirectToAction("Index", "Home"); } // 辅助类:解析令牌响应 private class TokenResponse { public string AccessToken { get; set; } public string RefreshToken { get; set; } } }
3. 客户端认证配置(确保支持刷新令牌)
在客户端启动类中,配置OpenID Connect时开启令牌保存:
builder.Services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.Authority = builder.Configuration["IdentityServer:Authority"]; options.ClientId = "web_app"; options.ClientSecret = "your_client_secret"; options.ResponseType = "code"; options.SaveTokens = true; // 保存令牌到Cookie中 options.Scope.Add("offline_access"); // 其他配置... });
三、注意事项
- 服务器端需对客户端传递的参数做合法性校验(如验证用户是否有权限选择该公司),避免恶意注入。
- 若无需将这些Claims传递给API服务,也可直接存储在客户端Session中,无需修改令牌。
- 刷新令牌的有效期需合理设置,避免频繁请求服务器。
内容的提问来源于stack exchange,提问作者Gopi
相关产品推荐
相关产品推荐

