♻️ 收敛小程序登录请求类型判断并调整登录编排类位置
- 新增 org.springblade.auth.support.MpLoginRequestSupport.isMp,供三条登录链路共用 - 删除 BladeUserDetailService / SmsTokenGranter / Oauth2SmsEndpoint 各自私有的同义判据 - MpLoginSupport 及其测试移入 auth.support 包,类名、行为与注入方式不变 - 顺带订正上述文件的注释为事实陈述
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* <p>
|
||||
* 除验证码校验外,还要按请求来源分流:小程序(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"))
|
||||
|
||||
@@ -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("用户身份无效,无法登录");
|
||||
|
||||
@@ -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<MpUserInfo> 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))
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* 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()));
|
||||
}
|
||||
|
||||
}
|
||||
+18
-21
@@ -23,7 +23,7 @@
|
||||
* <p>
|
||||
* 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 / 手机号找到或创建账号,并补齐司机档案与微信绑定。
|
||||
* <p>
|
||||
* 只有「建号失败」会让登录失败;建档与绑 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 绑定。
|
||||
* <p>
|
||||
* 返回 {@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<MpUser> 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 账号的手机号换成新号
|
||||
* <p>
|
||||
* 只 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);
|
||||
+2
-2
@@ -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;
|
||||
/**
|
||||
* 登录编排测试。
|
||||
* <p>
|
||||
* 守住票 02 / 03 / 05 的三条口径:建号失败才阻断登录;建档失败放行;
|
||||
* 保住三条不变式:建号失败才阻断登录;建档失败放行;
|
||||
* openid 换号撞键时落到手机号账号且不迁移 openid。
|
||||
*/
|
||||
class MpLoginSupportTest {
|
||||
Reference in New Issue
Block a user