Angular2基于JWT的认证实现:用户活跃时Token自动刷新方案咨询
实现基于JWT的Token自动刷新(Angular + Spring)
这需求太普遍了,我在好几个项目里都这么落地过,分享下实际可行的方案,分后端Spring和前端Angular两部分拆解:
一、后端Spring的核心调整
首先得把JWT拆成Access Token和Refresh Token两个角色:
- Access Token:保持15分钟过期,用来做日常接口的身份校验,过期时间短更安全
- Refresh Token:过期时间设长一点(比如7天),专门用来申请新的Access Token,相当于用户的"长期通行证"
1. 登录接口返回双Token
登录成功时,同时生成并返回这两个Token,响应体示例:
{ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "accessExpiresIn": 900, // 15分钟(秒) "refreshExpiresIn": 604800 // 7天(秒) }
2. 实现Refresh Token刷新接口
写一个POST接口(比如/api/auth/refresh),接收前端传来的Refresh Token,做这几步校验:
- 验证Token签名合法性
- 检查Token是否过期
- 从数据库/Redis中校验该Refresh Token是否绑定当前用户(防止被盗用后注销仍能使用)
校验通过后,生成新的Access Token(推荐同时生成新的Refresh Token,滚动刷新更安全),返回给前端。
Spring Boot示例代码:
@PostMapping("/refresh") public ResponseEntity<AuthResponse> refreshToken(@RequestBody RefreshTokenRequest request) { // 解析Refresh Token获取用户信息 Claims claims = jwtUtil.extractAllClaims(request.getRefreshToken()); String username = claims.getSubject(); // 从数据库查询用户,校验Refresh Token有效性 User user = userService.findByUsername(username); if (!jwtUtil.validateToken(request.getRefreshToken(), user) || !request.getRefreshToken().equals(user.getRefreshToken())) { throw new UnauthorizedException("无效的刷新令牌"); } // 生成新的双Token String newAccessToken = jwtUtil.generateToken(user); String newRefreshToken = jwtUtil.generateRefreshToken(user); // 更新数据库中的Refresh Token user.setRefreshToken(newRefreshToken); userService.save(user); return ResponseEntity.ok(new AuthResponse(newAccessToken, newRefreshToken, 900, 604800)); }
3. 后端安全注意事项
- Refresh Token必须存在持久化存储(数据库/Redis),绑定用户ID,注销时立即清除对应记录
- Refresh Token过期时间别太长,平衡安全性和用户体验
- 给刷新接口加限流,防止恶意刷接口攻击
二、前端Angular的自动刷新逻辑
核心是用HttpInterceptor拦截所有请求,自动处理Token过期和刷新,让用户完全无感知。
1. 封装AuthService管理Token
写一个服务来统一存储、获取、更新Token:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Router } from '@angular/router'; @Injectable({ providedIn: 'root' }) export class AuthService { private readonly ACCESS_TOKEN_KEY = 'access_token'; private readonly REFRESH_TOKEN_KEY = 'refresh_token'; private readonly ACCESS_EXPIRES_KEY = 'access_expires'; constructor(private http: HttpClient, private router: Router) {} getAccessToken(): string | null { return localStorage.getItem(this.ACCESS_TOKEN_KEY); } getRefreshToken(): string | null { return localStorage.getItem(this.REFRESH_TOKEN_KEY); } setTokens(accessToken: string, refreshToken: string, accessExpiresIn: number): void { const expiresAt = Date.now() + accessExpiresIn * 1000; localStorage.setItem(this.ACCESS_TOKEN_KEY, accessToken); localStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken); localStorage.setItem(this.ACCESS_EXPIRES_KEY, expiresAt.toString()); } isAccessTokenExpiring(): boolean { const expiresAt = parseInt(localStorage.getItem(this.ACCESS_EXPIRES_KEY) || '0', 10); // 提前3分钟判断是否要刷新,避免请求时刚好过期 return Date.now() > expiresAt - 3 * 60 * 1000; } async refreshToken(): Promise<any> { const refreshToken = this.getRefreshToken(); if (!refreshToken) throw new Error('没有可用的刷新令牌'); return this.http.post('/api/auth/refresh', { refreshToken }).toPromise(); } logout(): void { localStorage.removeItem(this.ACCESS_TOKEN_KEY); localStorage.removeItem(this.REFRESH_TOKEN_KEY); localStorage.removeItem(this.ACCESS_EXPIRES_KEY); this.router.navigate(['/login']); } }
2. 实现HttpInterceptor拦截请求
拦截所有HTTP请求,自动处理Token过期逻辑:
import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpInterceptor, HttpErrorResponse } from '@angular/common/http'; import { catchError, switchMap, filter, take, throwError } from 'rxjs/operators'; import { BehaviorSubject, Observable } from 'rxjs'; @Injectable() export class AuthInterceptor implements HttpInterceptor { private isRefreshing = false; private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null); constructor(private authService: AuthService) {} intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> { // 登录和刷新接口直接放行 if (request.url.includes('/api/auth/login') || request.url.includes('/api/auth/refresh')) { return next.handle(request); } const accessToken = this.authService.getAccessToken(); if (accessToken) { // 检查Token是否快过期 if (this.authService.isAccessTokenExpiring()) { return this.handleRefreshToken(request, next); } // 给请求加上Authorization头 request = this.addToken(request, accessToken); } return next.handle(request).pipe( catchError((error: HttpErrorResponse) => { // 401说明Access Token已过期,尝试刷新 if (error.status === 401 && !request.url.includes('/api/auth/refresh')) { return this.handleRefreshToken(request, next); } // 其他错误(比如Refresh Token也过期)直接登出 this.authService.logout(); return throwError(() => error); }) ); } private handleRefreshToken(request: HttpRequest<any>, next: HttpHandler): Observable<any> { if (!this.isRefreshing) { this.isRefreshing = true; this.refreshTokenSubject.next(null); return this.authService.refreshToken().pipe( switchMap((tokens: any) => { this.isRefreshing = false; this.refreshTokenSubject.next(tokens.accessToken); // 更新本地Token this.authService.setTokens(tokens.accessToken, tokens.refreshToken, tokens.accessExpiresIn); // 重新发送原请求 return next.handle(this.addToken(request, tokens.accessToken)); }), catchError((error) => { this.isRefreshing = false; this.authService.logout(); return throwError(() => error); }) ); } else { // 正在刷新时,等待刷新完成后再发请求 return this.refreshTokenSubject.pipe( filter(token => token !== null), take(1), switchMap(token => { return next.handle(this.addToken(request, token)); }) ); } } private addToken(request: HttpRequest<any>, token: string): HttpRequest<any> { return request.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); } }
3. 注册拦截器
在app.module.ts中注册拦截器,让它生效:
import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ] }) export class AppModule {}
三、关键细节提醒
- 并发请求处理:拦截器里的
isRefreshing标志和BehaviorSubject能避免多个请求同时触发刷新,只发一次刷新请求,其他请求等待结果 - 存储选择:敏感系统推荐用
sessionStorage代替localStorage,浏览器关闭后自动清除,安全性更高 - 异常兜底:如果Refresh Token也过期了,一定要直接跳转到登录页,不能无限循环刷新
- 注销同步:用户主动注销时,后端要立即删除数据库里的Refresh Token,防止被恶意使用
内容的提问来源于stack exchange,提问作者syv
相关产品推荐
相关产品推荐

