# NestJS JWT 最小实现(可直接改造) 适用目标: - 登录成功签发 `accessToken`、`refreshToken` - 受保护接口通过 `JwtAuthGuard` 鉴权 - 支持刷新 token > 示例使用 NestJS + `@nestjs/jwt` + `passport-jwt`,偏最小可用,不含完整业务细节。 --- ## 1) 安装依赖 ```bash npm i @nestjs/jwt @nestjs/passport passport passport-jwt ``` --- ## 2) 约定 payload 建议 access token payload 只放必要字段: ```ts type JwtPayload = { sub: string; // 用户ID role: 'user' | 'admin'; type: 'access'; }; ``` refresh token 可增加 `jti`: ```ts type RefreshPayload = { sub: string; type: 'refresh'; jti: string; }; ``` --- ## 3) `auth.module.ts` ```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` ```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` ```ts import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') {} ``` --- ## 6) `auth.service.ts`(登录与刷新核心) ```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` ```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` - 客户端和管理端使用不同 `issuer`、`audience` - guard 分离:`ClientJwtAuthGuard` 与 `AdminJwtAuthGuard` - token 绝不互认(即使都是 JWT) --- ## 9) `.env.example` 最小补充 ```env 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 隔离版本)。