🏗️ NestJS JWT 认证架构

核心模块依赖

bash

npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install @types/passport-jwt bcryptjs
npm install @types/bcryptjs

🔐 1. 基础配置和模块设置

JWT 配置模块

typescript

// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { RefreshTokenStrategy } from './strategies/refresh-token.strategy';

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.register({
      secret: process.env.JWT_ACCESS_SECRET,
      signOptions: { 
        expiresIn: process.env.JWT_ACCESS_EXPIRES_IN || '15m' 
      },
    }),
  ],
  providers: [AuthService, JwtStrategy, RefreshTokenStrategy],
  exports: [AuthService, JwtModule],
})
export class AuthModule {}

环境配置

typescript

// src/config/configuration.ts
export default () => ({
  jwt: {
    accessSecret: process.env.JWT_ACCESS_SECRET || 'access_secret',
    accessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN || '15m',
    refreshSecret: process.env.JWT_REFRESH_SECRET || 'refresh_secret',
    refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
  },
});

🎯 2. JWT 策略实现

Access Token 策略

typescript

// src/auth/strategies/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../../users/users.service';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(
    private configService: ConfigService,
    private usersService: UsersService,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromExtractors([
        ExtractJwt.fromAuthHeaderAsBearerToken(),
        ExtractJwt.fromUrlQueryParameter('token'),
      ]),
      ignoreExpiration: false,
      secretOrKey: configService.get('jwt.accessSecret'),
    });
  }

  async validate(payload: any) {
    const user = await this.usersService.findById(payload.sub);
    
    if (!user) {
      throw new UnauthorizedException('User not found');
    }

    if (!user.isActive) {
      throw new UnauthorizedException('User account is inactive');
    }

    return {
      id: user.id,
      email: user.email,
      role: user.role,
    };
  }
}

Refresh Token 策略

typescript

// src/auth/strategies/refresh-token.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../../users/users.service';

@Injectable()
export class RefreshTokenStrategy extends PassportStrategy(
  Strategy,
  'refresh-token',
) {
  constructor(
    private configService: ConfigService,
    private usersService: UsersService,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromBodyField('refreshToken'),
      ignoreExpiration: false,
      secretOrKey: configService.get('jwt.refreshSecret'),
      passReqToCallback: true,
    });
  }

  async validate(req: Request, payload: any) {
    const refreshToken = req.body['refreshToken'];
    const user = await this.usersService.findById(payload.sub);
    
    if (!user) {
      throw new UnauthorizedException('User not found');
    }

    // 验证 refresh token 是否有效
    const isValidRefreshToken = await this.usersService.validateRefreshToken(
      user.id,
      refreshToken,
    );

    if (!isValidRefreshToken) {
      throw new UnauthorizedException('Invalid refresh token');
    }

    return {
      id: user.id,
      email: user.email,
      role: user.role,
      refreshToken,
    };
  }
}

🛠️ 3. Auth Service 实现

typescript

// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcryptjs';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
    private configService: ConfigService,
  ) {}

  async validateUser(email: string, password: string): Promise<any> {
    const user = await this.usersService.findByEmail(email);
    
    if (user && (await bcrypt.compare(password, user.password))) {
      const { password, ...result } = user;
      return result;
    }
    
    return null;
  }

  async login(user: any) {
    const payload = { 
      email: user.email, 
      sub: user.id,
      role: user.role 
    };

    const tokens = await this.generateTokens(payload);
    
    // 保存 refresh token
    await this.usersService.saveRefreshToken(user.id, tokens.refreshToken);

    return {
      accessToken: tokens.accessToken,
      refreshToken: tokens.refreshToken,
      user: {
        id: user.id,
        email: user.email,
        role: user.role,
      },
    };
  }

  async refreshTokens(refreshToken: string, userId: string) {
    // 验证 refresh token 是否有效
    const isValid = await this.usersService.validateRefreshToken(
      userId,
      refreshToken,
    );

    if (!isValid) {
      throw new UnauthorizedException('Invalid refresh token');
    }

    const user = await this.usersService.findById(userId);
    const payload = { 
      email: user.email, 
      sub: user.id,
      role: user.role 
    };

    const tokens = await this.generateTokens(payload);
    
    // 更新 refresh token
    await this.usersService.saveRefreshToken(user.id, tokens.refreshToken);

    return {
      accessToken: tokens.accessToken,
      refreshToken: tokens.refreshToken,
    };
  }

  private async generateTokens(payload: any) {
    const [accessToken, refreshToken] = await Promise.all([
      this.jwtService.signAsync(payload, {
        secret: this.configService.get('jwt.accessSecret'),
        expiresIn: this.configService.get('jwt.accessExpiresIn'),
      }),
      this.jwtService.signAsync(payload, {
        secret: this.configService.get('jwt.refreshSecret'),
        expiresIn: this.configService.get('jwt.refreshExpiresIn'),
      }),
    ]);

    return { accessToken, refreshToken };
  }

  async logout(userId: string) {
    // 移除 refresh token
    await this.usersService.removeRefreshToken(userId);
    return { message: 'Logged out successfully' };
  }
}

🚀 4. Auth Controller

typescript

