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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user