From f0c4e810cbaefc074996887fedbdafc62e529a06 Mon Sep 17 00:00:00 2001 From: zizi Date: Wed, 10 Jun 2026 10:31:42 +0000 Subject: [PATCH] =?UTF-8?q?fix(passport):=20e2e=E5=8F=8CP0=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E2=80=94=E2=80=94=E5=8C=BF=E5=90=8D=E5=86=99=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=AE=A1=E8=AE=A1=E5=88=97=E5=85=9C=E5=BA=95=C3=975?= =?UTF-8?q?=E5=86=99=E7=82=B9=20+=20=E6=89=8B=E6=9C=BA=E5=8F=B7/=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=A0=81=E6=98=8E=E6=96=87=E6=97=A5=E5=BF=97=E4=B8=A4?= =?UTF-8?q?=E9=9D=A2=E5=90=88=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P0-1: @PermitAll匿名无登录态→DefaultDBFieldHandler不填creator/updater撞NOT NULL(V11漏继承审计列,与M-b执行器updater雷同根)。修=Service写点显式兜底"0"(注册/会话开局/会话收尾UPDATE形态/遥测batch+beacon共点/ad计费台账),归属维度user_id/anon_id/creator_user_id不受影响 - P0-2: ApiAccessLogInterceptor非prod无条件打印原始请求体(不读@ApiAccessLog注解,framework不可改)→staging抬该logger至WARN关控制台面+三端点sanitizeKeys脱敏持久化面;SmsSendConsumer脱敏mobile、code不打印 - 单测: 4模块匿名落库creator='0'断言+登录态不强置负向对照 - e2e第一轮判定: E3/E8/E12/UI过门;E2/E4/E6/E7/E1/E13被P0-1阻断待二轮重跑 Co-Authored-By: Claude Opus 4.8 --- .../service/revenue/AdRevenueServiceImpl.java | 8 ++++ .../revenue/AdRevenueServiceImplTest.java | 4 ++ .../session/RuntimeSessionServiceImpl.java | 15 +++++++ .../RuntimeSessionServiceImplTest.java | 41 +++++++++++++++++++ .../service/event/EventIngestServiceImpl.java | 9 ++++ .../event/EventIngestServiceImplTest.java | 35 ++++++++++++++++ .../app/passport/AppPassportController.java | 8 ++++ .../mq/consumer/sms/SmsSendConsumer.java | 9 +++- .../service/passport/PassportServiceImpl.java | 6 +++ .../passport/PassportServiceImplTest.java | 6 +++ .../main/resources/application-staging.yaml | 5 +++ 11 files changed, 145 insertions(+), 1 deletion(-) diff --git a/game-cloud/game-module-ad/game-module-ad-server/src/main/java/cn/wanxiang/game/module/ad/service/revenue/AdRevenueServiceImpl.java b/game-cloud/game-module-ad/game-module-ad-server/src/main/java/cn/wanxiang/game/module/ad/service/revenue/AdRevenueServiceImpl.java index ddbb41b8..def73ae8 100644 --- a/game-cloud/game-module-ad/game-module-ad-server/src/main/java/cn/wanxiang/game/module/ad/service/revenue/AdRevenueServiceImpl.java +++ b/game-cloud/game-module-ad/game-module-ad-server/src/main/java/cn/wanxiang/game/module/ad/service/revenue/AdRevenueServiceImpl.java @@ -156,6 +156,14 @@ public class AdRevenueServiceImpl implements AdRevenueService { revenueDO.setTraceId(traceId); revenueDO.setSettleStatus(AdSettleStatusEnum.UNSETTLED.getStatus()); revenueDO.setStatDate(LocalDate.now()); + // 2026-06-10 鉴权波 e2e 静态定论(P0-1):本波两计费端点(report/impression、report/reward)已 @PermitAll, + // 计费按 gameId 反查作者归因、不依赖调用方身份;匿名调用时 handler 无登录态可填(撞 NOT NULL), + // 带 token 调用(如 test1 批跑)虽可被 handler 填登录 id,但台账审计列统一记 "0"=系统计费写入口径,避免匿名/登录两态值漂移。 + // DefaultDBFieldHandler.insertFill 仅在 getLoginUserId()!=null 时填 creator/updater + // (见 yudao-spring-boot-starter-mybatis .../handler/DefaultDBFieldHandler.java:38-43),匿名两列保持 null 撞 NOT NULL → 500。 + // 故无条件显式兜底 "0"(与系统身份 id=0 同口径,显式值优先于 handler)。收入归属另由 creator_user_id 列承载,不受影响。 + revenueDO.setCreator("0"); + revenueDO.setUpdater("0"); try { adRevenueMapper.insert(revenueDO); return new AdBillingResult(revenue, true); diff --git a/game-cloud/game-module-ad/game-module-ad-server/src/test/java/cn/wanxiang/game/module/ad/service/revenue/AdRevenueServiceImplTest.java b/game-cloud/game-module-ad/game-module-ad-server/src/test/java/cn/wanxiang/game/module/ad/service/revenue/AdRevenueServiceImplTest.java index d11895e2..ab3dec63 100644 --- a/game-cloud/game-module-ad/game-module-ad-server/src/test/java/cn/wanxiang/game/module/ad/service/revenue/AdRevenueServiceImplTest.java +++ b/game-cloud/game-module-ad/game-module-ad-server/src/test/java/cn/wanxiang/game/module/ad/service/revenue/AdRevenueServiceImplTest.java @@ -144,6 +144,10 @@ class AdRevenueServiceImplTest extends BaseMockitoUnitTest { verify(adRevenueMapper).insert(captor.capture()); assertEquals(AdEventTypeEnum.REWARD.getType(), captor.getValue().getEventType()); assertEquals(5L, captor.getValue().getRevenueAmount()); + // 2026-06-10 鉴权波 e2e 静态定论(P0-1):计费端点已 @PermitAll、bill() 无 userId 入参 → 落台账必无登录态, + // 显式兜底审计列 creator/updater="0",规避 DefaultDBFieldHandler 不填撞 NOT NULL 的 500 + assertEquals("0", captor.getValue().getCreator(), "匿名计费落台账 creator 应兜底 0"); + assertEquals("0", captor.getValue().getUpdater(), "匿名计费落台账 updater 应兜底 0"); } // ============================== 拒计:停用 / 归因失败 ============================== diff --git a/game-cloud/game-module-runtime/game-module-runtime-server/src/main/java/cn/wanxiang/game/module/runtime/service/session/RuntimeSessionServiceImpl.java b/game-cloud/game-module-runtime/game-module-runtime-server/src/main/java/cn/wanxiang/game/module/runtime/service/session/RuntimeSessionServiceImpl.java index 0d6855fb..1fb05f47 100644 --- a/game-cloud/game-module-runtime/game-module-runtime-server/src/main/java/cn/wanxiang/game/module/runtime/service/session/RuntimeSessionServiceImpl.java +++ b/game-cloud/game-module-runtime/game-module-runtime-server/src/main/java/cn/wanxiang/game/module/runtime/service/session/RuntimeSessionServiceImpl.java @@ -59,6 +59,14 @@ public class RuntimeSessionServiceImpl implements RuntimeSessionService { session.setDurationMs(0L); session.setTraceId(reqVO.getTraceId()); session.setStartTime(LocalDateTime.now()); // 服务端记开局时间(时长以服务端 end-start 为准) + // 2026-06-10 鉴权波 e2e 实证(P0-1):匿名试玩 @PermitAll 无登录态,DefaultDBFieldHandler.insertFill 仅在 + // getLoginUserId()!=null 时填 creator/updater(见 yudao-spring-boot-starter-mybatis .../handler/DefaultDBFieldHandler.java:38-43), + // 匿名(userId==null)两列保持 null 撞 NOT NULL → SQLIntegrityConstraintViolationException 500。 + // 故此处显式兜底 "0"(与 M-b 执行器系统身份 LoginUser id=0 同口径);显式值优先于 handler(handler 仅 isNull 才填,不覆盖)。 + if (userId == null) { + session.setCreator("0"); + session.setUpdater("0"); + } runtimeSessionMapper.insert(session); // 关键链路日志:含归属维度(userId 可空=匿名,anonId 归属),便于匿名/登录会话排障与对账 log.info("[startSession] 新建试玩会话 sessionId={}, gameId={}, versionId={}, scene={}, userId={}, anonId={}", @@ -86,6 +94,13 @@ public class RuntimeSessionServiceImpl implements RuntimeSessionService { update.setDurationMs(durationMs); update.setEndReason(StringUtils.hasText(reqVO.getEndReason()) ? reqVO.getEndReason() : "game_end"); update.setEndTime(endTime); + // 2026-06-10 鉴权波 e2e 实证(P0-1·M-b 雷=UPDATE SET updater=null 形态):匿名收尾 @PermitAll 无登录态, + // DefaultDBFieldHandler.updateFill 仅在 getLoginUserId()!=null 时填 updater(见 .../handler/DefaultDBFieldHandler.java:57-61), + // 匿名(当前无登录态 userId==null 或会话本就为匿名归属 player_user_id 为空)时 updater 保持 null 撞 NOT NULL → 500。 + // 故显式兜底 updater="0"(与开局/系统身份同口径);显式值优先于 handler(仅 isNull 才填,不覆盖)。 + if (userId == null || session.getPlayerUserId() == null) { + update.setUpdater("0"); + } runtimeSessionMapper.updateById(update); // TODO 对接点【telemetry 回灌】:会话收尾后投递 play_end 事件至 telemetry(契约 #5 events.schema.json)。 // 约定 sessionId(int64) 透传进 telemetry envelope.session_id(转 string)作对账键; diff --git a/game-cloud/game-module-runtime/game-module-runtime-server/src/test/java/cn/wanxiang/game/module/runtime/service/session/RuntimeSessionServiceImplTest.java b/game-cloud/game-module-runtime/game-module-runtime-server/src/test/java/cn/wanxiang/game/module/runtime/service/session/RuntimeSessionServiceImplTest.java index 1d0cc7e4..53917936 100644 --- a/game-cloud/game-module-runtime/game-module-runtime-server/src/test/java/cn/wanxiang/game/module/runtime/service/session/RuntimeSessionServiceImplTest.java +++ b/game-cloud/game-module-runtime/game-module-runtime-server/src/test/java/cn/wanxiang/game/module/runtime/service/session/RuntimeSessionServiceImplTest.java @@ -90,6 +90,24 @@ class RuntimeSessionServiceImplTest extends BaseMockitoUnitTest { assertNull(saved.getPlayerUserId(), "匿名会话 player_user_id 应为 NULL"); assertEquals("anon-abc", saved.getAnonId(), "匿名会话应落 anonId 归属"); assertEquals(SessionStatusEnum.PLAYING.getStatus(), saved.getStatus()); + // 2026-06-10 鉴权波 e2e 实证(P0-1):匿名开局显式兜底审计列 creator/updater="0",规避 DefaultDBFieldHandler 不填撞 NOT NULL 的 500 + assertEquals("0", saved.getCreator(), "匿名开局 creator 应兜底 0"); + assertEquals("0", saved.getUpdater(), "匿名开局 updater 应兜底 0"); + } + + @Test + void testStartSession_loggedIn_creatorNotForced() { + // 登录态开局(userId 非空):不显式兜底审计列,交由 DefaultDBFieldHandler 按登录身份填充(creator/updater 保持 null 等 handler 填) + when(runtimeSessionMapper.selectByClientPlayToken("ptk_login")).thenReturn(null); + when(runtimeSessionMapper.insert(any(RuntimeSessionDO.class))).thenReturn(1); + + runtimeSessionService.startSession(startReq("ptk_login", "play"), 99L); // 登录态 + + ArgumentCaptor captor = ArgumentCaptor.forClass(RuntimeSessionDO.class); + verify(runtimeSessionMapper).insert(captor.capture()); + RuntimeSessionDO saved = captor.getValue(); + assertNull(saved.getCreator(), "登录态开局不应显式兜底 creator(交 handler 按登录身份填)"); + assertNull(saved.getUpdater(), "登录态开局不应显式兜底 updater(交 handler 按登录身份填)"); } @Test @@ -127,6 +145,7 @@ class RuntimeSessionServiceImplTest extends BaseMockitoUnitTest { session.setId(888L); session.setStatus(SessionStatusEnum.PLAYING.getStatus()); session.setStartTime(LocalDateTime.now().minusMinutes(2)); + session.setPlayerUserId(99L); // 登录态会话(明确非匿名,避免触发匿名 updater 兜底分支) when(runtimeSessionMapper.selectById(888L)).thenReturn(session); SessionEndReqVO reqVO = new SessionEndReqVO(); @@ -142,6 +161,28 @@ class RuntimeSessionServiceImplTest extends BaseMockitoUnitTest { assertTrue(update.getDurationMs() >= 119000L, "服务端时长应约 120000ms,实际=" + update.getDurationMs()); assertEquals("game_end", update.getEndReason()); assertNotNull(update.getEndTime()); + assertNull(update.getUpdater(), "登录态收尾不应显式兜底 updater(交 handler 按登录身份填)"); + } + + @Test + void testEndSession_anonymous_updaterFallbackZero() { + // 2026-06-10 鉴权波 e2e 实证(P0-1·M-b 雷=UPDATE SET updater=null):匿名收尾(当前无登录态 userId=null) + // 显式兜底 update.updater="0",规避 DefaultDBFieldHandler.updateFill 不填 updater 撞 NOT NULL 的 500。 + RuntimeSessionDO session = new RuntimeSessionDO(); + session.setId(889L); + session.setStatus(SessionStatusEnum.PLAYING.getStatus()); + session.setStartTime(LocalDateTime.now().minusMinutes(1)); + session.setPlayerUserId(null); // 匿名会话归属 + when(runtimeSessionMapper.selectById(889L)).thenReturn(session); + + SessionEndReqVO reqVO = new SessionEndReqVO(); + reqVO.setSessionId(889L); + reqVO.setEndReason("game_end"); + runtimeSessionService.endSession(reqVO, null); // 匿名收尾 + + ArgumentCaptor captor = ArgumentCaptor.forClass(RuntimeSessionDO.class); + verify(runtimeSessionMapper).updateById(captor.capture()); + assertEquals("0", captor.getValue().getUpdater(), "匿名收尾 updater 应兜底 0"); } @Test diff --git a/game-cloud/game-module-telemetry/game-module-telemetry-server/src/main/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImpl.java b/game-cloud/game-module-telemetry/game-module-telemetry-server/src/main/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImpl.java index 99a57d7a..85d06ab1 100644 --- a/game-cloud/game-module-telemetry/game-module-telemetry-server/src/main/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImpl.java +++ b/game-cloud/game-module-telemetry/game-module-telemetry-server/src/main/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImpl.java @@ -334,6 +334,15 @@ public class EventIngestServiceImpl implements EventIngestService { // props 为 null 时透传 null(列可空);空对象序列化为 "{}"(合法 JSON)。 eventDO.setProps(envelope.getProps() == null ? null : JsonUtils.toJsonString(envelope.getProps())); eventDO.setProcessStatus(EventProcessStatusEnum.PENDING.getStatus()); + // 2026-06-10 鉴权波 e2e 实证(P0-1):匿名上报(batch/beacon 两端点 @PermitAll 共用本点)无登录态, + // DefaultDBFieldHandler.insertFill 仅在 getLoginUserId()!=null 时填 creator/updater + // (见 yudao-spring-boot-starter-mybatis .../handler/DefaultDBFieldHandler.java:38-43),匿名两列保持 null 撞 NOT NULL → 500。 + // 归属维度已用 user_id/anon_id 承载,审计列匿名时显式兜底 "0"(与系统身份 id=0 同口径,显式值优先于 handler)。 + // 注:后续 markAggregated 的 updateById 仅更新 process_status,其 updater=null 不进 UPDATE SET(MP 跳过 null 字段),不覆盖本行 "0"。 + if (eventDO.getUserId() == null) { + eventDO.setCreator("0"); + eventDO.setUpdater("0"); + } return eventDO; } diff --git a/game-cloud/game-module-telemetry/game-module-telemetry-server/src/test/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImplTest.java b/game-cloud/game-module-telemetry/game-module-telemetry-server/src/test/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImplTest.java index fcf421d5..da6ca626 100644 --- a/game-cloud/game-module-telemetry/game-module-telemetry-server/src/test/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImplTest.java +++ b/game-cloud/game-module-telemetry/game-module-telemetry-server/src/test/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImplTest.java @@ -313,6 +313,41 @@ class EventIngestServiceImplTest extends BaseMockitoUnitTest { verifyNoInteractions(feedApi); } + // ============================== 匿名信封审计列兜底(2026-06-10 鉴权波 P0-1)============================== + + @Test + void testIngestOne_anonymousEnvelope_creatorUpdaterFallbackZero() { + // 2026-06-10 鉴权波 e2e 实证(P0-1):匿名上报(信封无 user.userId 且当前无登录态 userId=null)→ 解析 user_id=null, + // 显式兜底审计列 creator/updater="0",规避 DefaultDBFieldHandler.insertFill 不填撞 NOT NULL 的 500(batch/beacon 共用此点)。 + when(telemetryEventMapper.selectByEventId(anyString())).thenReturn(null); + EnvelopeReqVO env = validEnvelope("game_play_start", "t-anon"); // 无 user 段 → 匿名 + + eventIngestService.ingestBatch(batchOf(env), null); // userId=null=匿名 + + ArgumentCaptor captor = ArgumentCaptor.forClass(TelemetryEventDO.class); + verify(telemetryEventMapper).insert(captor.capture()); + TelemetryEventDO saved = captor.getValue(); + assertNull(saved.getUserId(), "匿名信封 user_id 应为 NULL"); + assertEquals("0", saved.getCreator(), "匿名上报 creator 应兜底 0"); + assertEquals("0", saved.getUpdater(), "匿名上报 updater 应兜底 0"); + } + + @Test + void testIngestOne_loggedInEnvelope_creatorNotForced() { + // 登录态上报(当前登录 userId=99):不显式兜底审计列,交由 DefaultDBFieldHandler 按登录身份填充(保持 null 等 handler 填) + when(telemetryEventMapper.selectByEventId(anyString())).thenReturn(null); + EnvelopeReqVO env = validEnvelope("game_play_start", "t-login"); + + eventIngestService.ingestBatch(batchOf(env), 99L); // 登录态 + + ArgumentCaptor captor = ArgumentCaptor.forClass(TelemetryEventDO.class); + verify(telemetryEventMapper).insert(captor.capture()); + TelemetryEventDO saved = captor.getValue(); + assertEquals(99L, saved.getUserId()); + assertNull(saved.getCreator(), "登录态上报不应显式兜底 creator(交 handler 按登录身份填)"); + assertNull(saved.getUpdater(), "登录态上报不应显式兜底 updater(交 handler 按登录身份填)"); + } + // ============================== 测试夹具 ============================== /** 构造一条信封必填项齐全的事件(含 eventId;无 context,不触发聚合) */ diff --git a/game-cloud/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/passport/AppPassportController.java b/game-cloud/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/passport/AppPassportController.java index 53edc929..20f2539c 100644 --- a/game-cloud/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/passport/AppPassportController.java +++ b/game-cloud/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/passport/AppPassportController.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.system.controller.app.passport; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter; @@ -48,6 +49,9 @@ public class AppPassportController { @PostMapping("/send-sms-code") @PermitAll // 免登:种子期 Debug 渠道(码落 system_sms_code 表,运营后台查码) + // 2026-06-10 鉴权波 E11 安全实证(响应安全基线 §5.1):脱敏持久化访问日志(system_api_access_log)的 mobile 键,禁明文落库。 + // 注:sanitizeKeys 仅由 ApiAccessLogFilter 消费(持久化日志),不影响 ApiAccessLogInterceptor 的控制台明文(框架层不可改,控制台面已由 staging logging.level 关闭,见 application-staging.yaml)。 + @ApiAccessLog(sanitizeKeys = {"mobile"}) @Operation(summary = "发送登录短信验证码", description = "场景固定 MEMBER_LOGIN;响应不区分手机号是否已注册(防枚举)") // R6 安全前置(§7.2):IP 维度限频 10 次/小时/IP,配合 Service 层 IP 每日/每小时 Redis 计数双闸;切真实渠道前另补图形验证码 @RateLimiter(time = 1, timeUnit = TimeUnit.HOURS, count = SMS_RATE_LIMIT_COUNT, @@ -60,6 +64,8 @@ public class AppPassportController { @PostMapping("/sms-login") @PermitAll // 免登:已注册登录 / 未注册自动注册一体(验证码即占有证明,拍板3) + // 2026-06-10 鉴权波 E11 安全实证(§5.1):脱敏持久化访问日志的 mobile/code 键(手机号+验证码禁明文落库;同口径见 send-sms-code 注释)。 + @ApiAccessLog(sanitizeKeys = {"mobile", "code"}) @Operation(summary = "手机号验证码登录", description = "已注册→登录;未注册→自动注册一体(register_channel=sms)") public CommonResult smsLogin(@Valid @RequestBody SmsLoginReqVO reqVO) { return success(passportService.smsLogin(reqVO, ServletUtils.getClientIP())); @@ -67,6 +73,8 @@ public class AppPassportController { @PostMapping("/invite-register") @PermitAll // 免登:报备前陌生玩家旁路(受退役开关管控,§5.4) + // 2026-06-10 鉴权波 E11 安全实证(§5.1):脱敏持久化访问日志的 mobile/inviteCode 键(手机号+邀请码禁明文落库;同口径见 send-sms-code 注释)。 + @ApiAccessLog(sanitizeKeys = {"mobile", "inviteCode"}) @Operation(summary = "邀请码受限激活注册", description = "手机号+邀请码,零短信依赖;受退役开关 wanxiang.passport.invite-register-enabled 管控") public CommonResult inviteRegister(@Valid @RequestBody InviteRegisterReqVO reqVO) { return success(passportService.inviteRegister(reqVO, ServletUtils.getClientIP())); diff --git a/game-cloud/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sms/SmsSendConsumer.java b/game-cloud/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sms/SmsSendConsumer.java index f1d9602a..f18551c7 100644 --- a/game-cloud/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sms/SmsSendConsumer.java +++ b/game-cloud/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/sms/SmsSendConsumer.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.system.mq.consumer.sms; +import cn.hutool.core.util.DesensitizedUtil; import cn.iocoder.yudao.module.system.mq.message.sms.SmsSendMessage; import cn.iocoder.yudao.module.system.service.sms.SmsSendService; import lombok.extern.slf4j.Slf4j; @@ -24,7 +25,13 @@ public class SmsSendConsumer { @EventListener @Async // Spring Event 默认在 Producer 发送的线程,通过 @Async 实现异步 public void onMessage(SmsSendMessage message) { - log.info("[onMessage][消息内容({})]", message); + // 2026-06-10 鉴权波 E11 安全实证(响应安全基线 §5.1):原 log.info("[消息内容({})]", message) 经 SmsSendMessage.toString() + // 会打印 mobile 明文 + templateParams(含登录验证码 code)明文,造成手机号/验证码击穿。 + // 改为脱敏输出:mobile 走 DesensitizedUtil.mobilePhone;code 一律不打印(templateParams 整体不落日志); + // 仅保留 logId/channelId/apiTemplateId 供排障与对账(关联 system_sms_code / system_sms_log 表)。 + log.info("[onMessage][短信发送 logId({}) mobile({}) channelId({}) apiTemplateId({})]", + message.getLogId(), DesensitizedUtil.mobilePhone(message.getMobile()), + message.getChannelId(), message.getApiTemplateId()); smsSendService.doSendSms(message); } diff --git a/game-cloud/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/passport/PassportServiceImpl.java b/game-cloud/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/passport/PassportServiceImpl.java index 94dae06b..7e6eb60e 100644 --- a/game-cloud/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/passport/PassportServiceImpl.java +++ b/game-cloud/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/passport/PassportServiceImpl.java @@ -183,6 +183,12 @@ public class PassportServiceImpl implements PassportService { // 目标3:anonId 仅注册时落一次;缺失落空串(归因降级不阻断登录,§12) player.setFirstAnonId(StringUtils.hasText(anonId) ? anonId : ""); player.setLoginDate(LocalDateTime.now()); + // 2026-06-10 鉴权波 e2e 实证(P0-1):免登注册(sms-login 自动注册 / invite-register 共用此点)@PermitAll 无登录态, + // 且注册者本人 id 此刻尚未生成;DefaultDBFieldHandler.insertFill 仅在 getLoginUserId()!=null 时填 creator/updater + // (见 yudao-spring-boot-starter-mybatis .../handler/DefaultDBFieldHandler.java:38-43),匿名两列保持 null 撞 NOT NULL → 500。 + // 故无条件显式兜底 "0"(与系统身份 id=0 同口径);显式值优先于 handler(仅 isNull 才填,不覆盖)。 + player.setCreator("0"); + player.setUpdater("0"); try { playerMapper.insert(player); log.info("[registerPlayer] 新玩家注册成功 userId={}, channel={}, mobile={}, hasAnonId={}", diff --git a/game-cloud/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/passport/PassportServiceImplTest.java b/game-cloud/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/passport/PassportServiceImplTest.java index ee63ca7d..018ead42 100644 --- a/game-cloud/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/passport/PassportServiceImplTest.java +++ b/game-cloud/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/passport/PassportServiceImplTest.java @@ -193,6 +193,9 @@ class PassportServiceImplTest extends BaseMockitoUnitTest { assertEquals("sms", captor.getValue().getRegisterChannel()); assertEquals("anon-x", captor.getValue().getFirstAnonId()); assertEquals(0, captor.getValue().getCreatorFlag()); // 默认非创作者 + // 2026-06-10 鉴权波 e2e 实证(P0-1):免登注册匿名路径显式兜底审计列 creator/updater="0",规避 NOT NULL 撞库 500 + assertEquals("0", captor.getValue().getCreator(), "匿名注册 creator 应兜底 0"); + assertEquals("0", captor.getValue().getUpdater(), "匿名注册 updater 应兜底 0"); // 发真 token,响应带 userId assertEquals(2048L, resp.getUserId()); assertEquals("happy-token", resp.getAccessToken()); @@ -283,6 +286,9 @@ class PassportServiceImplTest extends BaseMockitoUnitTest { assertEquals(77L, captor.getValue().getInviteCodeId()); // 关联核销的邀请码 assertEquals("anon-y", captor.getValue().getFirstAnonId()); assertEquals("小明", captor.getValue().getNickname()); // 传入昵称生效 + // 2026-06-10 鉴权波 e2e 实证(P0-1):邀请码注册同为免登匿名路径,审计列 creator/updater 显式兜底 "0" + assertEquals("0", captor.getValue().getCreator(), "匿名注册 creator 应兜底 0"); + assertEquals("0", captor.getValue().getUpdater(), "匿名注册 updater 应兜底 0"); assertEquals(2048L, resp.getUserId()); } diff --git a/game-cloud/yudao-server/src/main/resources/application-staging.yaml b/game-cloud/yudao-server/src/main/resources/application-staging.yaml index d7e5bd92..e00aad64 100644 --- a/game-cloud/yudao-server/src/main/resources/application-staging.yaml +++ b/game-cloud/yudao-server/src/main/resources/application-staging.yaml @@ -127,6 +127,11 @@ spring: logging: level: org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR + # 2026-06-10 鉴权波 E11 安全实证(响应安全基线 §5.1):ApiAccessLogInterceptor.preHandle 在非 prod 会 log.info 打印原始请求体 + # (见 yudao-spring-boot-starter-web .../interceptor/ApiAccessLogInterceptor.java:51),passport 三端点请求体含 mobile/code/inviteCode 明文。 + # 该拦截器属 framework 层不可改、且无条件注册(不读 @ApiAccessLog 注解),唯一不改框架的关停手段=抬高其日志级别至 WARN(信任边界=配置)。 + # 控制台明文面由此关闭;持久化访问日志面由三端点 @ApiAccessLog(sanitizeKeys) 脱敏,两面合围。 + cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor: WARN debug: false