You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

NestJS中Passport-JWT策略下刷新令牌的实现方法问询

Implementing Refresh Tokens with 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 table
  • token: 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 expires
  • revoked: 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):

  1. Grab the stored refresh token (from HttpOnly Cookie or secure storage)
  2. Call your /auth/refresh endpoint
  3. Use the new access token to retry the original request
  4. 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

火山引擎 最新活动