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:
parent
5df9c97794
commit
e8d9cb72a9
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue