diff --git a/blade-auth/src/main/java/org/springblade/auth/endpoint/Oauth2SmsEndpoint.java b/blade-auth/src/main/java/org/springblade/auth/endpoint/Oauth2SmsEndpoint.java index 0729dfe8..8bbd2755 100644 --- a/blade-auth/src/main/java/org/springblade/auth/endpoint/Oauth2SmsEndpoint.java +++ b/blade-auth/src/main/java/org/springblade/auth/endpoint/Oauth2SmsEndpoint.java @@ -29,6 +29,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; import lombok.SneakyThrows; import org.springblade.auth.constant.BladeAuthConstant; +import org.springblade.auth.support.MpLoginRequestSupport; import org.springblade.core.oauth2.props.OAuth2Properties; import org.springblade.core.oauth2.provider.OAuth2Request; import org.springblade.core.oauth2.service.OAuth2User; @@ -39,7 +40,6 @@ import org.springblade.core.tool.utils.SM2Util; import org.springblade.core.tool.utils.StringUtil; import org.springblade.resource.feign.ISmsClient; import org.springblade.resource.utils.SmsUtil; -import org.springblade.system.pojo.enums.MpUserIdentity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -91,11 +91,9 @@ public class Oauth2SmsEndpoint { // buildArgs:带上 User-Type 等请求头,小程序传 User-Type=mp 时查 blade_mp_user OAuth2Request request = OAuth2Request.create().buildArgs(); request.setTenantId(tenantId); - // 「手机号是否已注册」只对 Web 保留。小程序端必须允许未注册手机号收码, - // 否则新号走不到「验证码校验通过后自动建号」那一步(票 13)。 - // 该处是全链路唯一的防短信轰炸点,放开小程序端不等于放开 Web; - // 这里只做查询,发码路径不建号,账号仍只在登录时创建。 - if (!isMpLoginRequest(request)) { + // 「手机号是否已注册」只对 Web 校验;小程序端未注册手机号也发码,账号在登录时创建。 + // 发码路径只做查询,不建号。 + if (!MpLoginRequestSupport.isMp(request)) { OAuth2User oAuth2User = userService.loadByPhone(decryptedPhone, request); if (oAuth2User == null) { return R.fail(USER_PHONE_NOT_FOUND); @@ -107,11 +105,4 @@ public class Oauth2SmsEndpoint { return result.isSuccess() ? R.data(result.getData(), SmsUtil.SEND_SUCCESS) : R.fail(SmsUtil.SEND_FAIL); } - /** - * 是否小程序请求:请求头 User-Type=mp,与登录链路同一判据 - */ - private boolean isMpLoginRequest(OAuth2Request request) { - return request != null && MpUserIdentity.DETAIL_TYPE.equalsIgnoreCase(Func.toStr(request.getUserType())); - } - } diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/SmsTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/SmsTokenGranter.java index b79f8497..cd01f6ef 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/SmsTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/SmsTokenGranter.java @@ -27,7 +27,8 @@ package org.springblade.auth.granter; import jakarta.servlet.http.HttpServletRequest; import org.springblade.auth.constant.BladeAuthConstant; -import org.springblade.auth.service.MpLoginSupport; +import org.springblade.auth.support.MpLoginRequestSupport; +import org.springblade.auth.support.MpLoginSupport; import org.springblade.core.oauth2.constant.OAuth2TokenConstant; import org.springblade.core.oauth2.exception.UserInvalidException; import org.springblade.core.oauth2.granter.AbstractTokenGranter; @@ -44,15 +45,13 @@ import org.springblade.core.tool.utils.SM2Util; import org.springblade.core.tool.utils.StringUtil; import org.springblade.core.tool.utils.WebUtil; import org.springblade.resource.feign.ISmsClient; -import org.springblade.system.pojo.enums.MpUserIdentity; import org.springframework.stereotype.Component; /** * SmsTokenGranter *

- * 除验证码校验外,还要按请求来源分流:小程序(User-Type: mp)登录时账号不存在要自动建, - * Web 短信登录保持原样不建号。建号逻辑不能下沉到 {@code BladeUserDetailService.loadByPhone}, - * 那条方法还被「发送短信验证码」复用,会在发码时就建出账号(刷号)。 + * 按请求来源分流:小程序(User-Type: mp)登录时账号不存在要自动建,Web 短信登录不建号。 + * {@code BladeUserDetailService.loadByPhone} 同时被「发送短信验证码」复用,其中不得建号。 * * @author BladeX */ @@ -94,8 +93,8 @@ public class SmsTokenGranter extends AbstractTokenGranter { if (!result.isSuccess()) { throw new UserInvalidException(OAuth2TokenConstant.CAPTCHA_NOT_CORRECT); } - // 获取用户信息:只有小程序登录才允许自动建号,Web 短信登录仍走原查询 - OAuth2User user = isMpLoginRequest(request) + // 获取用户信息:小程序登录走自动建号编排,Web 短信登录只做查询 + OAuth2User user = MpLoginRequestSupport.isMp(request) ? mpLoginSupport.ensureLoginAccount(tenantId, decryptedPhone, null) : userService.loadByPhone(decryptedPhone, request); // 校验用户信息 @@ -107,13 +106,6 @@ public class SmsTokenGranter extends AbstractTokenGranter { return user; } - /** - * 是否小程序登录:请求头 User-Type=mp(Web 与小程序共用客户端,不能按 clientId 分流) - */ - private boolean isMpLoginRequest(OAuth2Request request) { - return request != null && MpUserIdentity.DETAIL_TYPE.equalsIgnoreCase(Func.toStr(request.getUserType())); - } - private SmsCode buildSmsCode() { HttpServletRequest request = WebUtil.getRequest(); return new SmsCode().setId(request.getParameter("id")) diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java index 101387c8..9d2bb75c 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/WechatMiniTokenGranter.java @@ -2,7 +2,7 @@ package org.springblade.auth.granter; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; -import org.springblade.auth.service.MpLoginSupport; +import org.springblade.auth.support.MpLoginSupport; import org.springblade.core.oauth2.exception.UserInvalidException; import org.springblade.core.oauth2.granter.AbstractTokenGranter; import org.springblade.core.oauth2.handler.PasswordHandler; @@ -79,7 +79,7 @@ public class WechatMiniTokenGranter extends AbstractTokenGranter { throw new UserInvalidException("未获取到微信手机号"); } - // 账号不存在时自动建号,建档与绑 openid 由编排层尽力而为,失败不阻断登录 + // 账号不存在时自动建号;建档与绑 openid 失败不阻断登录 OAuth2User user = mpLoginSupport.ensureLoginAccount(request.getTenantId(), phone, session.getOpenid()); if (!userService.validateUser(user)) { throw new UserInvalidException("用户身份无效,无法登录"); diff --git a/blade-auth/src/main/java/org/springblade/auth/service/BladeUserDetailService.java b/blade-auth/src/main/java/org/springblade/auth/service/BladeUserDetailService.java index 22904efe..10f5d6b7 100644 --- a/blade-auth/src/main/java/org/springblade/auth/service/BladeUserDetailService.java +++ b/blade-auth/src/main/java/org/springblade/auth/service/BladeUserDetailService.java @@ -23,6 +23,7 @@ package org.springblade.auth.service; import lombok.RequiredArgsConstructor; +import org.springblade.auth.support.MpLoginRequestSupport; import org.springblade.auth.utils.TokenUtil; import org.springblade.core.oauth2.exception.OAuth2ErrorCode; import org.springblade.core.oauth2.provider.OAuth2Request; @@ -36,7 +37,6 @@ import org.springblade.system.feign.IMpUserClient; import org.springblade.system.feign.IUserClient; import org.springblade.system.pojo.entity.MpUserInfo; import org.springblade.system.pojo.entity.UserInfo; -import org.springblade.system.pojo.enums.MpUserIdentity; import org.springblade.system.pojo.enums.UserType; import java.util.Optional; @@ -54,7 +54,7 @@ public class BladeUserDetailService implements OAuth2UserService { @Override public OAuth2User loadByUserId(String userId, OAuth2Request request) { // 小程序刷新/鉴权:User-Type=mp 时只查 blade_mp_user - if (isMpLoginRequest(request)) { + if (MpLoginRequestSupport.isMp(request)) { OAuth2User mpUser = loadMpUserById(userId); if (mpUser == null) { OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER); @@ -83,7 +83,7 @@ public class BladeUserDetailService implements OAuth2UserService { String tenantId = request.getTenantId(); // 小程序账号密码 / 图形验证码登录:仅查 blade_mp_user - if (isMpLoginRequest(request)) { + if (MpLoginRequestSupport.isMp(request)) { R mpResult = mpUserClient.userInfoByAccount(tenantId, username); if (mpResult == null || !mpResult.isSuccess() || mpResult.getData() == null) { return null; @@ -104,7 +104,7 @@ public class BladeUserDetailService implements OAuth2UserService { String tenantId = request.getTenantId(); // 小程序短信登录:仅查 blade_mp_user - if (isMpLoginRequest(request)) { + if (MpLoginRequestSupport.isMp(request)) { return loadMpUserByPhone(tenantId, phone); } @@ -125,17 +125,6 @@ public class BladeUserDetailService implements OAuth2UserService { .isPresent(); } - /** - * 是否小程序登录:请求头 User-Type=mp(Web 与小程序共用 saber3 客户端,不能按 clientId 分流) - */ - private boolean isMpLoginRequest(OAuth2Request request) { - return request != null && isMpUserType(request.getUserType()); - } - - private boolean isMpUserType(String userType) { - return MpUserIdentity.DETAIL_TYPE.equalsIgnoreCase(Func.toStr(userType)); - } - private String resolveUserType(OAuth2Request request) { return Optional.ofNullable(request.getUserType()) .filter(s -> !StringUtil.isBlank(s)) diff --git a/blade-auth/src/main/java/org/springblade/auth/support/MpLoginRequestSupport.java b/blade-auth/src/main/java/org/springblade/auth/support/MpLoginRequestSupport.java new file mode 100644 index 00000000..012ffbbb --- /dev/null +++ b/blade-auth/src/main/java/org/springblade/auth/support/MpLoginRequestSupport.java @@ -0,0 +1,54 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.auth.support; + +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.pojo.enums.MpUserIdentity; + +/** + * 登录请求来源判定 + * + * @author Chill + */ +public final class MpLoginRequestSupport { + + private MpLoginRequestSupport() { + } + + /** + * 是否小程序请求:请求头 User-Type 为 mp + *

+ * Web 与小程序共用同一个客户端,来源只认请求头 User-Type。 + * + * @param request 登录请求,可为 null + * @return 请求头 User-Type 为 mp 时返回 true + */ + public static boolean isMp(OAuth2Request request) { + return request != null && MpUserIdentity.DETAIL_TYPE.equalsIgnoreCase(Func.toStr(request.getUserType())); + } + +} diff --git a/blade-auth/src/main/java/org/springblade/auth/service/MpLoginSupport.java b/blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java similarity index 87% rename from blade-auth/src/main/java/org/springblade/auth/service/MpLoginSupport.java rename to blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java index 6032ebeb..1a48b334 100644 --- a/blade-auth/src/main/java/org/springblade/auth/service/MpLoginSupport.java +++ b/blade-auth/src/main/java/org/springblade/auth/support/MpLoginSupport.java @@ -23,7 +23,7 @@ *

* Author: Chill Zhuang (bladejava@qq.com) */ -package org.springblade.auth.service; +package org.springblade.auth.support; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -47,10 +47,10 @@ import org.springframework.stereotype.Component; import java.util.Objects; /** - * 小程序登录编排:按 openid / 手机号找到或创建账号,并尽力补齐司机档案与微信绑定。 + * 小程序登录编排:按 openid / 手机号找到或创建账号,并补齐司机档案与微信绑定。 *

- * 只有「建号失败」会让登录失败;建档与绑 openid 都是尽力而为,失败只记 WARN, - * 下次登录自愈。复制这段判断到别处前先确认调用方不是登录路径。 + * 只有「建号失败」会让登录失败;建档与绑 openid 失败只记 WARN,不影响本次登录。 + * 本类只供登录链路调用。 * * @author Chill */ @@ -65,7 +65,7 @@ public class MpLoginSupport { public static final String DETAIL_LOGIN_NOTICE = "loginNotice"; /** - * 换号撞键:openid 属于旧账号、新手机号已属于另一账号(票 05 Q2-A) + * 换号撞键提示:openid 属于旧账号、新手机号已属于另一账号 */ private static final String NOTICE_PHONE_OCCUPIED = "该微信已绑定其他账号,请联系管理员"; @@ -73,11 +73,10 @@ public class MpLoginSupport { private final IDriverClient driverClient; /** - * 找到或创建本次登录使用的账号,并尽力补齐档案与 openid 绑定。 + * 找到或创建本次登录使用的账号,并补齐档案与 openid 绑定。 *

- * 返回 {@link OAuth2User} 而不是 {@link MpUserInfo}:两条登录链路都需要 OAuth2User, - * 且 MpUserInfo 的唯一组装点(identities / permissions / detail)在 system 侧, - * 由本类统一转换可避免 auth 侧复制一份 detail 口径。 + * 返回 {@link OAuth2User}:两条登录链路都消费 OAuth2User,而 MpUserInfo + * (identities / permissions / detail)只在 system 侧组装。 * * @param tenantId 请求租户,为空时由 system 侧兜到默认租户 * @param phone 本次登录的手机号 @@ -102,8 +101,7 @@ public class MpLoginSupport { if (!samePhone(openidAccount.getPhone(), phone)) { MpUser phoneAccount = findByPhone(tenantId, phone); if (phoneAccount != null && !Objects.equals(phoneAccount.getId(), openidAccount.getId())) { - // ①-撞键:落到手机号账号,不迁移 openid。以 openid 账号为准会把用户 - // 正在用的手机号账号顶掉,自动合并又要动档案归属(票 05) + // ①-撞键:落到手机号账号,不迁移 openid account = phoneAccount; loginNotice = NOTICE_PHONE_OCCUPIED; openidOccupied = true; @@ -128,12 +126,12 @@ public class MpLoginSupport { } } } - // ③ 身份含 driver 才建档;纯 admin 不建,避免污染司机列表与派单池(票 03) + // ③ 身份含 driver 才建档;纯 admin 不建 String profileNotice = ensureProfile(account, phone, tenantId); if (StringUtil.isBlank(loginNotice)) { loginNotice = profileNotice; } - // ④ 微信登录补绑 openid;失败只 WARN(票 05) + // ④ 微信登录补绑 openid;失败只 WARN bindOpenid(account, openid, openidOccupied); MpUserInfo userInfo = loadUserInfo(account); attachNotice(userInfo, loginNotice); @@ -148,8 +146,7 @@ public class MpLoginSupport { R result = mpUserClient.userByOpenid(tenantId, openid); return isSuccess(result) ? result.getData() : null; } catch (Exception exception) { - // 查询失败按未命中处理:随后 ensureUser 会再次触达系统服务, - // 真不可用时在那里以「建号失败」拦下,避免这里静默建出重复账号 + // 查询失败按未命中处理,随后由 ensureUser 触达系统服务并兜住建号失败 log.warn("按 openid 查询小程序账号失败:租户={} openid={}", tenantId, openid, exception); return null; } @@ -166,7 +163,7 @@ public class MpLoginSupport { } /** - * ② 查找或创建账号:这是登录路径上唯一允许让登录失败的动作 + * ② 查找或创建账号:登录路径上唯一允许让登录失败的动作 */ private MpUser ensureUser(String tenantId, String phone, String openid) { MpUserEnsureDTO param = new MpUserEnsureDTO(); @@ -191,10 +188,10 @@ public class MpLoginSupport { } /** - * 把 openid 账号的手机号换成新号(票 03:openid 命中即复用并更新手机号) + * 把 openid 账号的手机号换成新号 *

* 只 patch phone:account 是登录名,不随手机号变;status 必须带原值, - * 否则 system 侧 updateUser 的 prepare 会把空 status 重置成启用,等于解开停用账号。 + * system 侧 updateUser 的 prepare 会把空 status 重置成启用。 * * @return 更新成功后的账号;失败返回 null,不抛异常 */ @@ -230,8 +227,8 @@ public class MpLoginSupport { if (account == null || !MpUserIdentity.has(account.getIdentities(), MpUserIdentity.DRIVER)) { return null; } - // 租户与手机号优先取账号上的值:档案必须与账号落同一租户(唯一键对 NULL 不去重), - // 手机号更新失败时也不能拿新号去建,否则档案和账号对不上 + // 租户与手机号取账号上的值:档案必须与账号落同一租户(唯一键对 NULL 不去重); + // 账号手机号未能更新时,不使用入参手机号建档 String tenantId = Func.toStr(account.getTenantId(), requestedTenantId); String phone = Func.toStr(account.getPhone(), requestedPhone); DriverEnsureProfileDTO param = new DriverEnsureProfileDTO(); @@ -313,7 +310,7 @@ public class MpLoginSupport { if (userInfo == null || StringUtil.isBlank(loginNotice)) { return; } - // detail 是否会下发到前端取决于令牌序列化,故冲突点同时都记了 WARN + // detail 是否下发到前端取决于令牌序列化,冲突点同时另记 WARN Kv detail = userInfo.getDetail() == null ? Kv.create() : userInfo.getDetail(); detail.set(DETAIL_LOGIN_NOTICE, loginNotice); userInfo.setDetail(detail); diff --git a/blade-auth/src/test/java/org/springblade/auth/service/MpLoginSupportTest.java b/blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java similarity index 98% rename from blade-auth/src/test/java/org/springblade/auth/service/MpLoginSupportTest.java rename to blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java index 98617200..4518e344 100644 --- a/blade-auth/src/test/java/org/springblade/auth/service/MpLoginSupportTest.java +++ b/blade-auth/src/test/java/org/springblade/auth/support/MpLoginSupportTest.java @@ -1,4 +1,4 @@ -package org.springblade.auth.service; +package org.springblade.auth.support; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -35,7 +35,7 @@ import static org.mockito.Mockito.when; /** * 登录编排测试。 *

- * 守住票 02 / 03 / 05 的三条口径:建号失败才阻断登录;建档失败放行; + * 保住三条不变式:建号失败才阻断登录;建档失败放行; * openid 换号撞键时落到手机号账号且不迁移 openid。 */ class MpLoginSupportTest {