NestJS中Passport-JWT策略下刷新令牌的实现方法问询
Got it, let's walk through exactly how to add refresh token support to your existing setup. I'll tie this directly to the JwtStrategy you've already written.
1. First: Store Refresh Tokens Securely
Refresh tokens need to be persisted so we can verify they're valid and haven't been revoked. You'll want a database table/collection (e.g., refresh_tokens) with these fields:
user_id: Foreign key linking to your users tabletoken: The hashed refresh token (use bcrypt to hash it—we don't need to decrypt it, just verify matches later)expires_at: Timestamp for when the token expiresrevoked: Boolean flag to mark tokens as invalid (for logouts or token rotation)
2. Modify Login to Issue Both Tokens
When a user logs in successfully, generate two tokens: a short-lived access token (15-30 mins) and a longer-lived refresh token (7-30 days). Add this logic to your AuthService:
import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import * as fs from 'fs'; @Injectable() export class AuthService { constructor( private jwtService: JwtService, private refreshTokenRepo: RefreshTokenRepository, // Your custom repository for refresh tokens ) {} async login(user: any) { // Match the payload structure your existing JwtStrategy expects const accessPayload = { sub: user.userId, preferred_username: user.username, name: user.name, email: user.email, realm_access: { roles: user.roles }, }; // Generate short-lived access token const accessToken = this.jwtService.sign(accessPayload, { expiresIn: '15m', secret: fs.readFileSync('./src/auth/publicKey.pem'), // Reuse your existing secret setup }); // Generate long-lived refresh token (keep payload minimal for security) const refreshPayload = { sub: user.userId }; const refreshToken = this.jwtService.sign(refreshPayload, { expiresIn: '7d', secret: 'your-unique-refresh-secret', // Use a separate secret from access tokens! }); // Hash and store the refresh token in your DB const hashedRefreshToken = await bcrypt.hash(refreshToken, 10); await this.refreshTokenRepo.create({ userId: user.userId, token: hashedRefreshToken, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), revoked: false, }); // Return tokens (prefer sending refresh token via HttpOnly Cookie for XSS protection) return { access_token: accessToken, // Optional: Use res.cookie('refresh_token', refreshToken, { httpOnly: true, secure: true, sameSite: 'strict' }) }; } // Helper to generate new access tokens during refresh async generateAccessToken(user: any) { const payload = { sub: user.userId, preferred_username: user.username, name: user.name, email: user.email, realm_access: { roles: user.roles }, }; return this.jwtService.sign(payload, { expiresIn: '15m', secret: fs.readFileSync('./src/auth/publicKey.pem'), }); } }
3. Create a Refresh Token Strategy
You'll need a separate Passport strategy to validate refresh tokens. This mirrors your existing JwtStrategy but with refresh-specific config:
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Strategy, ExtractJwt } from 'passport-jwt'; import { AuthService } from './auth.service'; @Injectable() export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { constructor(private authService: AuthService) { super({ // Extract refresh token from request body or Cookie (Cookie is more secure) jwtFromRequest: ExtractJwt.fromBodyField('refresh_token'), // Or use Cookie extraction: ExtractJwt.fromExtractors([(req) => req.cookies.refresh_token]) ignoreExpiration: false, secretOrKey: 'your-unique-refresh-secret', // Match the secret used to sign refresh tokens }); } async validate(payload: any) { // Verify the refresh token exists in DB and isn't revoked/expired const user = await this.authService.validateRefreshToken(payload.sub); if (!user) { throw new UnauthorizedException('Invalid or revoked refresh token'); } return user; // Attach user to the request object for the refresh endpoint } }
Don't forget to register this strategy in your AuthModule:
@Module({ providers: [AuthService, JwtStrategy, JwtRefreshStrategy], // ... other imports/exports }) export class AuthModule {}
4. Build the Refresh Token Endpoint
Create an endpoint that accepts a valid refresh token and returns a new access token (optionally a new refresh token for rolling sessions):
import { Controller, Post, UseGuards, Request } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { AuthService } from './auth.service'; @Controller('auth') export class AuthController { constructor(private authService: AuthService) {} // ... your existing login endpoint @Post('refresh') @UseGuards(AuthGuard('jwt-refresh')) async refresh(@Request() req) { // Generate new access token using the authenticated user const newAccessToken = await this.authService.generateAccessToken(req.user); // Optional: Implement rolling refresh (issue new refresh token, invalidate old one) // const newRefreshToken = await this.authService.generateAndStoreRefreshToken(req.user); return { access_token: newAccessToken, // refresh_token: newRefreshToken // Include if using rolling refresh }; } }
5. Handle Expired Access Tokens in the Client
When your frontend receives a 401 Unauthorized response (from an expired access token):
- Grab the stored refresh token (from HttpOnly Cookie or secure storage)
- Call your
/auth/refreshendpoint - Use the new access token to retry the original request
- If the refresh token is also invalid/expired, redirect the user to login
Key Best Practices
- Use HttpOnly Cookies for Refresh Tokens: Prevents XSS attacks from stealing the token.
- Rolling Refresh Tokens: Issue a new refresh token each time a user refreshes their access token, and invalidate the old one to limit exposure of stolen tokens.
- Hash Refresh Tokens in DB: Never store plaintext refresh tokens—bcrypt hashing ensures even a DB breach won't expose usable tokens.
- Add Token Revocation: Implement a logout endpoint that marks the user's refresh tokens as revoked in the DB.
内容的提问来源于stack exchange,提问作者David Carneros




