You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Identity Server:更新用户信息后刷新Razor客户端Cookie及AccessToken方案咨询

问题:Razor客户端更新用户资料后刷新Cookie中的Access Token

架构背景

  • 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:

  1. 获取当前用户的Refresh Token:
var refreshToken = await HttpContext.GetTokenAsync("refresh_token");
  1. 调用Identity Server的Token端点获取新的Access Token和Refresh Token(逻辑参考现有OnValidatePrincipal中的代码)
  2. 更新认证票据并重新签发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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 04:02:04