Identity Server:已认证用户Access Token过期后的刷新方法问询
使用Refresh Token刷新过期的Access Token(基于Duende Identity Server的MVC客户端)
背景
你的MVC客户端已完成如下配置:
- 启用
Cookie+OpenIdConnect认证,包含offline_accessscope以获取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_accessscope,否则无法获取Refresh Token - Refresh Token有自身生命周期,需在Duende Identity Server后台合理配置过期时间
- 生产环境建议复用
HttpClient实例,避免频繁创建导致性能损耗 - 若Refresh Token也过期,需引导用户重新登录
内容的提问来源于stack exchange,提问作者KA-Yasso
相关产品推荐
相关产品推荐

