From ccf3d7adf7b98908745966a2cb81b8d37f04a100 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Mon, 26 Oct 2026 13:48:30 +0800 Subject: [PATCH] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E9=87=8D=E6=96=B0=E6=8E=88=E6=9D=83=E5=9B=A0=E7=A9=BA?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E5=88=A4=E7=A9=BA=E5=A4=B1=E8=B4=A5=E6=8A=A5?= =?UTF-8?q?=20500=20=E4=B8=94=20openid=20=E6=97=A0=E6=B3=95=E7=BB=91?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit system 侧 R.data(null) 被空值序列化写成 {},Feign 侧反序列化出 id 为空的 MpUser 而非 null,被当作「已查到」:换号撞键分支误置 openidOccupied,导致 openid 永不绑定并提示「该微信已绑定其他账号」;手机号查不到账号时还会以 空主键、空租户调用 bindOpenId,系统侧判缺参 400,再被 Feign 降级包成 AssertionError(Error 而非 Exception)逃出 catch (Exception),登录返回 500。 - 新增 presentAccount():以主键判定是否命中,findByOpenid/findByPhone 统一走它 - bindOpenid 入口加守卫:缺主键或租户只记 WARN 后跳过,不再发空参数请求 - 补 2 条单测:空对象 data 视为未命中;不落空主键账号、不发空主键绑定 --- .../auth/support/MpLoginSupport.java | 22 ++++++++++- .../auth/support/MpLoginSupportTest.java | 38 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java b/blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java index ab28cd7f..da5442a4 100644 --- a/blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java +++ b/blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java @@ -144,7 +144,7 @@ public class MpLoginSupport { } try { R result = mpUserClient.userByOpenid(tenantId, openid); - return isSuccess(result) ? result.getData() : null; + return presentAccount(isSuccess(result) ? result.getData() : null); } catch (Exception exception) { // 查询失败按未命中处理,随后由 ensureUser 触达系统服务并兜住建号失败 log.warn("按 openid 查询小程序账号失败:租户={} openid={}", tenantId, openid, exception); @@ -155,13 +155,25 @@ public class MpLoginSupport { private MpUser findByPhone(String tenantId, String phone) { try { R result = mpUserClient.userByPhone(tenantId, phone); - return isSuccess(result) ? result.getData() : null; + return presentAccount(isSuccess(result) ? result.getData() : null); } catch (Exception exception) { log.warn("按手机号查询小程序账号失败:租户={} 手机号={}", tenantId, phone, exception); return null; } } + /** + * 远端「查无此账号」不等于客户端拿到 null + *

+ * 服务端 R.data(null) 落 HTTP 时被 null 空值序列化写成 {}(BladeBeanSerializerModifier 对 + * Object 属性 null 输出空对象),Feign 侧于是反序列化出一个 id 为空的 MpUser 实例而不是 null。 + * 调用方只判 null,会把「没查到」当成「查到了空账号」,越过 ensureUser 建号并带着空主键去改库。 + * 故一律以主键判定是否命中。 + */ + private static MpUser presentAccount(MpUser account) { + return account == null || account.getId() == null ? null : account; + } + /** * ② 查找或创建账号:登录路径上唯一允许让登录失败的动作 */ @@ -267,6 +279,12 @@ public class MpLoginSupport { if (account == null || StringUtil.isBlank(openid)) { return; } + // 空主键/空租户来自「查无账号」被反序列化成空对象,绑定请求会被系统侧判为缺参 400 + if (account.getId() == null || StringUtil.isBlank(account.getTenantId())) { + log.warn("小程序账号缺少主键或租户,跳过绑定 openid:租户={} 账号={} openid={}", + account.getTenantId(), account.getId(), openid); + return; + } // 撞键场景 openid 已属于另一个账号,明确不迁移,也不再尝试绑定 if (openidOccupied) { return; diff --git a/blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java b/blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java index 6d09b316..79284906 100644 --- a/blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java +++ b/blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java @@ -229,6 +229,44 @@ class MpLoginSupportTest { verify(mpUserClient, never()).bindOpenId(anyString(), anyLong(), anyString()); } + @Test + @DisplayName("远端查无账号返回空对象(id 为空):按未命中建号,并用真实主键补绑 openid") + void emptyAccountFromRemoteIsTreatedAsMiss() { + // 服务端 R.data(null) 落 HTTP 是 {},Feign 反序列化成 id 为空的 MpUser 而非 null + MpUser created = mpUser(61L, PHONE, null, "driver"); + when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(new MpUser())); + when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(new MpUser())); + when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(created)); + when(mpUserClient.bindOpenId(TENANT_ID, 61L, OPENID)).thenReturn(R.data(Boolean.TRUE)); + when(mpUserClient.userInfoById(61L)).thenReturn(R.data(userInfo(created, "driver"))); + when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult())); + + OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID); + + assertNotNull(user); + assertEquals("61", user.getUserId()); + assertNull(user.getDetail().get(MpLoginSupport.DETAIL_LOGIN_NOTICE), "不该出现换号撞键提示"); + verify(mpUserClient).ensureUser(any(MpUserEnsureDTO.class)); + verify(mpUserClient, never()).updateUser(any(MpUser.class)); + verify(mpUserClient).bindOpenId(TENANT_ID, 61L, OPENID); + } + + @Test + @DisplayName("两个查询都返回空对象时:不落空账号、不发起带空主键的绑定") + void emptyAccountDoesNotLeakNullIdentityIntoBind() { + MpUser created = mpUser(62L, PHONE, OPENID, "driver"); + when(mpUserClient.userByOpenid(TENANT_ID, OPENID)).thenReturn(R.data(new MpUser())); + when(mpUserClient.userByPhone(TENANT_ID, PHONE)).thenReturn(R.data(new MpUser())); + when(mpUserClient.ensureUser(any(MpUserEnsureDTO.class))).thenReturn(R.data(created)); + when(mpUserClient.userInfoById(62L)).thenReturn(R.data(userInfo(created, "driver"))); + when(driverClient.ensureProfile(any(DriverEnsureProfileDTO.class))).thenReturn(R.data(profileResult())); + + OAuth2User user = mpLoginSupport.ensureLoginAccount(TENANT_ID, PHONE, OPENID); + + assertEquals("62", user.getUserId()); + verify(mpUserClient, never()).bindOpenId(anyString(), anyLong(), anyString()); + } + private MpUser mpUser(Long id, String phone, String openid, String identities) { MpUser user = new MpUser(); user.setId(id);