Files
chat-one-service/docs/jwt-minimal-nestjs.md
alboped 0fa6617341 feat: 初始化 Nest 服务骨架与多平台 Chat SSE 网关
- 新增 NestJS + Fastify 入口、配置模块与 Swagger 集成
- 划分 client-app / admin-app 与 shared-domain ai-gateway
- 实现统一 SSE Chat 接口,支持千问、DeepSeek、火山引擎非流式上游与网关分片输出
- 补充项目方案与 JWT 最小实现文档

Made-with: Cursor
2026-04-17 02:27:08 +08:00

5.8 KiB
Raw Permalink Blame History

NestJS JWT 最小实现(可直接改造)

适用目标:

  • 登录成功签发 accessTokenrefreshToken
  • 受保护接口通过 JwtAuthGuard 鉴权
  • 支持刷新 token

示例使用 NestJS + @nestjs/jwt + passport-jwt,偏最小可用,不含完整业务细节。


1) 安装依赖

npm i @nestjs/jwt @nestjs/passport passport passport-jwt

2) 约定 payload

建议 access token payload 只放必要字段:

type JwtPayload = {
  sub: string;          // 用户ID
  role: 'user' | 'admin';
  type: 'access';
};

refresh token 可增加 jti

type RefreshPayload = {
  sub: string;
  type: 'refresh';
  jti: string;
};

3) auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_ACCESS_SECRET,
      signOptions: { expiresIn: process.env.JWT_ACCESS_EXPIRES_IN || '2h' },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

4) jwt.strategy.ts

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

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_ACCESS_SECRET,
    });
  }

  async validate(payload: { sub: string; role: string; type: string }) {
    if (payload.type !== 'access') {
      throw new UnauthorizedException('Invalid token type');
    }
    return {
      userId: payload.sub,
      role: payload.role,
    };
  }
}

5) jwt-auth.guard.ts

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

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

6) auth.service.ts(登录与刷新核心)

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

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

  // 这里仅示例;真实项目应校验数据库用户和密码/验证码
  async login(user: { id: string; role: 'user' | 'admin' }) {
    const accessToken = await this.signAccessToken(user.id, user.role);
    const refreshToken = await this.signRefreshToken(user.id);
    return { accessToken, refreshToken };
  }

  async refresh(refreshToken: string) {
    let payload: { sub: string; type: string; jti: string };
    try {
      payload = await this.jwtService.verifyAsync(refreshToken, {
        secret: process.env.JWT_REFRESH_SECRET,
      });
    } catch {
      throw new UnauthorizedException('Invalid refresh token');
    }

    if (payload.type !== 'refresh') {
      throw new UnauthorizedException('Invalid token type');
    }

    // 生产建议:校验 jti 是否在黑名单中Redis
    const newAccessToken = await this.signAccessToken(payload.sub, 'user');
    const newRefreshToken = await this.signRefreshToken(payload.sub);
    return { accessToken: newAccessToken, refreshToken: newRefreshToken };
  }

  private signAccessToken(sub: string, role: string) {
    return this.jwtService.signAsync(
      { sub, role, type: 'access' },
      {
        secret: process.env.JWT_ACCESS_SECRET,
        expiresIn: process.env.JWT_ACCESS_EXPIRES_IN || '2h',
        issuer: 'chat-one-client',
        audience: 'chat-one-client-api',
      },
    );
  }

  private signRefreshToken(sub: string) {
    return this.jwtService.signAsync(
      { sub, type: 'refresh', jti: randomUUID() },
      {
        secret: process.env.JWT_REFRESH_SECRET,
        expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d',
        issuer: 'chat-one-client',
        audience: 'chat-one-client-api',
      },
    );
  }
}

7) auth.controller.ts

import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';

@Controller('api/client/v1/auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  async login(@Body() body: { userId: string }) {
    // 示例:实际应先校验短信验证码
    return this.authService.login({ id: body.userId, role: 'user' });
  }

  @Post('refresh')
  async refresh(@Body() body: { refreshToken: string }) {
    return this.authService.refresh(body.refreshToken);
  }

  @UseGuards(JwtAuthGuard)
  @Get('profile')
  async profile(@Req() req: { user: { userId: string; role: string } }) {
    return req.user;
  }
}

8) 双端隔离要点(你项目强烈建议)

  • 客户端和管理端使用不同密钥:
    • CLIENT_JWT_ACCESS_SECRET / CLIENT_JWT_REFRESH_SECRET
    • ADMIN_JWT_ACCESS_SECRET / ADMIN_JWT_REFRESH_SECRET
  • 客户端和管理端使用不同 issueraudience
  • guard 分离:ClientJwtAuthGuardAdminJwtAuthGuard
  • token 绝不互认(即使都是 JWT

9) .env.example 最小补充

JWT_ACCESS_SECRET=replace_me_access
JWT_REFRESH_SECRET=replace_me_refresh
JWT_ACCESS_EXPIRES_IN=2h
JWT_REFRESH_EXPIRES_IN=30d

如果你下一步要,我可以按你现在文档里的目录(apps/client-app + apps/admin-app)直接生成一套可运行的代码骨架(含双端 JWT 隔离版本)。