From e8d9cb72a9d5b2846071fd2df6efed7b4965ce30 Mon Sep 17 00:00:00 2001 From: hailin Date: Thu, 5 Mar 2026 08:52:39 -0800 Subject: [PATCH] =?UTF-8?q?feat(presence-service):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=89=B4=E6=9D=83=E2=80=94=E2=80=94=E7=94=A8=E6=88=B7JWT?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E5=BF=83=E8=B7=B3=EF=BC=8C=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98JWT=E6=9F=A5=E8=AF=A2=E5=9C=A8=E7=BA=BF/DAU=E6=95=B0?= =?UTF-8?q?=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 @nestjs/jwt 依赖,AppModule 注册全局 JwtModule - 重写 JwtAuthGuard:使用 JwtService.verifyAsync 解析用户 token (type=access) - 新建 AdminGuard:验证管理员 token (type=admin),与 identity-service 共享 JWT_SECRET - heartbeat 接口:保持 JwtAuthGuard(用户 JWT) - online-count / online-history / dau:改用 AdminGuard(管理员 JWT) Co-Authored-By: Claude Sonnet 4.6 --- .../services/presence-service/package.json | 1 + .../presence-service/src/api/api.module.ts | 3 ++ .../api/controllers/analytics.controller.ts | 6 +-- .../api/controllers/presence.controller.ts | 11 +++--- .../presence-service/src/app.module.ts | 10 ++++- .../src/shared/guards/admin.guard.ts | 39 +++++++++++++++++++ .../src/shared/guards/jwt-auth.guard.ts | 33 +++++++++++----- 7 files changed, 85 insertions(+), 18 deletions(-) create mode 100644 backend/services/presence-service/src/shared/guards/admin.guard.ts diff --git a/backend/services/presence-service/package.json b/backend/services/presence-service/package.json index d1c6282f..4adbbd58 100644 --- a/backend/services/presence-service/package.json +++ b/backend/services/presence-service/package.json @@ -33,6 +33,7 @@ "@nestjs/core": "^10.0.0", "@nestjs/cqrs": "^10.2.7", "@nestjs/platform-express": "^10.0.0", + "@nestjs/jwt": "^10.2.0", "@nestjs/schedule": "^4.0.0", "@nestjs/swagger": "^7.1.17", "@prisma/client": "^5.7.0", diff --git a/backend/services/presence-service/src/api/api.module.ts b/backend/services/presence-service/src/api/api.module.ts index 96d580db..f3e83bd6 100644 --- a/backend/services/presence-service/src/api/api.module.ts +++ b/backend/services/presence-service/src/api/api.module.ts @@ -3,6 +3,8 @@ import { ApplicationModule } from '../application/application.module'; import { AnalyticsController } from './controllers/analytics.controller'; import { PresenceController } from './controllers/presence.controller'; import { HealthController } from './controllers/health.controller'; +import { JwtAuthGuard } from '../shared/guards/jwt-auth.guard'; +import { AdminGuard } from '../shared/guards/admin.guard'; @Module({ imports: [ApplicationModule], @@ -11,5 +13,6 @@ import { HealthController } from './controllers/health.controller'; PresenceController, HealthController, ], + providers: [JwtAuthGuard, AdminGuard], }) export class ApiModule {} diff --git a/backend/services/presence-service/src/api/controllers/analytics.controller.ts b/backend/services/presence-service/src/api/controllers/analytics.controller.ts index 63225b5d..381d03fa 100644 --- a/backend/services/presence-service/src/api/controllers/analytics.controller.ts +++ b/backend/services/presence-service/src/api/controllers/analytics.controller.ts @@ -7,7 +7,7 @@ import { DauStatsResponseDto } from '../dto/response/dau-stats.dto'; import { RecordEventsCommand } from '../../application/commands/record-events/record-events.command'; import { GetDauStatsQuery } from '../../application/queries/get-dau-stats/get-dau-stats.query'; import { Public } from '../../shared/decorators/public.decorator'; -import { JwtAuthGuard } from '../../shared/guards/jwt-auth.guard'; +import { AdminGuard } from '../../shared/guards/admin.guard'; @ApiTags('Analytics') @Controller('analytics') @@ -25,9 +25,9 @@ export class AnalyticsController { } @Get('dau') - @UseGuards(JwtAuthGuard) + @UseGuards(AdminGuard) @ApiBearerAuth() - @ApiOperation({ summary: '查询DAU统计' }) + @ApiOperation({ summary: '查询DAU统计(管理员 JWT)' }) async getDauStats(@Query() dto: QueryDauDto): Promise { return this.queryBus.execute( new GetDauStatsQuery(new Date(dto.startDate), new Date(dto.endDate)), diff --git a/backend/services/presence-service/src/api/controllers/presence.controller.ts b/backend/services/presence-service/src/api/controllers/presence.controller.ts index 364d3549..640b6b9b 100644 --- a/backend/services/presence-service/src/api/controllers/presence.controller.ts +++ b/backend/services/presence-service/src/api/controllers/presence.controller.ts @@ -9,6 +9,7 @@ import { RecordHeartbeatCommand } from '../../application/commands/record-heartb import { GetOnlineCountQuery } from '../../application/queries/get-online-count/get-online-count.query'; import { GetOnlineHistoryQuery } from '../../application/queries/get-online-history/get-online-history.query'; import { JwtAuthGuard } from '../../shared/guards/jwt-auth.guard'; +import { AdminGuard } from '../../shared/guards/admin.guard'; import { CurrentUser } from '../../shared/decorators/current-user.decorator'; @ApiTags('Presence') @@ -22,7 +23,7 @@ export class PresenceController { @Post('heartbeat') @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOperation({ summary: '心跳上报' }) + @ApiOperation({ summary: '心跳上报(用户 JWT)' }) async heartbeat( @CurrentUser('userId') userId: string, // userSerialNum, e.g. "D25121400005" @Body() dto: HeartbeatDto, @@ -38,9 +39,9 @@ export class PresenceController { } @Get('online-count') - @UseGuards(JwtAuthGuard) + @UseGuards(AdminGuard) @ApiBearerAuth() - @ApiOperation({ summary: '获取当前在线人数' }) + @ApiOperation({ summary: '获取当前在线人数(管理员 JWT)' }) async getOnlineCount(): Promise { const result = await this.queryBus.execute(new GetOnlineCountQuery()); return { @@ -51,9 +52,9 @@ export class PresenceController { } @Get('online-history') - @UseGuards(JwtAuthGuard) + @UseGuards(AdminGuard) @ApiBearerAuth() - @ApiOperation({ summary: '获取在线人数历史数据' }) + @ApiOperation({ summary: '获取在线人数历史数据(管理员 JWT)' }) async getOnlineHistory( @Query() dto: QueryOnlineHistoryDto, ): Promise { diff --git a/backend/services/presence-service/src/app.module.ts b/backend/services/presence-service/src/app.module.ts index 54e22dbb..bb37a216 100644 --- a/backend/services/presence-service/src/app.module.ts +++ b/backend/services/presence-service/src/app.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; import { ScheduleModule } from '@nestjs/schedule'; import { ApiModule } from './api/api.module'; import { ApplicationModule } from './application/application.module'; @@ -12,6 +13,13 @@ import { InfrastructureModule } from './infrastructure/infrastructure.module'; isGlobal: true, envFilePath: ['.env.local', '.env'], }), + JwtModule.registerAsync({ + global: true, + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET'), + }), + }), ScheduleModule.forRoot(), DomainModule, InfrastructureModule, diff --git a/backend/services/presence-service/src/shared/guards/admin.guard.ts b/backend/services/presence-service/src/shared/guards/admin.guard.ts new file mode 100644 index 00000000..ba87c921 --- /dev/null +++ b/backend/services/presence-service/src/shared/guards/admin.guard.ts @@ -0,0 +1,39 @@ +import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; + +/** + * 管理员 JWT Guard + * + * 验证 identity-service 管理员登录颁发的 JWT(type: 'admin')。 + * 用于保护 online-count、online-history、dau 等聚合查询接口。 + */ +@Injectable() +export class AdminGuard implements CanActivate { + constructor(private readonly jwtService: JwtService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + if (!token) throw new UnauthorizedException('缺少认证令牌'); + + try { + const payload = await this.jwtService.verifyAsync(token); + if (payload.type !== 'admin') throw new UnauthorizedException('需要管理员权限'); + request.user = { + adminId: payload.sub, + email: payload.email, + role: payload.role, + type: 'admin', + }; + } catch { + throw new UnauthorizedException('令牌无效或已过期'); + } + + return true; + } + + private extractTokenFromHeader(request: any): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} diff --git a/backend/services/presence-service/src/shared/guards/jwt-auth.guard.ts b/backend/services/presence-service/src/shared/guards/jwt-auth.guard.ts index 069dd024..761cdfb7 100644 --- a/backend/services/presence-service/src/shared/guards/jwt-auth.guard.ts +++ b/backend/services/presence-service/src/shared/guards/jwt-auth.guard.ts @@ -1,28 +1,43 @@ import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; +import { JwtService } from '@nestjs/jwt'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; @Injectable() export class JwtAuthGuard implements CanActivate { - constructor(private reflector: Reflector) {} + constructor( + private readonly jwtService: JwtService, + private readonly reflector: Reflector, + ) {} - canActivate(context: ExecutionContext): boolean { + async canActivate(context: ExecutionContext): Promise { const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ context.getHandler(), context.getClass(), ]); - - if (isPublic) { - return true; - } + if (isPublic) return true; const request = context.switchToHttp().getRequest(); - const user = request.user; + const token = this.extractTokenFromHeader(request); + if (!token) throw new UnauthorizedException('缺少认证令牌'); - if (!user || !user.userId) { - throw new UnauthorizedException('未授权访问'); + try { + const payload = await this.jwtService.verifyAsync(token); + if (payload.type !== 'access') throw new UnauthorizedException('无效的令牌类型'); + request.user = { + userId: payload.userId, + accountSequence: payload.accountSequence, + deviceId: payload.deviceId, + }; + } catch { + throw new UnauthorizedException('令牌无效或已过期'); } return true; } + + private extractTokenFromHeader(request: any): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } }