From 97f8b7339f116ea00270fc45a377b938823cb823 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 28 Feb 2026 05:01:37 -0800 Subject: [PATCH] =?UTF-8?q?fix(auth):=20LOGIN=20=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=E7=A6=81=E7=94=A8=E5=90=8E=E5=BC=BA=E5=88=B6=E4=B8=8B=E7=BA=BF?= =?UTF-8?q?=E5=B7=B2=E7=99=BB=E5=BD=95=E7=94=A8=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 问题 管理员在后台禁用用户的 LOGIN 能力后,该用户仍然可以正常使用 mining-app。 原因是 LOGIN 检查只在登录/刷新 token 时执行,已持有有效 JWT(7天有效期) 的用户不会被影响,直到 token 过期才会被拦截。 ## 修复 ### 后端 - JwtAuthGuard (auth-service) - 在 JWT 验证通过后,增加 LOGIN 能力实时检查 - 调用 CapabilityService.isCapabilityEnabled() 查询 Redis 缓存 - LOGIN 被禁用时返回 403 ForbiddenException("您的账户已被限制登录") - 采用 fail-open 策略:Redis/DB 查询失败时放行,不影响正常用户 - 每次认证请求多一次 Redis GET(<1ms),对当前用户规模无性能影响 ### 前端 - mining-app API Client - 新增 onLoginDisabled 全局回调(类似现有的 onUnauthorized) - Dio 拦截器检测 403 响应中包含"限制登录"关键词时触发回调 - 回调执行:清除用户状态 + 跳转登录页(与 401 处理一致) ## 影响范围 - 所有使用 @UseGuards(JwtAuthGuard) 的端点都会实时检查 LOGIN 能力 - 管理员禁用 LOGIN 后,用户下一次 API 请求即被拦截并强制下线 - 不影响公开端点(登录、注册等不经过 JwtAuthGuard 的接口) Co-Authored-By: Claude Opus 4.6 --- .../src/shared/guards/jwt-auth.guard.ts | 28 +++++++++++++++++++ .../lib/core/network/api_client.dart | 11 ++++++++ frontend/mining-app/lib/main.dart | 10 +++++++ 3 files changed, 49 insertions(+) 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);