🔀 合并小程序发码放开:未注册手机号可收码,账号仍在登录时创建
This commit is contained in:
@@ -39,6 +39,7 @@ 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;
|
||||
@@ -87,13 +88,18 @@ public class Oauth2SmsEndpoint {
|
||||
if (StringUtil.isBlank(decryptedPhone)) {
|
||||
return R.fail(USER_PHONE_NOT_FOUND);
|
||||
}
|
||||
// 校验手机是否已注册,防止恶意发送验证码
|
||||
// buildArgs:带上 User-Type 等请求头,小程序传 User-Type=mp 时查 blade_mp_user
|
||||
OAuth2Request request = OAuth2Request.create().buildArgs();
|
||||
request.setTenantId(tenantId);
|
||||
OAuth2User oAuth2User = userService.loadByPhone(decryptedPhone, request);
|
||||
if (oAuth2User == null) {
|
||||
return R.fail(USER_PHONE_NOT_FOUND);
|
||||
// 「手机号是否已注册」只对 Web 保留。小程序端必须允许未注册手机号收码,
|
||||
// 否则新号走不到「验证码校验通过后自动建号」那一步(票 13)。
|
||||
// 该处是全链路唯一的防短信轰炸点,放开小程序端不等于放开 Web;
|
||||
// 这里只做查询,发码路径不建号,账号仍只在登录时创建。
|
||||
if (!isMpLoginRequest(request)) {
|
||||
OAuth2User oAuth2User = userService.loadByPhone(decryptedPhone, request);
|
||||
if (oAuth2User == null) {
|
||||
return R.fail(USER_PHONE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
// 使用指定短信资源(默认 ali_reg)
|
||||
String smsResourceCode = Func.toStr(code, BladeAuthConstant.LOGIN_SMS_CODE);
|
||||
@@ -101,4 +107,11 @@ 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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package org.springblade.auth.endpoint;
|
||||
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springblade.core.oauth2.props.OAuth2Properties;
|
||||
import org.springblade.core.oauth2.service.OAuth2User;
|
||||
import org.springblade.core.oauth2.service.OAuth2UserService;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.SM2Util;
|
||||
import org.springblade.resource.feign.ISmsClient;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springblade.core.oauth2.constant.OAuth2TokenConstant.USER_PHONE_NOT_FOUND;
|
||||
|
||||
/**
|
||||
* 发码端点的端分流测试。
|
||||
* <p>
|
||||
* 守住票 13 的口径:小程序端(User-Type: mp)对未注册手机号也要发码,
|
||||
* 否则新号走不到「验证码校验通过后自动建号」;Web 端保持拒绝。
|
||||
* 同时守住「发码不建号」:mp 分支根本不查账号。
|
||||
*/
|
||||
class Oauth2SmsEndpointTest {
|
||||
|
||||
private static final String TENANT_ID = "000000";
|
||||
private static final String PHONE = "13800000001";
|
||||
private static final String SMS_CODE = "ali_reg";
|
||||
|
||||
private static String publicKey;
|
||||
private static String privateKey;
|
||||
|
||||
private ISmsClient smsClient;
|
||||
private OAuth2UserService userService;
|
||||
private Oauth2SmsEndpoint endpoint;
|
||||
|
||||
@BeforeAll
|
||||
static void initSm2Keys() {
|
||||
AsymmetricCipherKeyPair keyPair = SM2Util.generateKeyPair();
|
||||
publicKey = SM2Util.getPublicKeyString(keyPair);
|
||||
privateKey = SM2Util.getPrivateKeyString(keyPair);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
OAuth2Properties properties = new OAuth2Properties();
|
||||
properties.setPublicKey(publicKey);
|
||||
properties.setPrivateKey(privateKey);
|
||||
smsClient = mock(ISmsClient.class);
|
||||
userService = mock(OAuth2UserService.class);
|
||||
endpoint = new Oauth2SmsEndpoint(smsClient, userService, properties);
|
||||
when(smsClient.sendValidate(anyString(), anyString(), anyString())).thenReturn(R.data("sms-id-1"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void clearRequestContext() {
|
||||
RequestContextHolder.resetRequestAttributes();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("小程序 + 未注册手机号:发码成功,且完全不查账号(发码不建号)")
|
||||
void mpUnregisteredPhoneSendsCode() {
|
||||
bindRequest("mp");
|
||||
when(userService.loadByPhone(anyString(), any())).thenReturn(null);
|
||||
|
||||
R result = endpoint.sendValidate(TENANT_ID, encrypt(PHONE), SMS_CODE);
|
||||
|
||||
assertTrue(result.isSuccess());
|
||||
verify(smsClient).sendValidate(TENANT_ID, SMS_CODE, PHONE);
|
||||
verify(userService, never()).loadByPhone(anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Web + 未注册手机号:仍返回手机号不存在,不发码")
|
||||
void webUnregisteredPhoneIsRejected() {
|
||||
bindRequest(null);
|
||||
when(userService.loadByPhone(anyString(), any())).thenReturn(null);
|
||||
|
||||
R result = endpoint.sendValidate(TENANT_ID, encrypt(PHONE), SMS_CODE);
|
||||
|
||||
assertFalse(result.isSuccess());
|
||||
assertEquals(USER_PHONE_NOT_FOUND, result.getMsg());
|
||||
verify(smsClient, never()).sendValidate(anyString(), anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Web + 已注册手机号:行为与改造前一致,正常发码")
|
||||
void webRegisteredPhoneSendsCode() {
|
||||
bindRequest(null);
|
||||
when(userService.loadByPhone(anyString(), any())).thenReturn(mock(OAuth2User.class));
|
||||
|
||||
R result = endpoint.sendValidate(TENANT_ID, encrypt(PHONE), SMS_CODE);
|
||||
|
||||
assertTrue(result.isSuccess());
|
||||
verify(userService).loadByPhone(anyString(), any());
|
||||
verify(smsClient).sendValidate(TENANT_ID, SMS_CODE, PHONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("小程序 + 已注册手机号:跳过注册查询,依旧发码")
|
||||
void mpRegisteredPhoneSendsCode() {
|
||||
bindRequest("mp");
|
||||
|
||||
R result = endpoint.sendValidate(TENANT_ID, encrypt(PHONE), SMS_CODE);
|
||||
|
||||
assertTrue(result.isSuccess());
|
||||
verify(userService, never()).loadByPhone(anyString(), any());
|
||||
verify(smsClient).sendValidate(TENANT_ID, SMS_CODE, PHONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("密文无法解密:无论哪端都不发码")
|
||||
void invalidCipherIsRejected() {
|
||||
bindRequest("mp");
|
||||
|
||||
R result = endpoint.sendValidate(TENANT_ID, "deadbeef", SMS_CODE);
|
||||
|
||||
assertFalse(result.isSuccess());
|
||||
assertEquals(USER_PHONE_NOT_FOUND, result.getMsg());
|
||||
verify(smsClient, never()).sendValidate(anyString(), anyString(), anyString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求上下文决定 buildArgs() 读到什么头;Authorization 是 buildClientArgs 的前提
|
||||
*/
|
||||
private void bindRequest(String userType) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
String credential = Base64.getEncoder()
|
||||
.encodeToString("saber:saber_secret".getBytes(StandardCharsets.UTF_8));
|
||||
request.addHeader("Authorization", "Basic " + credential);
|
||||
if (userType != null) {
|
||||
request.addHeader("User-Type", userType);
|
||||
}
|
||||
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
|
||||
}
|
||||
|
||||
private String encrypt(String plainPhone) {
|
||||
return Hex.toHexString(SM2Util.encrypt(plainPhone, publicKey));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user