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

233 lines
5.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 隔离版本)。