Identity Server:更新用户信息后刷新Razor客户端Cookie及AccessToken方案咨询
架构背景
- Identity Server:提供认证服务,设有更新用户资料的自定义端点,nickname、country声明仅在请求ApiScope时包含在Access Token中(而非Identity Token)
- Api:仅通过
AddJWTBearer验证包含上述两个声明的Access Token - Razor Web客户端:使用Cookie认证,通过OpenID Connect对接Identity Server
核心问题
Razor客户端调用Identity Server的自定义端点更新用户资料后,客户端Cookie中存储的Access Token仍保留旧的nickname和country声明,导致后续调用Api时验证失败。需要更新Cookie中的Access Token为包含最新声明的版本。
已知用Refresh Token可以获取新的Access Token,但现有代码的刷新逻辑无法同步更新Cookie。
现有客户端代码示例
builder.Services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }) .AddOpenIdConnect("oidc", options => { options.Authority = "https://localhost:44302"; options.ClientId = "web"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.SaveTokens = true; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("offline_access"); options.Scope.Add("IdentityServerApi"); options.GetClaimsFromUserInfoEndpoint = true; options.ClaimActions.MapAll(); }) .AddCookie("Cookies", options => { options.Events = new CookieAuthenticationEvents { OnValidatePrincipal = async x => { if (x.HttpContext != null && x.HttpContext.User != null && x.HttpContext.User.Identity != null) { var now = DateTimeOffset.UtcNow; var timeElapsed = now.Subtract(value: x.Properties.IssuedUtc!.Value); var timeRemaining = x.Properties.ExpiresUtc!.Value.Subtract(now); if (timeElapsed > timeRemaining) { using var discoveryHttpClient = new HttpClient(); var discoveryResponse = await discoveryHttpClient.GetDiscoveryDocumentAsync("https://localhost:44302"); if (discoveryResponse.IsError) { throw new Exception(discoveryResponse.Error); } if (x.Principal != null) { if (x.Principal.Identity is ClaimsIdentity identity) { var accessTokenClaim = identity.FindFirst("access_token"); var refreshTokenClaim = identity.FindFirst("refresh_token"); var tokenClient = new HttpClient(); var refreshTokenEndpoint = discoveryResponse.TokenEndpoint ?? string.Empty; tokenClient.BaseAddress = new Uri(refreshTokenEndpoint); var refreshToken = refreshTokenClaim?.Value ?? string.Empty; var tokenResponse = await tokenClient.RequestRefreshTokenAsync(new RefreshTokenRequest() { ClientId = "web", ClientSecret = "secret", GrantType = "refresh_token", RefreshToken = refreshToken }); if (tokenResponse != null && !tokenResponse.IsError && !string.IsNullOrWhiteSpace(tokenResponse.AccessToken) && !string.IsNullOrWhiteSpace(tokenResponse.RefreshToken)) { identity.RemoveClaim(accessTokenClaim); identity.RemoveClaim(refreshTokenClaim); identity.AddClaims(new[] { new Claim("access_token", tokenResponse.AccessToken), new Claim("refresh_token", tokenResponse.RefreshToken) }); x.ShouldRenew = true; } } } } } } }; });
解决方案
方案1:更新资料后主动触发Refresh Token获取并更新Cookie
在Razor客户端调用完资料更新接口后,直接执行Refresh Token流程并重新签发Cookie:
- 获取当前用户的Refresh Token:
var refreshToken = await HttpContext.GetTokenAsync("refresh_token");
- 调用Identity Server的Token端点获取新的Access Token和Refresh Token(逻辑参考现有
OnValidatePrincipal中的代码) - 更新认证票据并重新签发Cookie:
var principal = HttpContext.User; if (principal.Identity is ClaimsIdentity identity) { // 移除旧token声明 var oldAccessToken = identity.FindFirst("access_token"); var oldRefreshToken = identity.FindFirst("refresh_token"); if (oldAccessToken != null) identity.RemoveClaim(oldAccessToken); if (oldRefreshToken != null) identity.RemoveClaim(oldRefreshToken); // 添加新token声明 identity.AddClaim(new Claim("access_token", newAccessToken)); identity.AddClaim(new Claim("refresh_token", newRefreshToken)); // 重新签发Cookie var newPrincipal = new ClaimsPrincipal(identity); await HttpContext.SignInAsync("Cookies", newPrincipal, HttpContext.User.ClaimsPrincipal.Properties); }
方案2:优化OnValidatePrincipal逻辑,确保Cookie正确更新
现有代码直接操作Claims的方式存在问题,应改用AuthenticationProperties的Token存储机制:
if (tokenResponse != null && !tokenResponse.IsError) { // 更新Properties中存储的Token(SaveTokens的Token实际存在这里) x.Properties.UpdateTokenValue("access_token", tokenResponse.AccessToken); x.Properties.UpdateTokenValue("refresh_token", tokenResponse.RefreshToken); // 标记需要刷新票据,框架自动重新签发Cookie x.ShouldRenew = true; }
注意:不要直接操作Claims中的
access_token,SaveTokens存储的Token是保存在AuthenticationProperties中的,而非Claims集合里。
方案3:强制触发OpenID Connect重新认证(简单但体验稍弱)
在资料更新后,直接触发重新认证流程,获取包含最新声明的Token:
await HttpContext.ChallengeAsync("oidc", new AuthenticationProperties { RedirectUri = "/profile" // 更新后跳转回资料页 });
这种方式会引导用户进入Identity Server的认证流程(会话未过期时会自动完成登录),能确保获取最新的Access Token。
内容的提问来源于stack exchange,提问作者Style
相关产品推荐
相关产品推荐

