首页 / NestJS 入门教程 / JWT 认证

NestJS 入门教程

JWT 认证

本教程共 47 篇 · 第 21 篇 · 更新于 2026-08-09 · 约 14 分钟阅读

NestJSJWTToken刷新TokenToken黑名单认证登出

本节目标:掌握 JWT 在 NestJS 中的完整用法,包括 Token 签发与验证、异步配置、刷新 Token 机制、Token 黑名单和登出实现。

JWT 是什么

上一节我们用 Passport 搭了一个基础的 JWT 认证。这一节把 JWT 的细节掰开揉碎讲清楚。

JWT(JSON Web Token)本质上就是一个加密的 JSON 对象。它由三部分组成,用 . 连接:

Header.Payload.Signature
  • Header:声明 Token 类型和签名算法(如 HS256)
  • Payload:存放的数据(比如用户 ID、角色)
  • Signature:用 secret 对前两部分做签名,防止篡改
Note

JWT 的 Payload 部分只是 Base64 编码,不是加密。任何人都能解码看到里面的内容。所以不要往 JWT 里放密码、身份证号等敏感信息

JWT 模块配置

基本配置

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

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

异步配置(推荐)

生产环境中,secret 应该从环境变量读取:

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 {}
Tip

异步配置的好处是 secret 不会硬编码在代码里。配合 .env 文件使用,开发和生产环境可以用不同的密钥。

Token 签发

登录成功后,用 JwtService.sign() 生成 Token:

import { Injectable, UnauthorizedException } 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.usersService.findByEmail(email);

    if (!user) {
      throw new UnauthorizedException('用户不存在');
    }

    const payload = {
      sub: user.id,       // 用户 ID,JWT 标准字段
      email: user.email,
      roles: user.roles,
    };

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

payload 里放什么由你决定,但有几个约定俗成的习惯:

  • sub:存放用户 ID(JWT 标准规定 sub 表示”主题”)
  • 不要放密码、手机号等敏感信息
  • 不要放太多数据,Token 越小越好
Warning

JWT 的 Payload 是明文编码的,任何人都能解码。只放”允许被看到”的信息。

你也可以在签发时覆盖过期时间:

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

Token 验证

通过 Passport 自动验证

上一节讲的 JWT 策略已经帮你做了验证——Passport 自动检查签名和过期时间。

手动验证

有时候你需要手动验证 Token(比如在自定义守卫里):

@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('Token 无效或已过期');
    }
  }
}

verify()decode() 的区别:

  • verify():验证签名 + 检查过期时间,不合法就抛异常
  • decode():只解码,不验证。用于需要读取 Token 内容但不关心合法性的场景
// 只解码,不验证
const decoded = this.jwtService.decode(token);
console.log(decoded); // { sub: 1, email: '...', iat: ..., exp: ... }

刷新 Token 机制

JWT 有一个天然的问题:一旦签发,在过期之前无法作废。如果 Token 被盗,攻击者在过期之前都能用它。

解决方案是”双 Token 机制”:

  • Access Token:短命(15 分钟),用于访问 API
  • Refresh Token:长命(7 天),用于换取新的 Access Token

登录时签发两个 Token

@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',
    });

    // 把 refresh token 存到数据库
    await this.refreshTokenService.save(user.id, refreshToken);

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

刷新接口

客户端的 access_token 过期后,用 refresh_token 来换新的:

async refreshToken(refreshToken: string) {
  // 1. 检查数据库里有没有这个 refresh token
  const storedToken = await this.refreshTokenService.find(refreshToken);
  if (!storedToken) {
    throw new UnauthorizedException('无效的 refresh token');
  }

  // 2. 验证 token 是否过期
  try {
    const payload = this.jwtService.verify(refreshToken);
    const user = await this.usersService.findOne(payload.sub);

    // 3. 签发新的 access token
    const newAccessToken = this.jwtService.sign(
      { sub: user.id, email: user.email },
      { expiresIn: '15m' },
    );

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

Refresh Token 存储

Refresh Token 需要存到数据库里,这样才能支持”撤销”和”登出所有设备”:

@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 });
  }
}
Tip

这里用了 TypeORM 的 Repository 做演示。不管你用什么数据库方案,核心思路一样:存 Token、查 Token、删 Token。

Token 黑名单

有时候你想让某个 Token 提前失效(比如用户修改了密码、或者主动登出)。JWT 本身不支持”作废”,但你可以维护一个黑名单。

@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();
  }
}

然后在守卫里检查黑名单:

@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 = request.headers.authorization?.split(' ')[1];

    if (token && await this.blacklistService.isBlacklisted(token)) {
      throw new UnauthorizedException('Token 已被撤销');
    }

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

黑名单方案在生产环境中通常用 Redis 来实现,性能比数据库好很多。Token 过期后自动从 Redis 中删除,不需要手动清理。

登出实现

有了黑名单机制,登出就是把当前 Token 加进黑名单:

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

  @Post('logout')
  @UseGuards(JwtAuthGuard)
  async logout(@Request() req) {
    const token = req.headers.authorization?.split(' ')[1];
    const decoded = this.jwtService.decode(token) as any;

    // 把 Token 加入黑名单,过期时间跟 Token 一致
    await this.blacklistService.addToBlacklist(
      token,
      new Date(decoded.exp * 1000),
    );

    return { message: '登出成功' };
  }
}
Tip

decoded.exp 是 Unix 时间戳(秒),需要乘以 1000 转成 JavaScript 的毫秒时间戳。

JWT 最佳实践

1. 用环境变量管理密钥

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

生成一个足够长的随机密钥:

import * as crypto from 'crypto';
const secret = crypto.randomBytes(64).toString('hex');

2. Access Token 过期时间要短

15 分钟是比较合理的默认值。配合 Refresh Token 机制,用户体验不会受影响,但安全性大幅提升。

3. 不要在 Payload 里放敏感信息

// 错误做法
const payload = {
  sub: user.id,
  password: user.password,      // 绝对不行
  creditCard: user.creditCard,  // 绝对不行
};

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

4. 生产环境必须用 HTTPS

JWT 在请求头里传输,如果是 HTTP,Token 会被明文截获。

5. 设置全局 JWT 模块

如果你的项目到处都需要 JwtService,可以设置 global: true 避免到处 import:

JwtModule.register({
  global: true,
  secret: jwtConstants.secret,
  signOptions: { expiresIn: '60s' },
}),

小结

JWT 认证的核心流程:

  1. 登录时验证身份,签发 Token
  2. 后续请求携带 Token,服务端验证签名和有效期
  3. Token 快过期时用 Refresh Token 换取新 Token
  4. 需要主动作废时用黑名单机制

关键知识点回顾:

  • JwtModule.register() / registerAsync() 配置签发参数
  • JwtService.sign() 签发 Token,verify() 验证,decode() 只解码
  • Payload 是明文的,不要放敏感信息
  • 双 Token 机制(Access + Refresh)兼顾安全和体验
  • Token 黑名单解决 JWT 无法主动作废的问题
  • 密钥必须用环境变量管理,不能硬编码

下一章我们聊聊权限控制,解决”你能干啥”的问题。