feat(aihr): harden M0-M2 mobile training flows
This commit is contained in:
@@ -6,6 +6,8 @@ import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
@@ -22,6 +24,7 @@ import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.domain.model.LoginBody;
|
||||
import org.dromara.common.core.domain.model.RegisterBody;
|
||||
import org.dromara.common.core.domain.model.SocialLoginBody;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.*;
|
||||
import org.dromara.common.encrypt.annotation.ApiEncrypt;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
@@ -71,7 +74,6 @@ import java.util.concurrent.TimeUnit;
|
||||
@RequestMapping("/auth")
|
||||
public class AuthController {
|
||||
|
||||
private static final String MOBILE_CLIENT_ID = "428a8310cd442757ae699df5d894f051";
|
||||
private static final String MOBILE_GRANT_TYPE = "sms";
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
@@ -125,25 +127,31 @@ public class AuthController {
|
||||
* 移动端短信登录,复用 app 客户端和 sms 授权策略。
|
||||
*/
|
||||
@PostMapping("/mobile/sms-login")
|
||||
public R<LoginVo> mobileSmsLogin(@Validated @RequestBody MobileSmsLoginBody body) {
|
||||
SysClientVo client = clientService.queryByClientId(MOBILE_CLIENT_ID);
|
||||
public R<MobileSmsLoginResponse> mobileSmsLogin(@Validated @RequestBody MobileSmsLoginBody body) {
|
||||
SysClientVo client = clientService.queryByClientId(SmsCodeUtils.MOBILE_CLIENT_ID);
|
||||
if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), MOBILE_GRANT_TYPE)) {
|
||||
log.info("移动端客户端id: {} 认证类型:{} 异常!.", MOBILE_CLIENT_ID, MOBILE_GRANT_TYPE);
|
||||
log.info("移动端客户端id: {} 认证类型:{} 异常!.", SmsCodeUtils.MOBILE_CLIENT_ID, MOBILE_GRANT_TYPE);
|
||||
return R.fail(MessageUtils.message("auth.grant.type.error"));
|
||||
} else if (!SystemConstants.NORMAL.equals(client.getStatus())) {
|
||||
return R.fail(MessageUtils.message("auth.grant.type.blocked"));
|
||||
}
|
||||
|
||||
String tenantId = StringUtils.blankToDefault(body.tenantId(), TenantConstants.DEFAULT_TENANT_ID);
|
||||
// 员工端没有匿名租户选择能力;不信任客户端传入的 tenantId,避免手机号验证码被用于加入其他租户。
|
||||
String tenantId = mobileTenantId();
|
||||
loginService.checkTenant(tenantId);
|
||||
|
||||
Map<String, String> loginBody = new HashMap<>();
|
||||
loginBody.put("clientId", MOBILE_CLIENT_ID);
|
||||
loginBody.put("clientId", SmsCodeUtils.MOBILE_CLIENT_ID);
|
||||
loginBody.put("grantType", MOBILE_GRANT_TYPE);
|
||||
loginBody.put("tenantId", tenantId);
|
||||
loginBody.put("phonenumber", body.phonenumber());
|
||||
loginBody.put("smsCode", body.smsCode());
|
||||
return R.ok(IAuthStrategy.login(JsonUtils.toJsonString(loginBody), client, MOBILE_GRANT_TYPE));
|
||||
LoginVo loginVo = IAuthStrategy.login(JsonUtils.toJsonString(loginBody), client, MOBILE_GRANT_TYPE);
|
||||
return R.ok(MobileSmsLoginResponse.from(loginVo));
|
||||
}
|
||||
|
||||
static String mobileTenantId() {
|
||||
return TenantConstants.DEFAULT_TENANT_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -272,14 +280,29 @@ public class AuthController {
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record MobileSmsLoginBody(
|
||||
@NotBlank(message = "{user.phonenumber.not.blank}")
|
||||
@Pattern(regexp = RegexConstants.MOBILE, message = "{user.mobile.phone.number.not.valid}")
|
||||
String phonenumber,
|
||||
@NotBlank(message = "{sms.code.not.blank}")
|
||||
String smsCode,
|
||||
String tenantId
|
||||
String smsCode
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动端保存会话所需的最小登录响应,避免复用包含后台扩展字段的登录视图对象。
|
||||
*/
|
||||
public record MobileSmsLoginResponse(
|
||||
@JsonProperty("access_token") String accessToken,
|
||||
@JsonProperty("client_id") String clientId
|
||||
) {
|
||||
static MobileSmsLoginResponse from(LoginVo loginVo) {
|
||||
if (loginVo == null || StringUtils.isBlank(loginVo.getAccessToken()) || StringUtils.isBlank(loginVo.getClientId())) {
|
||||
throw new ServiceException("移动端登录未返回认证信息");
|
||||
}
|
||||
return new MobileSmsLoginResponse(loginVo.getAccessToken(), loginVo.getClientId());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
-3
@@ -13,8 +13,10 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.constant.RegexConstants;
|
||||
import org.dromara.common.core.constant.TenantConstants;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.SmsCodeUtils;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mail.config.properties.MailProperties;
|
||||
@@ -28,6 +30,9 @@ import org.dromara.sms4j.api.SmsBlend;
|
||||
import org.dromara.sms4j.api.entity.SmsResponse;
|
||||
import org.dromara.sms4j.core.factory.SmsFactory;
|
||||
import org.dromara.web.domain.vo.CaptchaVo;
|
||||
import org.redisson.api.RBucket;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.Profiles;
|
||||
@@ -41,6 +46,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.awt.*;
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 验证码操作处理
|
||||
@@ -84,7 +90,6 @@ public class CaptchaController {
|
||||
public R<Void> smsCode(
|
||||
@NotBlank(message = "{user.phonenumber.not.blank}")
|
||||
@Pattern(regexp = RegexConstants.MOBILE, message = "{user.mobile.phone.number.not.valid}") String phonenumber) {
|
||||
String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber;
|
||||
boolean prodProfile = environment.acceptsProfiles(Profiles.of("prod"));
|
||||
boolean fixedCodeEnabled = shouldUseFixedSmsCode(
|
||||
smsDevFixedCode,
|
||||
@@ -93,7 +98,7 @@ public class CaptchaController {
|
||||
);
|
||||
if (fixedCodeEnabled) {
|
||||
log.warn("aihr.sms.dev-fixed-code 已启用,短信验证码走固定码,未真实发送短信");
|
||||
RedisUtils.setCacheObject(key, smsDevFixedCode, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
storeSmsCode(RedisUtils.getClient(), TenantConstants.DEFAULT_TENANT_ID, phonenumber, smsDevFixedCode);
|
||||
return R.ok();
|
||||
}
|
||||
if (StringUtils.isBlank(smsLoginTemplateId)) {
|
||||
@@ -108,10 +113,30 @@ public class CaptchaController {
|
||||
log.error("验证码短信发送异常 => {}", smsResponse);
|
||||
return R.fail(String.valueOf(smsResponse.getData()));
|
||||
}
|
||||
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
storeSmsCode(RedisUtils.getClient(), TenantConstants.DEFAULT_TENANT_ID, phonenumber, code);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
static void storeSmsCode(RedissonClient redisClient, String tenantId, String phonenumber, String code) {
|
||||
RLock lock = redisClient.getLock(SmsCodeUtils.verificationLockKey(tenantId, phonenumber));
|
||||
boolean locked = false;
|
||||
try {
|
||||
locked = lock.tryLock(2, 5, TimeUnit.SECONDS);
|
||||
if (!locked) {
|
||||
throw new ServiceException("验证码服务繁忙,请稍后重试");
|
||||
}
|
||||
RBucket<String> bucket = redisClient.getBucket(SmsCodeUtils.cacheKey(tenantId, phonenumber));
|
||||
bucket.set(code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ServiceException("验证码服务被中断,请重试");
|
||||
} finally {
|
||||
if (locked && lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean shouldUseFixedSmsCode(String fixedCode, boolean prodProfile, boolean prodFixedCodeEnabled) {
|
||||
return StringUtils.isNotBlank(fixedCode) && (!prodProfile || prodFixedCodeEnabled);
|
||||
}
|
||||
|
||||
+62
-11
@@ -7,7 +7,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.domain.model.SmsLoginBody;
|
||||
@@ -16,6 +15,7 @@ import org.dromara.common.core.enums.UserType;
|
||||
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.SmsCodeUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
@@ -29,8 +29,13 @@ import org.dromara.system.mapper.SysUserMapper;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.dromara.web.service.IAuthStrategy;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
import org.redisson.api.RBucket;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 短信认证策略
|
||||
*
|
||||
@@ -51,9 +56,10 @@ public class SmsAuthStrategy implements IAuthStrategy {
|
||||
String tenantId = loginBody.getTenantId();
|
||||
String phonenumber = loginBody.getPhonenumber();
|
||||
String smsCode = loginBody.getSmsCode();
|
||||
boolean appClient = isAppClient(client);
|
||||
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
|
||||
loginService.checkLogin(LoginType.SMS, tenantId, phonenumber, () -> !validateSmsCode(tenantId, phonenumber, smsCode));
|
||||
SysUserVo user = loadOrRegisterUserByPhonenumber(tenantId, phonenumber);
|
||||
SysUserVo user = loadOrRegisterUserByPhonenumber(tenantId, phonenumber, appClient);
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
return loginService.buildLoginUser(user);
|
||||
});
|
||||
@@ -80,18 +86,60 @@ public class SmsAuthStrategy implements IAuthStrategy {
|
||||
* 校验短信验证码
|
||||
*/
|
||||
private boolean validateSmsCode(String tenantId, String phonenumber, String smsCode) {
|
||||
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber);
|
||||
if (StringUtils.isBlank(code)) {
|
||||
loginService.recordLogininfor(tenantId, phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
return code.equals(smsCode);
|
||||
return validateSmsCode(RedisUtils.getClient(), tenantId, phonenumber, smsCode);
|
||||
}
|
||||
|
||||
private SysUserVo loadOrRegisterUserByPhonenumber(String tenantId, String phonenumber) {
|
||||
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>()
|
||||
boolean validateSmsCode(RedissonClient redisClient, String tenantId, String phonenumber, String smsCode) {
|
||||
String key = SmsCodeUtils.cacheKey(tenantId, phonenumber);
|
||||
RLock lock = redisClient.getLock(SmsCodeUtils.verificationLockKey(tenantId, phonenumber));
|
||||
boolean locked = false;
|
||||
try {
|
||||
locked = lock.tryLock(2, 5, TimeUnit.SECONDS);
|
||||
if (!locked) {
|
||||
throw new UserException("验证码校验繁忙,请稍后重试");
|
||||
}
|
||||
RBucket<String> bucket = redisClient.getBucket(key);
|
||||
String code = bucket.get();
|
||||
if (StringUtils.isBlank(code)) {
|
||||
loginService.recordLogininfor(tenantId, phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
if (!code.equals(smsCode)) {
|
||||
return false;
|
||||
}
|
||||
bucket.delete();
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new UserException("验证码校验被中断,请稍后重试");
|
||||
} finally {
|
||||
if (locked && lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isAppClient(SysClientVo client) {
|
||||
return client != null && SmsCodeUtils.MOBILE_CLIENT_ID.equals(client.getClientId());
|
||||
}
|
||||
|
||||
SysUserVo loadOrRegisterUserByPhonenumber(String tenantId, String phonenumber, boolean appClient) {
|
||||
if (appClient && userMapper.exists(new LambdaQueryWrapper<SysUser>()
|
||||
.eq(SysUser::getTenantId, tenantId)
|
||||
.eq(SysUser::getPhonenumber, phonenumber));
|
||||
.eq(SysUser::getPhonenumber, phonenumber)
|
||||
.and(wrapper -> wrapper.isNull(SysUser::getUserType)
|
||||
.or()
|
||||
.ne(SysUser::getUserType, UserType.APP_USER.getUserType())))) {
|
||||
log.warn("移动端短信登录拒绝同手机号系统用户:{}", phonenumber);
|
||||
throw new UserException("移动端账号未开通,请联系管理员");
|
||||
}
|
||||
LambdaQueryWrapper<SysUser> query = new LambdaQueryWrapper<SysUser>()
|
||||
.eq(SysUser::getTenantId, tenantId)
|
||||
.eq(SysUser::getPhonenumber, phonenumber);
|
||||
if (appClient) {
|
||||
query.eq(SysUser::getUserType, UserType.APP_USER.getUserType());
|
||||
}
|
||||
SysUserVo user = userMapper.selectVoOne(query);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
SysUser sysUser = new SysUser();
|
||||
sysUser.setTenantId(tenantId);
|
||||
@@ -108,6 +156,9 @@ public class SmsAuthStrategy implements IAuthStrategy {
|
||||
log.info("登录用户:{} 已被停用.", phonenumber);
|
||||
throw new UserException("user.blocked", phonenumber);
|
||||
}
|
||||
if (appClient && (user == null || !UserType.APP_USER.getUserType().equals(user.getUserType()))) {
|
||||
throw new UserException("移动端账号未开通,请联系管理员");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package org.dromara.web.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.SmsCodeUtils;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.redisson.api.RBucket;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@Tag("dev")
|
||||
class MobileSmsLoginTenantBoundaryTest {
|
||||
|
||||
@Test
|
||||
void mobileLoginIgnoresClientTenantSelection() throws Exception {
|
||||
AuthController.MobileSmsLoginBody body = new ObjectMapper().readValue("""
|
||||
{"phonenumber":"13900000000","smsCode":"123456","tenantId":"tenant-b"}
|
||||
""", AuthController.MobileSmsLoginBody.class);
|
||||
|
||||
assertEquals("13900000000", body.phonenumber());
|
||||
assertEquals("123456", body.smsCode());
|
||||
assertEquals("000000", AuthController.mobileTenantId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void resendingSmsUsesTheSameLockAsVerification() throws Exception {
|
||||
RedissonClient redisClient = mock(RedissonClient.class);
|
||||
RLock lock = mock(RLock.class);
|
||||
RBucket<String> bucket = mock(RBucket.class);
|
||||
String tenantId = "000000";
|
||||
String phonenumber = "13900000000";
|
||||
when(redisClient.getLock(SmsCodeUtils.verificationLockKey(tenantId, phonenumber))).thenReturn(lock);
|
||||
when(redisClient.<String>getBucket(anyString())).thenReturn(bucket);
|
||||
when(lock.tryLock(2, 5, TimeUnit.SECONDS)).thenReturn(true);
|
||||
when(lock.isHeldByCurrentThread()).thenReturn(true);
|
||||
|
||||
CaptchaController.storeSmsCode(redisClient, tenantId, phonenumber, "123456");
|
||||
|
||||
verify(bucket).set("123456", Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
verify(lock).unlock();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mobileLoginResponseUsesTheH5AuthContractOnly() throws Exception {
|
||||
LoginVo loginVo = new LoginVo();
|
||||
loginVo.setAccessToken("test-access-token");
|
||||
loginVo.setClientId("test-mobile-client");
|
||||
loginVo.setRefreshToken("must-not-be-returned");
|
||||
|
||||
JsonNode json = new ObjectMapper().readTree(new ObjectMapper().writeValueAsString(
|
||||
AuthController.MobileSmsLoginResponse.from(loginVo)
|
||||
));
|
||||
|
||||
assertEquals("test-access-token", json.path("access_token").asText());
|
||||
assertEquals("test-mobile-client", json.path("client_id").asText());
|
||||
assertFalse(json.has("refresh_token"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mobileLoginResponseRejectsAnIncompleteStrategyResult() {
|
||||
LoginVo loginVo = new LoginVo();
|
||||
loginVo.setClientId("test-mobile-client");
|
||||
|
||||
assertThrows(ServiceException.class, () -> AuthController.MobileSmsLoginResponse.from(loginVo));
|
||||
}
|
||||
}
|
||||
+86
-12
@@ -1,25 +1,99 @@
|
||||
package org.dromara.web.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import org.dromara.common.core.enums.UserType;
|
||||
import org.dromara.common.core.utils.SmsCodeUtils;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.dromara.system.domain.SysUser;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.domain.vo.SysUserVo;
|
||||
import org.dromara.system.mapper.SysUserMapper;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.redisson.api.RBucket;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@Tag("dev")
|
||||
class SmsAuthStrategyTenantTest {
|
||||
|
||||
@Test
|
||||
void mobileSmsUserLookupKeepsTenantBoundary() throws Exception {
|
||||
Path source = Path.of("src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java");
|
||||
if (!Files.exists(source)) {
|
||||
source = Path.of("ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java");
|
||||
}
|
||||
String code = Files.readString(source);
|
||||
private static final String TEST_MOBILE = "13900000000";
|
||||
|
||||
assertTrue(code.contains(".eq(SysUser::getTenantId, tenantId)"));
|
||||
assertTrue(code.contains(".eq(SysUser::getPhonenumber, phonenumber)"));
|
||||
@BeforeAll
|
||||
static void initializeSysUserLambdaMetadata() {
|
||||
if (TableInfoHelper.getTableInfo(SysUser.class) == null) {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), SysUser.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void mobileSmsLoginUsesAppUsersAndRejectsAConflictingSystemUser() {
|
||||
SysLoginService loginService = mock(SysLoginService.class);
|
||||
SysUserMapper userMapper = mock(SysUserMapper.class);
|
||||
SmsAuthStrategy strategy = new SmsAuthStrategy(loginService, userMapper);
|
||||
when(userMapper.selectVoOne(any())).thenReturn(null);
|
||||
when(userMapper.exists(any())).thenReturn(true);
|
||||
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> strategy.loadOrRegisterUserByPhonenumber("000000", TEST_MOBILE, true));
|
||||
verify(userMapper, never()).insert(any(SysUser.class));
|
||||
|
||||
SysClientVo appClient = new SysClientVo();
|
||||
appClient.setClientId(SmsCodeUtils.MOBILE_CLIENT_ID);
|
||||
assertTrue(SmsAuthStrategy.isAppClient(appClient));
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingAppUserIsRetainedAndWrongOtpDoesNotConsumeTheCode() throws Exception {
|
||||
SysLoginService loginService = mock(SysLoginService.class);
|
||||
SysUserMapper userMapper = mock(SysUserMapper.class);
|
||||
SmsAuthStrategy strategy = new SmsAuthStrategy(loginService, userMapper);
|
||||
SysUserVo appUser = new SysUserVo();
|
||||
appUser.setUserType(UserType.APP_USER.getUserType());
|
||||
appUser.setStatus("0");
|
||||
when(userMapper.selectVoOne(any())).thenReturn(appUser);
|
||||
assertSame(appUser, strategy.loadOrRegisterUserByPhonenumber("000000", TEST_MOBILE, true));
|
||||
|
||||
RedissonClient redisClient = mock(RedissonClient.class);
|
||||
RLock lock = mock(RLock.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
RBucket<String> bucket = mock(RBucket.class);
|
||||
when(redisClient.getLock(SmsCodeUtils.verificationLockKey("000000", TEST_MOBILE))).thenReturn(lock);
|
||||
when(redisClient.<String>getBucket(anyString())).thenReturn(bucket);
|
||||
when(lock.tryLock(eq(2L), eq(5L), eq(TimeUnit.SECONDS))).thenReturn(true);
|
||||
when(lock.isHeldByCurrentThread()).thenReturn(true);
|
||||
when(bucket.get()).thenReturn("123456");
|
||||
|
||||
assertFalse(strategy.validateSmsCode(redisClient, "000000", TEST_MOBILE, "000000"));
|
||||
verify(bucket, never()).delete();
|
||||
verify(lock).unlock();
|
||||
|
||||
assertTrue(strategy.validateSmsCode(redisClient, "000000", TEST_MOBILE, "123456"));
|
||||
verify(bucket).delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mobileSmsLoginRejectsAConflictingSystemUserEvenWhenAnAppUserExists() {
|
||||
SysLoginService loginService = mock(SysLoginService.class);
|
||||
SysUserMapper userMapper = mock(SysUserMapper.class);
|
||||
SmsAuthStrategy strategy = new SmsAuthStrategy(loginService, userMapper);
|
||||
SysUserVo appUser = new SysUserVo();
|
||||
appUser.setUserType(UserType.APP_USER.getUserType());
|
||||
when(userMapper.exists(any())).thenReturn(true);
|
||||
when(userMapper.selectVoOne(any())).thenReturn(appUser);
|
||||
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> strategy.loadOrRegisterUserByPhonenumber("000000", TEST_MOBILE, true));
|
||||
verify(userMapper, never()).selectVoOne(any());
|
||||
verify(userMapper, never()).insert(any(SysUser.class));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user