diff --git a/backend/services/auth-service/src/shared/guards/jwt-auth.guard.ts b/backend/services/auth-service/src/shared/guards/jwt-auth.guard.ts index d9914941..a70bc136 100644 --- a/backend/services/auth-service/src/shared/guards/jwt-auth.guard.ts +++ b/backend/services/auth-service/src/shared/guards/jwt-auth.guard.ts @@ -3,15 +3,22 @@ import { CanActivate, ExecutionContext, UnauthorizedException, + ForbiddenException, + Logger, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; +import { CapabilityService } from '@/application/services/capability.service'; +import { Capability } from '@/domain/value-objects/capability.vo'; @Injectable() export class JwtAuthGuard implements CanActivate { + private readonly logger = new Logger(JwtAuthGuard.name); + constructor( private readonly jwtService: JwtService, private readonly configService: ConfigService, + private readonly capabilityService: CapabilityService, ) {} async canActivate(context: ExecutionContext): Promise { @@ -34,8 +41,29 @@ export class JwtAuthGuard implements CanActivate { source: payload.source, }; + // 检查 LOGIN 能力 — 禁用后立即阻断所有已认证请求(强制下线) + try { + const loginEnabled = await this.capabilityService.isCapabilityEnabled( + payload.sub, + Capability.LOGIN, + ); + if (!loginEnabled) { + throw new ForbiddenException('您的账户已被限制登录,请联系客服'); + } + } catch (error) { + // 如果是我们主动抛出的 ForbiddenException,直接向上传递 + if (error instanceof ForbiddenException) { + throw error; + } + // 能力查询失败时放行(fail-open),避免影响正常用户 + this.logger.warn(`LOGIN 能力检查失败 (${payload.sub}): ${error?.message}`); + } + return true; } catch (error) { + if (error instanceof ForbiddenException) { + throw error; + } throw new UnauthorizedException('访问令牌无效或已过期'); } } diff --git a/frontend/mining-app/lib/core/network/api_client.dart b/frontend/mining-app/lib/core/network/api_client.dart index 98937716..ffd61a8e 100644 --- a/frontend/mining-app/lib/core/network/api_client.dart +++ b/frontend/mining-app/lib/core/network/api_client.dart @@ -12,6 +12,8 @@ class ApiClient { // 全局回调,用于处理 401 跳转登录 static void Function()? onUnauthorized; + // 全局回调,用于处理 403 LOGIN 被禁用(强制下线) + static void Function(String message)? onLoginDisabled; ApiClient({required this.dio}) { dio.options = BaseOptions( @@ -40,6 +42,15 @@ class ApiClient { if (error.response?.statusCode == 401) { onUnauthorized?.call(); } + // 全局处理 403 LOGIN 禁用 — 强制下线 + if (error.response?.statusCode == 403) { + final data = error.response?.data; + final errorObj = data is Map ? data['error'] : null; + final errorMsg = errorObj is Map ? errorObj['message'] : null; + if (errorMsg is String && errorMsg.contains('限制登录')) { + onLoginDisabled?.call(errorMsg); + } + } handler.next(error); }, )); diff --git a/frontend/mining-app/lib/main.dart b/frontend/mining-app/lib/main.dart index ef1f961f..482ed734 100644 --- a/frontend/mining-app/lib/main.dart +++ b/frontend/mining-app/lib/main.dart @@ -38,6 +38,8 @@ class _MiningAppState extends ConsumerState { super.initState(); // 设置 401 全局处理回调 ApiClient.onUnauthorized = _handleUnauthorized; + // 设置 403 LOGIN 禁用回调 — 强制下线 + ApiClient.onLoginDisabled = _handleLoginDisabled; } void _handleUnauthorized() { @@ -48,6 +50,14 @@ class _MiningAppState extends ConsumerState { router.go(Routes.login); } + void _handleLoginDisabled(String message) { + // 清除用户状态(与 401 一样强制下线) + ref.read(userNotifierProvider.notifier).logout(); + // 跳转到登录页 + final router = ref.read(appRouterProvider); + router.go(Routes.login); + } + @override Widget build(BuildContext context) { final router = ref.watch(appRouterProvider);