// src/auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post('login')
  @HttpCode(HttpStatus.OK)
  async login(@Body() loginDto: LoginDto) {
    const user = await this.authService.validateUser(
      loginDto.email,
      loginDto.password,
    );
    
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    return this.authService.login(user);
  }

  @Post('refresh')
  @HttpCode(HttpStatus.OK)
  @UseGuards(AuthGuard('refresh-token'))
  async refreshTokens(@Body() refreshTokenDto: RefreshTokenDto) {
    return this.authService.refreshTokens(
      refreshTokenDto.refreshToken,
      refreshTokenDto.userId,
    );
  }

  @Post('logout')
  @HttpCode(HttpStatus.OK)
  @UseGuards(AuthGuard('jwt'))
  async logout(@Body('userId') userId: string) {
    return this.authService.logout(userId);
  }
}

📝 5. DTO(数据传输对象)

typescript

// src/auth/dto/login.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';

export class LoginDto {
  @IsEmail()
  email: string;

  @IsString()
  @MinLength(6)
  password: string;
}

// src/auth/dto/refresh-token.dto.ts
import { IsJWT, IsString } from 'class-validator';

export class RefreshTokenDto {
  @IsJWT()
  refreshToken: string;

  @IsString()
  userId: string;
}

👤 6. User Service 增强

typescript

// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import * as bcrypt from 'bcryptjs';
import { User, UserDocument } from './schemas/user.schema';

@Injectable()
export class UsersService {
  constructor(
    @InjectModel(User.name) private userModel: Model<UserDocument>,
  ) {}

  async findByEmail(email: string): Promise<UserDocument | null> {
    return this.userModel.findOne({ email }).exec();
  }

  async findById(id: string): Promise<UserDocument | null> {
    return this.userModel.findById(id).exec();
  }

  async saveRefreshToken(userId: string, refreshToken: string): Promise<void> {
    // 哈希存储 refresh token
    const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
    
    await this.userModel.findByIdAndUpdate(userId, {
      refreshToken: hashedRefreshToken,
      refreshTokenExpires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7天
    }).exec();
  }

  async validateRefreshToken(userId: string, refreshToken: string): Promise<boolean> {
    const user = await this.userModel.findById(userId).exec();
    
    if (!user || !user.refreshToken) {
      return false;
    }

    // 检查 token 是否过期
    if (user.refreshTokenExpires < new Date()) {
      return false;
    }

    // 比较 token
    return bcrypt.compare(refreshToken, user.refreshToken);
  }

  async removeRefreshToken(userId: string): Promise<void> {
    await this.userModel.findByIdAndUpdate(userId, {
      refreshToken: null,
      refreshTokenExpires: null,
    }).exec();
  }
}

🛡️ 7. 保护路由和使用守卫

自定义装饰器

typescript

// src/auth/decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

// src/auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

角色守卫

typescript

// src/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    
    if (!requiredRoles) {
      return true;
    }

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.includes(user.role);
  }
}

路由保护示例

typescript

// src/users/users.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';

@Controller('users')
@UseGuards(AuthGuard('jwt'), RolesGuard)
export class UsersController {
  @Get('profile')
  getProfile(@CurrentUser() user: any) {
    return user;
  }

  @Get('admin')
  @Roles('admin')
  adminEndpoint() {
    return { message: 'Admin access granted' };
  }
}

🔒 8. 安全最佳实践

Token 黑名单(可选增强)

typescript

// src/auth/token-blacklist.service.ts
import { Injectable } from '@nestjs/common';
import { RedisService } from '@nestjs-modules/ioredis';

@Injectable()
export class TokenBlacklistService {
  constructor(private readonly redisService: RedisService) {}

  async addToBlacklist(token: string, expiresIn: number): Promise<void> {
    // 将 token 加入黑名单,设置过期时间
    await this.redisService.set(`blacklist:${token}`, '1', 'EX', expiresIn);
  }

  async isBlacklisted(token: string): Promise<boolean> {
    const result = await this.redisService.exists(`blacklist:${token}`);
    return result === 1;
  }
}

增强的 JWT 策略(支持黑名单)

typescript

// 在 JwtStrategy 中添加黑名单检查
async validate(payload: any) {
  // 检查 token 是否在黑名单中
  const isBlacklisted = await this.tokenBlacklistService.isBlacklisted(
    // 需要从请求中获取 token
  );
  
  if (isBlacklisted) {
    throw new UnauthorizedException('Token revoked');
  }

  // 原有的验证逻辑...
}

🎯 完整的工作流程

  1. 用户登录 → 生成 access token 和 refresh token

  2. Access token → 短期有效(15分钟),用于API访问

  3. Refresh token → 长期有效(7天),存储在数据库(哈希后)

  4. Token过期 → 使用 refresh token 获取新的 access token

  5. 用户登出 → 移除 refresh token,可选加入黑名单

客户端处理示例:

javascript

// 前端 token 刷新逻辑
async function refreshAccessToken() {
  try {
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        refreshToken: localStorage.getItem('refreshToken'),
        userId: localStorage.getItem('userId'),
      }),
    });

    if (response.ok) {
      const { accessToken, refreshToken } = await response.json();
      // 保存新的 tokens
      localStorage.setItem('accessToken', accessToken);
      localStorage.setItem('refreshToken', refreshToken);
      return accessToken;
    } else {
      // 刷新失败,需要重新登录
      logout();
    }
  } catch (error) {
    logout();
  }
}

这个实现提供了完整的、生产环境级别的 JWT 身份验证方案,包括安全的 token 刷新机制!

工具如下:

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