Skip to content
第 17 章 后端 ⏱ 14 分钟阅读

第 17 章:JWT 认证 ​

学习目标 ​

  • 用 @nestjs/jwt 实现登录
  • 写 JwtStrategy 和 JwtAuthGuard
  • 实现 token 刷新
  • 避开 4 个 JWT 坑

一、什么是 JWT ​

JWT(JSON Web Token)是无状态 token,由三段组成:header.payload.signature,服务器只校验签名不存 session。

二、安装 ​

bash
pnpm add @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
pnpm add -D @types/passport-jwt

三、配置 JwtModule ​

typescript
// app.module.ts
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    JwtModule.registerAsync({
      inject: [ConfigService],
      useFactory: (cfg: ConfigService) => ({
        secret: cfg.get('JWT_SECRET'),
        signOptions: { expiresIn: '1h' },                // 1 小时过期
      }),
    }),
  ],
})
export class AppModule {}

四、登录接口 ​

typescript
// auth/auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';

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

  @Post('login')
  async login(@Body() dto: LoginDto) {
    const user = await this.auth.validate(dto.username, dto.password);
    return this.auth.login(user);                          // 返回 token
  }

  @Post('register')
  register(@Body() dto: RegisterDto) {
    return this.auth.register(dto);
  }
}
typescript
// auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(User) private readonly users: Repository<User>,
    private readonly jwt: JwtService,
  ) {}

  async validate(username: string, password: string) {
    const user = await this.users.findOne({ where: { username } });
    if (!user) throw new UnauthorizedException('用户不存在');
    const ok = await bcrypt.compare(password, user.password);
    if (!ok) throw new UnauthorizedException('密码错误');
    return user;
  }

  async login(user: User) {
    const payload = { sub: user.id, username: user.username };
    return {
      access_token: this.jwt.sign(payload),
      expires_in: 3600,
    };
  }

  async register(dto: RegisterDto) {
    const hash = await bcrypt.hash(dto.password, 10);
    const user = this.users.create({ username: dto.username, password: hash });
    await this.users.save(user);
    return { id: user.id, username: user.username };
  }
}

测试:

bash
curl -X POST http://localhost:3000/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"tom","password":"123456"}'
# {"access_token":"eyJhbGc...","expires_in":3600}

⚠️ 坑 1:密码明文存数据库 → 用 bcrypt.hash(password, 10) 加盐哈希。

五、JwtStrategy ​

typescript
// auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(cfg: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),    // Bearer xxx
      ignoreExpiration: false,
      secretOrKey: cfg.get('JWT_SECRET'),
    });
  }

  async validate(payload: any) {
    // payload = { sub, username }
    return { id: payload.sub, username: payload.username };        // 写入 req.user
  }
}

注册:

typescript
@Module({
  imports: [
    PassportModule,
    JwtModule.registerAsync({ /* ... */ }),
  ],
  providers: [JwtStrategy, AuthService],
})
export class AuthModule {}

六、JwtAuthGuard ​

typescript
// auth/jwt.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

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

挂载:

typescript
@Controller('profile')
@UseGuards(JwtAuthGuard)
export class ProfileController {
  @Get()
  me(@Req() req) { return req.user; }       // { id, username }
}

JwtGuard + JwtStrategy 配合关系:

类职责
JwtStrategy怎么验证(secret、提取 token、validate 函数)
JwtAuthGuard什么时候触发(路由拦截 → 触发 Passport)

靠 'jwt' 字符串关联:

typescript
export class JwtStrategy extends PassportStrategy(Strategy);
//                  ^^^^^^^^
//                                PassportStrategy 内部注册成 'jwt' 名字

export class JwtAuthGuard extends AuthGuard('jwt');
//                                   ^^^^
//                                   字符串 'jwt' 跟 Strategy 对应

完整流程:

GET /api/profile  Authorization: Bearer eyJ...
   ↓
NestJS 拦截 → JwtAuthGuard.canActivate()
   ↓
Guard 调 Passport → 找 'jwt' Strategy → 找到 JwtStrategy
   ↓
JwtStrategy 用 cfg.get('JWT_SECRET') 验证 token
   ↓
┌─ 失败 → 抛 401 Unauthorized
└─ 成功 → 调 validate(payload) → 拿到 { id, username }
   ↓
Passport 写到 req.user
   ↓
Guard 通过 → 到 handler
   ↓
return req.user;  // { id, username }

类比:

角色比喻
JwtStrategy保安培训手册(怎么验证身份证)
JwtAuthGuard门口的保安(执行手册)
'jwt' 字符串手册编号(两个对得上)

记忆口诀:

  • JwtStrategy = 怎么验证(token 提取 + secret + validate)
  • JwtAuthGuard = 什么时候触发(路由拦截)
  • 'jwt' 字符串 = 两者的连接点,必须一致
  • req.user = 来自 validate() 的返回值

测试:

bash
TOKEN="eyJhbGc..."
curl http://localhost:3000/profile -H "Authorization: Bearer $TOKEN"

⚠️ 坑 2:AuthGuard('jwt') 字符串写错 → guard 不生效,接口裸奔。

七、刷新 token ​

typescript
@Post('refresh')
async refresh(@Body('token') token: string) {
  try {
    const payload = await this.jwt.verifyAsync(token, {
      secret: this.cfg.get('JWT_REFRESH_SECRET'),
    });
    return this.auth.login({ id: payload.sub, username: payload.username });
  } catch {
    throw new UnauthorizedException('invalid refresh token');
  }
}

access token 用一个 secret,refresh 用另一个,refresh 有效期长(如 7 天)。

⚠️ 坑 3:access 和 refresh 用同一个 secret → refresh 被吊销时 access 也废。

八、Token 黑名单(强制下线) ​

JWT 无状态,要做主动失效只能维护一个黑名单:

typescript
// 把 token jti 塞 Redis,设过期时间
await this.redis.set(`black:${jti}`, '1', 'EX', 3600);

校验前检查 Redis,命中就拒绝。

九、Passport 模块全局 ​

typescript
import { PassportModule } from '@nestjs/passport';

@Module({
  imports: [PassportModule.register({ session: false })],
})
export class AppModule {}

十、实战:登录 + 受保护接口 ​

typescript
@Controller('users')
export class UsersController {
  @Get()
  list() { return this.svc.findAll(); }                  // 公开

  @Get('me')
  @UseGuards(JwtAuthGuard)
  me(@Req() req) { return req.user; }                    // 需登录
}

⚠️ 坑 4:@UseGuards 写在类级别,公开接口也要走 guard → 用 @Public() 元数据跳过(见第 9 章)。

十一、本章小结 ​

要点关键
模块JwtModule.registerAsync({secret, expiresIn})
策略PassportStrategy(Strategy) + validate
GuardAuthGuard('jwt')
取用户req.user(validate 返回值)
密码bcrypt.hash/compare
安全access + refresh 双 token

动手练习 ​

  1. 登录:写 /auth/login,返回 JWT
  2. 保护:写 /profile,用 JwtAuthGuard 保护
  3. 刷新:写 /auth/refresh,换新 token

下一章:第 18 章:RBAC 权限 →

本站基于 VitePress 构建 · 由 StackHub 团队维护