Angular与Symfony项目中,为何要采用令牌系统做身份认证?
Angular + Symfony JWT 身份认证方案梳理与实现建议
嘿,你这套Angular前端搭配Symfony后端的JWT认证思路很靠谱!我结合你提到的Bundle和认证流程,把关键实现细节和注意事项整理出来,帮你把这个体系落地得更顺畅:
项目核心架构与认证流程
你当前的认证流程逻辑是完全合理的,梳理下来是这样:
- 前端提交用户名+密码到后端登录接口
- 后端生成访问令牌(Access Token)和刷新令牌(Refresh Token)
- 前端通过
localStorage存储这两个令牌 - 每次发起业务请求前,前端检查Access Token是否过期:
- 未过期:直接携带令牌请求后端
- 已过期:调用刷新令牌接口获取新的Access Token,再发起请求
已引入的关键Symfony Bundle
你提到的两个Bundle是JWT认证体系的核心:
- LexikJWTAuthenticationBundle:负责JWT令牌的生成、签名、验证全流程,是整个认证体系的基础
- JWTRefreshTokenBundle(应该是指
gesdinet/jwt-refresh-token-bundle):专门处理刷新令牌的持久化、校验和刷新逻辑,解决Access Token过期后无需重复登录的痛点
后端Symfony配置与实现要点
1. 基础配置
确保在config/packages/lexik_jwt_authentication.yaml中正确配置密钥和令牌有效期:
lexik_jwt_authentication: secret_key: '%env(resolve:JWT_SECRET_KEY)%' public_key: '%env(resolve:JWT_PUBLIC_KEY)%' pass_phrase: '%env(JWT_PASSPHRASE)%' token_ttl: 3600 # Access Token建议设为1小时,安全性更高
配置刷新令牌的有效期和关联字段(config/packages/gesdinet_jwt_refresh_token.yaml):
gesdinet_jwt_refresh_token: token_parameter_name: refresh_token ttl: 2592000 # 刷新令牌可以设为30天,平衡安全性和用户体验 user_identity_field: username # 关联用户的唯一标识字段
2. 路由配置
在config/routes.yaml中添加登录和刷新令牌的接口路由:
api_login_check: path: /api/login_check methods: [POST] api_refresh_token: path: /api/token/refresh methods: [POST]
3. 额外优化建议
- 可以在用户注销时,调用接口删除数据库中对应的刷新令牌,防止令牌被滥用
- 考虑给刷新令牌添加唯一标识,每次刷新时生成新的刷新令牌并作废旧的,提升安全性
前端Angular实现要点
1. 封装认证服务(AuthService)
把令牌的存储、获取、过期检查逻辑封装成一个全局服务:
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class AuthService { private readonly accessTokenKey = 'access_token'; private readonly refreshTokenKey = 'refresh_token'; getAccessToken(): string | null { return localStorage.getItem(this.accessTokenKey); } getRefreshToken(): string | null { return localStorage.getItem(this.refreshTokenKey); } setTokens(accessToken: string, refreshToken: string): void { localStorage.setItem(this.accessTokenKey, accessToken); localStorage.setItem(this.refreshTokenKey, refreshToken); } isTokenExpired(token: string): boolean { const payload = JSON.parse(atob(token.split('.')[1])); const expirationTimestamp = payload.exp * 1000; return Date.now() > expirationTimestamp; } logout(): void { localStorage.removeItem(this.accessTokenKey); localStorage.removeItem(this.refreshTokenKey); // 这里可以添加跳转到登录页的逻辑,比如this.router.navigate(['/login']) } }
2. 实现HTTP拦截器自动处理令牌
用Angular的HttpInterceptor拦截所有请求,自动携带令牌并处理过期刷新:
import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError, switchMap } from 'rxjs/operators'; import { AuthService } from './auth.service'; import { HttpClient } from '@angular/common/http'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private authService: AuthService, private http: HttpClient) {} intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { const accessToken = this.authService.getAccessToken(); if (!accessToken) { return next.handle(request); } // 检查Access Token是否过期 if (this.authService.isTokenExpired(accessToken)) { // 调用刷新令牌接口 return this.refreshToken().pipe( switchMap((newTokens: { access_token: string; refresh_token: string }) => { this.authService.setTokens(newTokens.access_token, newTokens.refresh_token); const updatedRequest = request.clone({ setHeaders: { Authorization: `Bearer ${newTokens.access_token}` } }); return next.handle(updatedRequest); }), catchError(error => { // 刷新失败,强制登出 this.authService.logout(); return throwError(() => new Error('令牌刷新失败,请重新登录')); }) ); } // 令牌有效,直接携带请求 const updatedRequest = request.clone({ setHeaders: { Authorization: `Bearer ${accessToken}` } }); return next.handle(updatedRequest); } private refreshToken(): Observable<{ access_token: string; refresh_token: string }> { const refreshToken = this.authService.getRefreshToken(); return this.http.post<{ access_token: string; refresh_token: string }>( '/api/token/refresh', { refresh_token: refreshToken } ); } }
3. 登录组件处理登录请求
在登录组件中调用登录接口,存储返回的令牌:
import { Component } from '@angular/core'; import { AuthService } from '../auth.service'; import { HttpClient } from '@angular/common/http'; import { Router } from '@angular/router'; @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent { username = ''; password = ''; loginError = ''; constructor( private http: HttpClient, private authService: AuthService, private router: Router ) {} onSubmit(): void { this.http.post<{ access_token: string; refresh_token: string }>( '/api/login_check', { username: this.username, password: this.password } ).subscribe({ next: tokens => { this.authService.setTokens(tokens.access_token, tokens.refresh_token); this.router.navigate(['/']); // 登录成功跳转到首页 }, error: () => { this.loginError = '用户名或密码错误'; } }); } }
重要安全提示
localStorage存储令牌存在XSS攻击风险,如果你的项目对安全性要求极高,可以考虑将刷新令牌存储在HttpOnlyCookie中,配合前端处理跨域问题- 定期清理过期的刷新令牌,可以在Symfony中添加命令或者事件监听来自动删除数据库中过期的令牌
- 避免在前端暴露敏感的令牌细节,比如不要在控制台打印令牌内容
内容的提问来源于stack exchange,提问作者SlimenTN
相关产品推荐
相关产品推荐

