✨ 放开小程序端未注册手机号发码,账号仍在登录时创建

This commit is contained in:
2026-10-23 16:06:24 +08:00
parent f8da2ceaeb
commit 5df95ce712
2 changed files with 175 additions and 4 deletions
@@ -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));
}
}