nestjs实现 JWT (JSON Web Token) 身份验证
·
🏗️ 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');
}
// 原有的验证逻辑...
}
🎯 完整的工作流程
-
用户登录 → 生成 access token 和 refresh token
-
Access token → 短期有效(15分钟),用于API访问
-
Refresh token → 长期有效(7天),存储在数据库(哈希后)
-
Token过期 → 使用 refresh token 获取新的 access token
-
用户登出 → 移除 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 刷新机制!
工具如下:

更多推荐
所有评论(0)