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

Symfony/LexitJWT+Angular中refresh_token续期与持久化认证问题

解决方案:实现两周免登录的JWT持续认证

你的核心问题是refresh token与access token(主JWT)有效期完全同步,导致主token过期时refresh token也已失效,无法完成自动刷新。要实现两周不登录仍能保持认证,必须单独延长refresh token的有效期,并调整后端配置和前端逻辑。

一、Symfony后端配置独立的Refresh Token有效期

假设你使用的是lexik/jwt-authentication-bundle,需要为refresh token设置独立的过期时间(比如两周=1209600秒),同时可选配置持久化存储(生产环境推荐用数据库存储refresh token,更安全)。

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: 84600 # 主access token有效期1天(保持原配置)
    refresh_token:
        ttl: 1209600 # refresh token有效期两周(60*60*24*14)
        token_provider: lexik_jwt_authentication.refresh_token.provider.in_memory

2. 生产环境配置(数据库存储)

如果要避免内存存储的局限性(比如服务器重启后refresh token丢失),可以用Doctrine存储refresh token:

  1. 先配置bundle:
lexik_jwt_authentication:
    # ... 其他基础配置
    refresh_token:
        ttl: 1209600
        token_provider: lexik_jwt_authentication.refresh_token.provider.doctrine
        entity_manager: default
        entity_class: App\Entity\RefreshToken
  1. 生成RefreshToken实体:
// src/Entity/RefreshToken.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Lexik\Bundle\JWTAuthenticationBundle\Model\RefreshTokenInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Model\RefreshTokenTrait;

#[ORM\Entity]
class RefreshToken implements RefreshTokenInterface
{
    use RefreshTokenTrait;

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private ?int $id = null;

    public function getId(): ?int
    {
        return $this->id;
    }
}
  1. 执行数据库迁移命令创建表:
php bin/console make:migration
php bin/console doctrine:migrations:migrate

二、调整前端Angular逻辑

1. 完善AuthService的token管理

确保刷新token时同时更新本地存储的refresh token(后端刷新接口需要返回新的refresh token):

// auth.service.ts
export class AuthService {
    // ... 其他方法
    setAuthTokens(tokenData: { token: string; refresh_token: string }): void {
        localStorage.setItem('access_token', tokenData.token);
        localStorage.setItem('refresh_token', tokenData.refresh_token);
    }

    getJwtToken(): string | null {
        return localStorage.getItem('access_token');
    }

    getJwtRefreshToken(): string | null {
        return localStorage.getItem('refresh_token');
    }

    clearAuthTokens(): void {
        localStorage.removeItem('access_token');
        localStorage.removeItem('refresh_token');
    }
}

2. 修复TokenInterceptor的刷新逻辑

原拦截器的else分支存在逻辑缺陷(refresh中直接用旧token重试),同时需要处理refresh token过期的情况:

@Injectable()
export class TokenInterceptor implements HttpInterceptor {
    private isRefreshing = false;
    constructor(private _authService: AuthService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const token = this._authService.getJwtToken();
        if (token) {
            request = this.setToken(request, token);
        }

        return next.handle(request).pipe(
            catchError(error => {
                if (error instanceof HttpErrorResponse && error.status === 401) {
                    const refreshToken = this._authService.getJwtRefreshToken();
                    if (refreshToken) {
                        return this.handleAuthorizationError(request, next);
                    } else {
                        // 无refresh token,直接跳登录
                        this._authService.clearAuthTokens();
                        window.location.href = '/login';
                    }
                }
                return throwError(() => error);
            })
        );
    }

    private setToken(request: HttpRequest<any>, token: string): HttpRequest<any> {
        return request.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
    }

    private handleAuthorizationError(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (!this.isRefreshing) {
            this.isRefreshing = true;
            return this._authService.refreshJwtToken().pipe(
                switchMap((tokenData: { token: string; refresh_token: string }) => {
                    this.isRefreshing = false;
                    this._authService.setAuthTokens(tokenData);
                    return next.handle(this.setToken(request, tokenData.token));
                }),
                catchError(() => {
                    this.isRefreshing = false;
                    // refresh token也过期,清token跳登录
                    this._authService.clearAuthTokens();
                    window.location.href = '/login';
                    return throwError(() => new Error('Refresh token expired'));
                })
            );
        } else {
            // 等待当前刷新请求完成,用新token重试
            return new Observable<HttpEvent<any>>(observer => {
                const interval = setInterval(() => {
                    if (!this.isRefreshing) {
                        clearInterval(interval);
                        const newToken = this._authService.getJwtToken();
                        if (newToken) {
                            next.handle(this.setToken(request, newToken)).subscribe(observer);
                        } else {
                            observer.error(new Error('No valid token after refresh'));
                        }
                    }
                }, 100);
            });
        }
    }
}

三、安全注意事项

  • 优先用HttpOnly Cookie存储refresh token:比localStorage更能抵御XSS攻击,Symfony可以在返回refresh token时设置Set-Cookie头,前端请求时开启withCredentials: true。
  • 启用refresh token旋转:每次刷新access token时,后端返回新的refresh token并作废旧的,降低被盗用后的风险。
  • 敏感操作二次验证:对于转账、修改密码等敏感操作,即使token有效,也要求用户输入密码或验证码。

内容的提问来源于stack exchange,提问作者Antoine Despres

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 18:40:38