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/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",

View File

@ -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 {}

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 { 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<DauStatsResponseDto> {
return this.queryBus.execute(
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 { 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<OnlineCountResponseDto> {
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<OnlineHistoryResponseDto> {

View File

@ -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<string>('JWT_SECRET'),
}),
}),
ScheduleModule.forRoot(),
DomainModule,
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 { 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<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(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;
}
}