feat: 初始化 Nest 服务骨架与多平台 Chat SSE 网关
- 新增 NestJS + Fastify 入口、配置模块与 Swagger 集成 - 划分 client-app / admin-app 与 shared-domain ai-gateway - 实现统一 SSE Chat 接口,支持千问、DeepSeek、火山引擎非流式上游与网关分片输出 - 补充项目方案与 JWT 最小实现文档 Made-with: Cursor
This commit is contained in:
232
docs/jwt-minimal-nestjs.md
Normal file
232
docs/jwt-minimal-nestjs.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# 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 隔离版本)。
|
||||
Reference in New Issue
Block a user