feat(presence-service): 修复鉴权——用户JWT验证心跳,管理员JWT查询在线/DAU数据

- 添加 @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 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-05 08:52:39 -08:00
parent 5df9c97794
commit e8d9cb72a9
7 changed files with 85 additions and 18 deletions

View File

@ -33,6 +33,7 @@
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@nestjs/cqrs": "^10.2.7", "@nestjs/cqrs": "^10.2.7",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/schedule": "^4.0.0", "@nestjs/schedule": "^4.0.0",
"@nestjs/swagger": "^7.1.17", "@nestjs/swagger": "^7.1.17",
"@prisma/client": "^5.7.0", "@prisma/client": "^5.7.0",

View File

@ -3,6 +3,8 @@ import { ApplicationModule } from '../application/application.module';
import { AnalyticsController } from './controllers/analytics.controller'; import { AnalyticsController } from './controllers/analytics.controller';
import { PresenceController } from './controllers/presence.controller'; import { PresenceController } from './controllers/presence.controller';
import { HealthController } from './controllers/health.controller'; import { HealthController } from './controllers/health.controller';
import { JwtAuthGuard } from '../shared/guards/jwt-auth.guard';
import { AdminGuard } from '../shared/guards/admin.guard';
@Module({ @Module({
imports: [ApplicationModule], imports: [ApplicationModule],
@ -11,5 +13,6 @@ import { HealthController } from './controllers/health.controller';
PresenceController, PresenceController,
HealthController, HealthController,
], ],
providers: [JwtAuthGuard, AdminGuard],
}) })
export class ApiModule {} export class ApiModule {}

View File

@ -7,7 +7,7 @@ import { DauStatsResponseDto } from '../dto/response/dau-stats.dto';
import { RecordEventsCommand } from '../../application/commands/record-events/record-events.command'; import { RecordEventsCommand } from '../../application/commands/record-events/record-events.command';
import { GetDauStatsQuery } from '../../application/queries/get-dau-stats/get-dau-stats.query'; import { GetDauStatsQuery } from '../../application/queries/get-dau-stats/get-dau-stats.query';
import { Public } from '../../shared/decorators/public.decorator'; import { Public } from '../../shared/decorators/public.decorator';
import { JwtAuthGuard } from '../../shared/guards/jwt-auth.guard'; import { AdminGuard } from '../../shared/guards/admin.guard';
@ApiTags('Analytics') @ApiTags('Analytics')
@Controller('analytics') @Controller('analytics')
@ -25,9 +25,9 @@ export class AnalyticsController {
} }
@Get('dau') @Get('dau')
@UseGuards(JwtAuthGuard) @UseGuards(AdminGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: '查询DAU统计' }) @ApiOperation({ summary: '查询DAU统计(管理员 JWT' })
async getDauStats(@Query() dto: QueryDauDto): Promise<DauStatsResponseDto> { async getDauStats(@Query() dto: QueryDauDto): Promise<DauStatsResponseDto> {
return this.queryBus.execute( return this.queryBus.execute(
new GetDauStatsQuery(new Date(dto.startDate), new Date(dto.endDate)), new GetDauStatsQuery(new Date(dto.startDate), new Date(dto.endDate)),

View File

@ -9,6 +9,7 @@ import { RecordHeartbeatCommand } from '../../application/commands/record-heartb
import { GetOnlineCountQuery } from '../../application/queries/get-online-count/get-online-count.query'; 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 { GetOnlineHistoryQuery } from '../../application/queries/get-online-history/get-online-history.query';
import { JwtAuthGuard } from '../../shared/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../shared/guards/jwt-auth.guard';
import { AdminGuard } from '../../shared/guards/admin.guard';
import { CurrentUser } from '../../shared/decorators/current-user.decorator'; import { CurrentUser } from '../../shared/decorators/current-user.decorator';
@ApiTags('Presence') @ApiTags('Presence')
@ -22,7 +23,7 @@ export class PresenceController {
@Post('heartbeat') @Post('heartbeat')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: '心跳上报' }) @ApiOperation({ summary: '心跳上报(用户 JWT' })
async heartbeat( async heartbeat(
@CurrentUser('userId') userId: string, // userSerialNum, e.g. "D25121400005" @CurrentUser('userId') userId: string, // userSerialNum, e.g. "D25121400005"
@Body() dto: HeartbeatDto, @Body() dto: HeartbeatDto,
@ -38,9 +39,9 @@ export class PresenceController {
} }
@Get('online-count') @Get('online-count')
@UseGuards(JwtAuthGuard) @UseGuards(AdminGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: '获取当前在线人数' }) @ApiOperation({ summary: '获取当前在线人数(管理员 JWT' })
async getOnlineCount(): Promise<OnlineCountResponseDto> { async getOnlineCount(): Promise<OnlineCountResponseDto> {
const result = await this.queryBus.execute(new GetOnlineCountQuery()); const result = await this.queryBus.execute(new GetOnlineCountQuery());
return { return {
@ -51,9 +52,9 @@ export class PresenceController {
} }
@Get('online-history') @Get('online-history')
@UseGuards(JwtAuthGuard) @UseGuards(AdminGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: '获取在线人数历史数据' }) @ApiOperation({ summary: '获取在线人数历史数据(管理员 JWT' })
async getOnlineHistory( async getOnlineHistory(
@Query() dto: QueryOnlineHistoryDto, @Query() dto: QueryOnlineHistoryDto,
): Promise<OnlineHistoryResponseDto> { ): Promise<OnlineHistoryResponseDto> {

View File

@ -1,5 +1,6 @@
import { Module } from '@nestjs/common'; 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 { ScheduleModule } from '@nestjs/schedule';
import { ApiModule } from './api/api.module'; import { ApiModule } from './api/api.module';
import { ApplicationModule } from './application/application.module'; import { ApplicationModule } from './application/application.module';
@ -12,6 +13,13 @@ import { InfrastructureModule } from './infrastructure/infrastructure.module';
isGlobal: true, isGlobal: true,
envFilePath: ['.env.local', '.env'], envFilePath: ['.env.local', '.env'],
}), }),
JwtModule.registerAsync({
global: true,
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
}),
}),
ScheduleModule.forRoot(), ScheduleModule.forRoot(),
DomainModule, DomainModule,
InfrastructureModule, InfrastructureModule,

View File

@ -0,0 +1,39 @@
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
/**
* JWT Guard
*
* identity-service JWTtype: 'admin'
* online-countonline-historydau
*/
@Injectable()
export class AdminGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
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;
}
}

View File

@ -1,28 +1,43 @@
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable() @Injectable()
export class JwtAuthGuard implements CanActivate { 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<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [ const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(), context.getHandler(),
context.getClass(), context.getClass(),
]); ]);
if (isPublic) return true;
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
const user = request.user; const token = this.extractTokenFromHeader(request);
if (!token) throw new UnauthorizedException('缺少认证令牌');
if (!user || !user.userId) { try {
throw new UnauthorizedException('未授权访问'); 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; return true;
} }
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
} }