🔀 合并小程序发码放开:未注册手机号可收码,账号仍在登录时创建
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.core.tool.utils.StringUtil;
|
||||||
import org.springblade.resource.feign.ISmsClient;
|
import org.springblade.resource.feign.ISmsClient;
|
||||||
import org.springblade.resource.utils.SmsUtil;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@@ -87,13 +88,18 @@ public class Oauth2SmsEndpoint {
|
|||||||
if (StringUtil.isBlank(decryptedPhone)) {
|
if (StringUtil.isBlank(decryptedPhone)) {
|
||||||
return R.fail(USER_PHONE_NOT_FOUND);
|
return R.fail(USER_PHONE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
// 校验手机是否已注册,防止恶意发送验证码
|
|
||||||
// buildArgs:带上 User-Type 等请求头,小程序传 User-Type=mp 时查 blade_mp_user
|
// buildArgs:带上 User-Type 等请求头,小程序传 User-Type=mp 时查 blade_mp_user
|
||||||
OAuth2Request request = OAuth2Request.create().buildArgs();
|
OAuth2Request request = OAuth2Request.create().buildArgs();
|
||||||
request.setTenantId(tenantId);
|
request.setTenantId(tenantId);
|
||||||
OAuth2User oAuth2User = userService.loadByPhone(decryptedPhone, request);
|
// 「手机号是否已注册」只对 Web 保留。小程序端必须允许未注册手机号收码,
|
||||||
if (oAuth2User == null) {
|
// 否则新号走不到「验证码校验通过后自动建号」那一步(票 13)。
|
||||||
return R.fail(USER_PHONE_NOT_FOUND);
|
// 该处是全链路唯一的防短信轰炸点,放开小程序端不等于放开 Web;
|
||||||
|
// 这里只做查询,发码路径不建号,账号仍只在登录时创建。
|
||||||
|
if (!isMpLoginRequest(request)) {
|
||||||
|
OAuth2User oAuth2User = userService.loadByPhone(decryptedPhone, request);
|
||||||
|
if (oAuth2User == null) {
|
||||||
|
return R.fail(USER_PHONE_NOT_FOUND);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 使用指定短信资源(默认 ali_reg)
|
// 使用指定短信资源(默认 ali_reg)
|
||||||
String smsResourceCode = Func.toStr(code, BladeAuthConstant.LOGIN_SMS_CODE);
|
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);
|
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