服务器重启后IdentityServer4客户端token失效检测及新令牌申请方法
核心问题原因
服务端重启后token失效是默认配置下IdentityServer与ASP.NET Core Data Protection的密钥均存储在内存中导致的,服务重启密钥丢失后,所有已签发的JWT、会话凭证都会被判定为无效。未重启时自动静默刷新逻辑正常运行,所以次日访问、新开标签页无需重新登录。
优先推荐根本解决方案
通过持久化密钥避免服务重启导致的token批量失效,无需修改前端逻辑即可解决问题:
// Startup.cs 配置Data Protection密钥持久化 services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(@"自定义密钥存储目录路径")) .SetApplicationName("当前应用唯一标识"); // IdentityServer配置持久化签名密钥,生产环境建议使用正式证书 services.AddIdentityServer() .AddApiAuthorization<ApplicationUser, AppDbContext>() // 开发环境可使用.AddDeveloperSigningCredential(true) 将自动生成的签名密钥存储到本地文件 .AddSigningCredential(new X509Certificate2("证书文件路径", "证书访问密码"));
客户端token失效检测与主动刷新方案
如果需要实现需求指定的客户端检测、主动刷新能力,可按以下步骤实现:
a) 检测客户端token已失效
可通过两种方式组合检测:
- 本地预校验:发送API请求前读取
UserManager存储的用户对象的expires_at字段,对比当前时间判断token是否已经过期。 - 响应拦截校验:给Angular添加全局HTTP拦截器,识别API请求返回的401状态码判定token失效(当前无报错是因为默认配置下服务端会把未认证的API请求重定向到登录页,返回200状态的HTML内容,前端无法识别异常,可通过下方服务端配置修正返回状态):
// Startup.cs 配置API请求未认证时返回401而非重定向 services.ConfigureApplicationCookie(options => { options.Events.OnRedirectToLogin = context => { if (context.Request.Path.StartsWithSegments("/api") && context.Response.StatusCode == StatusCodes.Status200OK) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; return Task.CompletedTask; } context.Response.Redirect(context.RedirectUri); return Task.CompletedTask; }; });
b) 主动向服务端请求新token
你已经开启了automaticSilentRenew配置,默认会在token过期前自动触发静默刷新,服务端重启导致的token提前失效可通过手动调用signinSilent方法实现刷新:
- 在
authorize.service.ts中添加刷新token方法:
import { from, Observable } from 'rxjs'; import { User } from 'oidc-client'; // ...原有逻辑 public refreshToken(): Observable<User> { return from(this.userManager.signinSilent()); }
- 全局HTTP拦截器中集成自动刷新逻辑,示例代码:
import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http'; import { catchError, switchMap, throwError } from 'rxjs'; import { AuthorizeService } from './authorize.service'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private authService: AuthorizeService) {} intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { return next.handle(request).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401) { // 触发token静默刷新 return this.authService.refreshToken().pipe( switchMap((newUser) => { // 携带新token重发原请求 const clonedRequest = request.clone({ setHeaders: { Authorization: `Bearer ${newUser.access_token}` } }); return next.handle(clonedRequest); }), catchError((refreshError) => { // 刷新失败跳转登录页 this.authService.login(); return throwError(() => refreshError); }) ); } return throwError(() => error); }) ); } }
- 在Angular的模块中注册拦截器:
import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; @NgModule({ // ...其他配置 providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ] }) export class AppModule { }
内容的提问来源于stack exchange,提问作者Boppity Bop
相关产品推荐
相关产品推荐

