NestJS JWT认证 #

JWT简介 #

JWT(JSON Web Token)是一种开放标准,用于在各方之间安全地传输信息。JWT由三部分组成:Header、Payload和Signature。

text
Header.Payload.Signature

安装依赖 #

bash
npm install @nestjs/jwt passport-jwt
npm install -D @types/passport-jwt

JWT模块配置 #

基本配置 #

typescript
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    JwtModule.register({
      secret: 'YOUR_SECRET_KEY',
      signOptions: { expiresIn: '1h' },
    }),
  ],
})
export class AuthModule {}

异步配置 #

typescript
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'),
        signOptions: {
          expiresIn: configService.get<string>('JWT_EXPIRES_IN', '1h'),
        },
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AuthModule {}

JWT策略 #

创建JWT策略 #

typescript
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get<string>('JWT_SECRET'),
    });
  }

  async validate(payload: JwtPayload) {
    return {
      userId: payload.sub,
      email: payload.email,
      roles: payload.roles,
    };
  }
}

interface JwtPayload {
  sub: number;
  email: string;
  roles: string[];
  iat?: number;
  exp?: number;
}

JWT Guard #

typescript
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

Token生成 #

登录生成Token #

typescript
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';

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

  async login(email: string, password: string) {
    const user = await this.validateUser(email, password);
    
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const payload = {
      sub: user.id,
      email: user.email,
      roles: user.roles,
    };

    return {
      access_token: this.jwtService.sign(payload),
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
      },
    };
  }

  private async validateUser(email: string, password: string) {
    const user = await this.usersService.findByEmail(email);
    if (user && await this.comparePassword(password, user.password)) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}

自定义Payload #

typescript
const payload = {
  sub: user.id,
  email: user.email,
  roles: user.roles,
  permissions: user.permissions,
};

const token = this.jwtService.sign(payload, {
  expiresIn: '15m',
  issuer: 'my-app',
  audience: 'my-app-users',
});

Token验证 #

手动验证 #

typescript
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(private jwtService: JwtService) {}

  async validateToken(token: string) {
    try {
      const payload = this.jwtService.verify(token);
      return payload;
    } catch (error) {
      throw new UnauthorizedException('Invalid token');
    }
  }
}

解码Token #

typescript
const decoded = this.jwtService.decode(token);
console.log(decoded);

刷新Token #

双Token机制 #

typescript
@Injectable()
export class AuthService {
  constructor(
    private jwtService: JwtService,
    private usersService: UsersService,
    private refreshTokenService: RefreshTokenService,
  ) {}

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

    const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' });
    const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });

    await this.refreshTokenService.save(user.id, refreshToken);

    return {
      access_token: accessToken,
      refresh_token: refreshToken,
    };
  }

  async refreshToken(refreshToken: string) {
    const storedToken = await this.refreshTokenService.find(refreshToken);
    
    if (!storedToken) {
      throw new UnauthorizedException('Invalid refresh token');
    }

    try {
      const payload = this.jwtService.verify(refreshToken);
      const user = await this.usersService.findOne(payload.sub);

      const newAccessToken = this.jwtService.sign(
        { sub: user.id, email: user.email },
        { expiresIn: '15m' },
      );

      return { access_token: newAccessToken };
    } catch {
      throw new UnauthorizedException('Invalid refresh token');
    }
  }
}

刷新Token存储 #

typescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RefreshToken } from './entities/refresh-token.entity';

@Injectable()
export class RefreshTokenService {
  constructor(
    @InjectRepository(RefreshToken)
    private tokenRepository: Repository<RefreshToken>,
  ) {}

  async save(userId: number, token: string) {
    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + 7);

    const refreshToken = this.tokenRepository.create({
      userId,
      token,
      expiresAt,
    });

    return this.tokenRepository.save(refreshToken);
  }

  async find(token: string) {
    return this.tokenRepository.findOne({
      where: { token },
    });
  }

  async revoke(token: string) {
    await this.tokenRepository.delete({ token });
  }

  async revokeAll(userId: number) {
    await this.tokenRepository.delete({ userId });
  }
}

Token黑名单 #

typescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BlacklistedToken } from './entities/blacklisted-token.entity';

@Injectable()
export class TokenBlacklistService {
  constructor(
    @InjectRepository(BlacklistedToken)
    private blacklistRepository: Repository<BlacklistedToken>,
  ) {}

  async addToBlacklist(token: string, expiresAt: Date) {
    const blacklisted = this.blacklistRepository.create({
      token,
      expiresAt,
    });
    await this.blacklistRepository.save(blacklisted);
  }

  async isBlacklisted(token: string): Promise<boolean> {
    const count = await this.blacklistRepository.count({
      where: { token },
    });
    return count > 0;
  }

  async cleanupExpired() {
    await this.blacklistRepository
      .createQueryBuilder()
      .delete()
      .where('expiresAt < :now', { now: new Date() })
      .execute();
  }
}

修改JWT Guard:

typescript
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(
    private reflector: Reflector,
    private blacklistService: TokenBlacklistService,
  ) {
    super();
  }

  async canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    const token = this.extractToken(request);

    if (token && await this.blacklistService.isBlacklisted(token)) {
      throw new UnauthorizedException('Token has been revoked');
    }

    return super.canActivate(context) as Promise<boolean>;
  }

  private extractToken(request: Request): string | undefined {
    const authorization = request.headers.authorization;
    if (!authorization) return undefined;
    const [type, token] = authorization.split(' ');
    return type === 'Bearer' ? token : undefined;
  }
}

登出实现 #

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

  @Post('logout')
  @UseGuards(JwtAuthGuard)
  async logout(@Request() req) {
    const token = req.headers.authorization?.split(' ')[1];
    const decoded = this.jwtService.decode(token) as any;
    
    await this.blacklistService.addToBlacklist(
      token,
      new Date(decoded.exp * 1000),
    );

    return { message: 'Successfully logged out' };
  }
}

JWT最佳实践 #

1. 安全存储密钥 #

typescript
// .env
JWT_SECRET=your-super-secret-key-at-least-32-characters-long
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d

2. 使用强密钥 #

typescript
import * as crypto from 'crypto';

const secret = crypto.randomBytes(64).toString('hex');

3. 短过期时间 #

typescript
signOptions: {
  expiresIn: '15m',  // 短过期时间
}

4. HTTPS传输 #

确保生产环境使用HTTPS。

5. 不存储敏感信息 #

typescript
// 不好的做法
const payload = {
  sub: user.id,
  password: user.password,  // 不要存储密码
  creditCard: user.creditCard,  // 不要存储敏感信息
};

// 好的做法
const payload = {
  sub: user.id,
  email: user.email,
};

总结 #

本章学习了NestJS JWT认证:

  • JWT模块配置
  • Token生成和验证
  • 刷新Token机制
  • Token黑名单
  • 登出实现
  • JWT最佳实践

接下来,让我们学习 权限控制

最后更新:2026-03-28