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

Identity Server:已认证用户Access Token过期后的刷新方法问询

使用Refresh Token刷新过期的Access Token(基于Duende Identity Server的MVC客户端)

背景

你的MVC客户端已完成如下配置:

  • 启用Cookie + OpenIdConnect认证,包含offline_access scope以获取Refresh Token
  • Access Token生命周期设为120秒,过期后需通过Refresh Token获取新令牌
  • 遵循Duende Identity Server交互快速入门方案实现

身份认证配置参考

services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = JwtClaimTypes.Name,
            RoleClaimType = JwtClaimTypes.Role,
        };
        
        options.SignedOutRedirectUri = "/Home";
        options.SignedOutCallbackPath = "/Account/SignOutCallback";

        options.Authority = Authority;
        options.ClientId = ClientId;
        options.ClientSecret = ClientSecret;
        options.ResponseType = "code";
        
        //Scopes is "Api.Scope", "offline_access"
        foreach (var scope in Scopes)
        {
            options.Scope.Add(scope);
        }

        options.SaveTokens = true;
    });

解决方案

方案1:通过中间件事件自动刷新令牌

修改OpenIdConnect配置,添加事件处理器,在令牌过期或验证时自动触发刷新:

.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    // 保留原有配置...

    options.Events = new OpenIdConnectEvents
    {
        // 令牌验证通过后,提前检查是否需要刷新
        OnTokenValidated = async context =>
        {
            var refreshToken = await context.HttpContext.GetTokenAsync("refresh_token");
            if (string.IsNullOrEmpty(refreshToken)) return;

            var tokenClient = new HttpClient();
            var discoveryDoc = await tokenClient.GetDiscoveryDocumentAsync(options.Authority);
            if (discoveryDoc.IsError) throw new Exception(discoveryDoc.Error);

            var refreshResponse = await tokenClient.RequestRefreshTokenAsync(new RefreshTokenRequest
            {
                Address = discoveryDoc.TokenEndpoint,
                ClientId = options.ClientId,
                ClientSecret = options.ClientSecret,
                RefreshToken = refreshToken
            });

            if (!refreshResponse.IsError)
            {
                var authProps = context.Properties;
                authProps.UpdateTokenValue("access_token", refreshResponse.AccessToken);
                authProps.UpdateTokenValue("refresh_token", refreshResponse.RefreshToken);

                await context.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, context.Principal, authProps);
            }
        },

        // 认证失败时(如Access Token过期)尝试刷新
        OnAuthenticationFailed = async context =>
        {
            if (context.Exception is SecurityTokenExpiredException)
            {
                var refreshToken = await context.HttpContext.GetTokenAsync("refresh_token");
                if (string.IsNullOrEmpty(refreshToken)) return;

                var tokenClient = new HttpClient();
                var discoveryDoc = await tokenClient.GetDiscoveryDocumentAsync(options.Authority);
                if (discoveryDoc.IsError) return;

                var refreshResponse = await tokenClient.RequestRefreshTokenAsync(new RefreshTokenRequest
                {
                    Address = discoveryDoc.TokenEndpoint,
                    ClientId = options.ClientId,
                    ClientSecret = options.ClientSecret,
                    RefreshToken = refreshToken
                });

                if (!refreshResponse.IsError)
                {
                    var authProps = context.Properties;
                    authProps.UpdateTokenValue("access_token", refreshResponse.AccessToken);
                    authProps.UpdateTokenValue("refresh_token", refreshResponse.RefreshToken);

                    await context.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, context.Principal, authProps);
                    context.HandleResponse();
                    context.Response.Redirect(context.Request.Path);
                }
            }
        }
    };
});

方案2:手动触发刷新(API调用前检查)

在调用API前主动检查Access Token状态,过期则手动刷新:

private async Task<string> GetValidAccessTokenAsync(IHttpContextAccessor contextAccessor)
{
    var httpContext = contextAccessor.HttpContext;
    var accessToken = await httpContext.GetTokenAsync("access_token");
    var refreshToken = await httpContext.GetTokenAsync("refresh_token");

    // 检查Access Token是否过期
    var tokenHandler = new JwtSecurityTokenHandler();
    if (tokenHandler.CanReadToken(accessToken))
    {
        var jwtToken = tokenHandler.ReadJwtToken(accessToken);
        if (jwtToken.ValidTo > DateTime.UtcNow)
        {
            return accessToken;
        }
    }

    // 无有效Refresh Token,需重新登录
    if (string.IsNullOrEmpty(refreshToken))
    {
        throw new UnauthorizedAccessException("无可用刷新令牌,请重新登录");
    }

    // 发起刷新请求
    var tokenClient = new HttpClient();
    var discoveryDoc = await tokenClient.GetDiscoveryDocumentAsync(Authority);
    if (discoveryDoc.IsError)
    {
        throw new Exception($"获取发现文档失败:{discoveryDoc.Error}");
    }

    var refreshResponse = await tokenClient.RequestRefreshTokenAsync(new RefreshTokenRequest
    {
        Address = discoveryDoc.TokenEndpoint,
        ClientId = ClientId,
        ClientSecret = ClientSecret,
        RefreshToken = refreshToken
    });

    if (refreshResponse.IsError)
    {
        throw new UnauthorizedAccessException($"令牌刷新失败:{refreshResponse.Error}");
    }

    // 更新Cookie中的令牌
    var authResult = await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    authResult.Properties.UpdateTokenValue("access_token", refreshResponse.AccessToken);
    authResult.Properties.UpdateTokenValue("refresh_token", refreshResponse.RefreshToken);

    await httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, authResult.Principal, authResult.Properties);

    return refreshResponse.AccessToken;
}

关键注意事项

  • 确保客户端已配置offline_access scope,否则无法获取Refresh Token
  • Refresh Token有自身生命周期,需在Duende Identity Server后台合理配置过期时间
  • 生产环境建议复用HttpClient实例,避免频繁创建导致性能损耗
  • 若Refresh Token也过期,需引导用户重新登录

内容的提问来源于stack exchange,提问作者KA-Yasso

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 02:35:10