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);