feat(open): 重构C端认证并支持密码重置
This commit is contained in:
@@ -5,6 +5,9 @@ import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 移动端用户实体
|
||||
*/
|
||||
@@ -47,6 +50,31 @@ public class ClientUser extends BaseEntity {
|
||||
*/
|
||||
private String wechatOpenid;
|
||||
|
||||
/**
|
||||
* 微信 unionid(同一微信开放平台下唯一标识,用于跨小程序/公众号识别同一用户)
|
||||
*/
|
||||
private String unionid;
|
||||
|
||||
/**
|
||||
* 微信小程序 session_key(用于解密手机号、数据校验,登录态刷新时更新)
|
||||
*/
|
||||
private String sessionKey;
|
||||
|
||||
/**
|
||||
* 注册来源(1-手机号 2-微信小程序)
|
||||
*/
|
||||
private Integer registerSource;
|
||||
|
||||
/**
|
||||
* 生日
|
||||
*/
|
||||
private LocalDate birthday;
|
||||
|
||||
/**
|
||||
* 最后登录时间
|
||||
*/
|
||||
private LocalDateTime lastLoginTime;
|
||||
|
||||
/**
|
||||
* 状态((1-正常 0-禁用)
|
||||
*/
|
||||
|
||||
@@ -37,7 +37,7 @@ import java.util.concurrent.Executor;
|
||||
@Slf4j
|
||||
public class LogAspect {
|
||||
|
||||
private static final int MAX_ERROR_MSG_LENGTH = 1000;
|
||||
private static final int MAX_ERROR_MSG_LENGTH = 2000;
|
||||
|
||||
private final LogService logService;
|
||||
private final Executor operationLogExecutor;
|
||||
|
||||
@@ -52,8 +52,8 @@ public interface RedisConstants {
|
||||
*/
|
||||
interface Captcha {
|
||||
String IMAGE_CODE = "captcha:image:{}"; // 图形验证码
|
||||
String SMS_LOGIN_CODE = "captcha:sms_login:{}"; // 登录短信验证码
|
||||
String SMS_REGISTER_CODE = "captcha:sms_register:{}";// 注册短信验证码
|
||||
String SMS_LOGIN_CODE = "captcha:sms_login:{}"; // 登录/注册通用短信验证码
|
||||
String SMS_RESET_CODE = "captcha:sms_reset:{}"; // 重置密码短信验证码
|
||||
String MOBILE_CODE = "captcha:mobile:{}"; // 绑定、更换手机验证码
|
||||
String EMAIL_CODE = "captcha:email:{}"; // 邮箱验证码
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package com.youlai.boot.open.controller;
|
||||
|
||||
import com.youlai.boot.open.model.req.OpenLoginReq;
|
||||
import com.youlai.boot.open.model.req.OpenRegisterReq;
|
||||
import com.youlai.boot.open.model.req.OpenPasswordLoginReq;
|
||||
import com.youlai.boot.open.model.req.OpenResetPasswordReq;
|
||||
import com.youlai.boot.open.service.OpenAuthService;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
@@ -30,28 +31,16 @@ public class OpenAuthController {
|
||||
private final OpenAuthService openAuthService;
|
||||
|
||||
|
||||
@Operation(summary = "手机号注册")
|
||||
@PostMapping("/register/mobile")
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.REGISTER)
|
||||
public Result<AuthenticationToken> registerByMobile(@RequestBody @Valid OpenRegisterReq request) {
|
||||
AuthenticationToken authenticationToken = openAuthService.registerByMobile(
|
||||
request.getMobile(),
|
||||
request.getCode(),
|
||||
request.getPassword(),
|
||||
request.getNickname()
|
||||
);
|
||||
return Result.success(authenticationToken);
|
||||
}
|
||||
|
||||
@Operation(summary = "发送注册短信验证码")
|
||||
@PostMapping("/register/sms/code")
|
||||
public Result<Void> sendRegisterSmsCode(
|
||||
@Operation(summary = "发送短信验证码(登录/注册通用)")
|
||||
@PostMapping("/sms/code")
|
||||
public Result<Void> sendSmsCode(
|
||||
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile
|
||||
) {
|
||||
return Result.judge(openAuthService.sendRegisterSmsCode(mobile));
|
||||
openAuthService.sendLoginSmsCode(mobile);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "手机号验证码登录")
|
||||
@Operation(summary = "手机号验证码登录/注册")
|
||||
@PostMapping("/login/mobile")
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
|
||||
public Result<AuthenticationToken> loginByMobile(@RequestBody @Valid OpenLoginReq request) {
|
||||
@@ -59,5 +48,56 @@ public class OpenAuthController {
|
||||
return Result.success(authenticationToken);
|
||||
}
|
||||
|
||||
@Operation(summary = "账号密码登录")
|
||||
@PostMapping("/login")
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
|
||||
public Result<AuthenticationToken> login(@RequestBody @Valid OpenPasswordLoginReq request) {
|
||||
AuthenticationToken authenticationToken = openAuthService.login(request.getUsername(), request.getPassword());
|
||||
return Result.success(authenticationToken);
|
||||
}
|
||||
|
||||
@Operation(summary = "发送登录短信验证码")
|
||||
@PostMapping("/login/sms/code")
|
||||
public Result<Void> sendLoginSmsCode(
|
||||
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile
|
||||
) {
|
||||
openAuthService.sendLoginSmsCode(mobile);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "发送重置密码短信验证码")
|
||||
@PostMapping("/reset-password/sms/code")
|
||||
public Result<Void> sendResetPwdSmsCode(
|
||||
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile
|
||||
) {
|
||||
return Result.judge(openAuthService.sendResetPwdSmsCode(mobile));
|
||||
}
|
||||
|
||||
@Operation(summary = "重置密码")
|
||||
@PostMapping("/reset-password")
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.RESET_PASSWORD)
|
||||
public Result<Void> resetPassword(@RequestBody @Valid OpenResetPasswordReq request) {
|
||||
openAuthService.resetPassword(request.getMobile(), request.getCode(), request.getPassword());
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "刷新令牌")
|
||||
@PostMapping("/refresh-token")
|
||||
public Result<AuthenticationToken> refreshToken(
|
||||
@Parameter(description = "刷新令牌") @RequestParam String refreshToken
|
||||
) {
|
||||
return Result.success(openAuthService.refreshToken(refreshToken));
|
||||
}
|
||||
|
||||
@Operation(summary = "退出登录")
|
||||
@PostMapping("/logout")
|
||||
public Result<Void> logout(
|
||||
@Parameter(description = "访问令牌(可选,Bearer 前缀可省略)", example = "eyJhbGciOiJIUzI1NiJ9...")
|
||||
@RequestParam(required = false) String accessToken
|
||||
) {
|
||||
openAuthService.logout(accessToken);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.youlai.boot.system.model.vo.UserPageVO;
|
||||
import com.youlai.boot.system.model.vo.UserProfileVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -46,6 +47,11 @@ public interface ClientUserMapper extends BaseMapper<ClientUser> {
|
||||
* @param username 用户名
|
||||
* @return 认证信息
|
||||
*/
|
||||
@Select("""
|
||||
SELECT id AS userId, username, nickname, password, status, avatar, mobile
|
||||
FROM app_user
|
||||
WHERE username = #{username} AND is_deleted = 0
|
||||
""")
|
||||
SecurityUser getAuthInfoByUsername(String username);
|
||||
|
||||
default SecurityUser getAuthCredentialsByUsername(String username) {
|
||||
@@ -58,6 +64,11 @@ public interface ClientUserMapper extends BaseMapper<ClientUser> {
|
||||
* @param mobile 手机号
|
||||
* @return 认证信息
|
||||
*/
|
||||
@Select("""
|
||||
SELECT id AS userId, username, nickname, password, status, avatar, mobile
|
||||
FROM app_user
|
||||
WHERE mobile = #{mobile} AND is_deleted = 0
|
||||
""")
|
||||
SecurityUser getAuthInfoByMobile(String mobile);
|
||||
|
||||
default SecurityUser getAuthCredentialsByMobile(String mobile) {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.youlai.boot.open.model.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 账号密码登录请求参数
|
||||
*
|
||||
*/
|
||||
@Schema(description = "账号密码登录请求参数")
|
||||
@Data
|
||||
public class OpenPasswordLoginReq {
|
||||
|
||||
@Schema(description = "用户名 / 手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18888888888")
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.youlai.boot.open.model.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
/**
|
||||
* 手机号注册请求参数
|
||||
*
|
||||
*/
|
||||
@Schema(description = "手机号注册请求参数")
|
||||
@Data
|
||||
public class OpenRegisterReq {
|
||||
|
||||
@Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18888888888")
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String mobile;
|
||||
|
||||
@Schema(description = "验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "昵称", example = "用户昵称")
|
||||
private String nickname;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.youlai.boot.open.model.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 重置密码请求参数
|
||||
*
|
||||
* @author TongTong Studio
|
||||
*/
|
||||
@Schema(description = "重置密码请求参数")
|
||||
@Data
|
||||
public class OpenResetPasswordReq {
|
||||
|
||||
@Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18888888888")
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String mobile;
|
||||
|
||||
@Schema(description = "验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "新密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
@Size(min = 6, max = 32, message = "密码长度需在 6-32 位之间")
|
||||
private String password;
|
||||
}
|
||||
@@ -11,25 +11,7 @@ public interface OpenAuthService {
|
||||
CaptchaInfo getCaptcha();
|
||||
|
||||
/**
|
||||
* 发送注册短信验证码
|
||||
*
|
||||
* @param mobile 手机号
|
||||
*/
|
||||
boolean sendRegisterSmsCode(String mobile);
|
||||
|
||||
/**
|
||||
* 手机号注册
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @param code 验证码
|
||||
* @param password 密码
|
||||
* @param nickname 昵称
|
||||
* @return 认证令牌
|
||||
*/
|
||||
AuthenticationToken registerByMobile(String mobile, String code, String password, String nickname);
|
||||
|
||||
/**
|
||||
* 发送登录短信验证码
|
||||
* 发送登录/注册短信验证码(用户存在则用于登录,不存在则用于注册)
|
||||
*
|
||||
* @param mobile 手机号
|
||||
*/
|
||||
@@ -53,11 +35,28 @@ public interface OpenAuthService {
|
||||
*/
|
||||
AuthenticationToken login(String username, String password);
|
||||
|
||||
/**
|
||||
* 发送重置密码短信验证码
|
||||
*
|
||||
* @param mobile 手机号
|
||||
*/
|
||||
boolean sendResetPwdSmsCode(String mobile);
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @param code 短信验证码
|
||||
* @param password 新密码
|
||||
*/
|
||||
void resetPassword(String mobile, String code, String password);
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*
|
||||
* @param accessToken 访问令牌(可选,为空时仅清理当前上下文)
|
||||
*/
|
||||
void logout();
|
||||
void logout(String accessToken);
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
package com.youlai.boot.open.service.impl;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.client.model.entity.ClientUser;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.youlai.boot.open.mapper.ClientUserMapper;
|
||||
import com.youlai.boot.open.service.OpenAuthService;
|
||||
import com.youlai.boot.client.service.ClientUserService;
|
||||
import com.youlai.boot.auth.model.resp.CaptchaInfo;
|
||||
import com.youlai.boot.auth.service.CaptchaService;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.util.CodeGeneratorUtil;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.support.sms.SmsService;
|
||||
import com.youlai.boot.support.sms.SmsResult;
|
||||
import com.youlai.boot.support.sms.SmsTypeEnum;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -31,165 +31,246 @@ import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 认证服务实现类
|
||||
* 开放式客户端认证服务实现类
|
||||
*
|
||||
* <p>所有 C 端用户(client / app_user)登录均在此模块内完成认证与令牌签发,
|
||||
* 不依赖后台管理端(sys_user)的全局认证链,实现账号体系分域隔离。</p>
|
||||
*
|
||||
* @author TongTong Studio
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class OpenAuthServiceImpl implements OpenAuthService {
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final TokenManager tokenManager;
|
||||
private final SmsService smsService;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final CaptchaService captchaService;
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final ClientUserService userService;
|
||||
private final ClientUserMapper clientUserMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
|
||||
/**
|
||||
* 获取验证码(C 端登录无需图形验证码,预留接口返回 null)
|
||||
*/
|
||||
@Override
|
||||
public CaptchaInfo getCaptcha() {
|
||||
public com.youlai.boot.auth.model.resp.CaptchaInfo getCaptcha() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送注册短信验证码
|
||||
* 发送短信验证码(登录/注册通用,阿里云)
|
||||
*
|
||||
* @param mobile 手机号
|
||||
*/
|
||||
@Override
|
||||
public boolean sendRegisterSmsCode(String mobile) {
|
||||
// 检查手机号是否已注册
|
||||
long count = userService.count(new LambdaQueryWrapper<ClientUser>()
|
||||
.eq(ClientUser::getMobile, mobile));
|
||||
if (count > 0) {
|
||||
throw new BusinessException("该手机号已被注册");
|
||||
}
|
||||
|
||||
public void sendLoginSmsCode(String mobile) {
|
||||
// 生成 6 位数字验证码
|
||||
String code = CodeGeneratorUtil.generateNumericCode(6);
|
||||
|
||||
// 发送短信验证码
|
||||
Map<String, String> templateParams = new HashMap<>();
|
||||
templateParams.put("code", code);
|
||||
|
||||
boolean success = smsService.send(mobile, SmsTypeEnum.REGISTER, templateParams);
|
||||
if (success) {
|
||||
// 缓存验证码至Redis,用于注册校验
|
||||
redisTemplate.opsForValue().set(StrUtil.format(RedisConstants.Captcha.SMS_REGISTER_CODE, mobile), code, 5, TimeUnit.MINUTES);
|
||||
SmsResult result = smsService.sendWithResult(mobile, SmsTypeEnum.LOGIN, templateParams);
|
||||
if (result.isSuccess()) {
|
||||
// 缓存验证码至 Redis,5 分钟内有效,用于登录校验
|
||||
redisTemplate.opsForValue().set(
|
||||
StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile),
|
||||
code, 5, TimeUnit.MINUTES);
|
||||
} else {
|
||||
log.warn("短信发送失败,手机号: {}", mobile);
|
||||
log.warn("登录短信验证码发送失败,手机号: {}, 原因: {}", mobile, result.getMessage());
|
||||
throw new BusinessException(resolveSmsErrorMessage(result.getMessage()));
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号注册
|
||||
* 手机验证码登录
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @param code 验证码
|
||||
* @param password 密码
|
||||
* @param nickname 昵称
|
||||
* @param code 短信验证码
|
||||
* @return 认证令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken registerByMobile(String mobile, String code, String password, String nickname) {
|
||||
// 1. 校验验证码
|
||||
String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_REGISTER_CODE, mobile);
|
||||
public AuthenticationToken loginBySms(String mobile, String code) {
|
||||
String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile);
|
||||
String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey);
|
||||
|
||||
if (StrUtil.isBlank(cachedCode)) {
|
||||
throw new BusinessException("验证码已过期");
|
||||
}
|
||||
|
||||
if (!Objects.equals(code, cachedCode)) {
|
||||
throw new BusinessException("验证码错误");
|
||||
}
|
||||
|
||||
// 2. 检查手机号是否已注册
|
||||
SecurityUser securityUser = clientUserMapper.getAuthInfoByMobile(mobile);
|
||||
if (securityUser == null) {
|
||||
// 用户不存在则自动注册(验证码登录/注册合一)
|
||||
ClientUser newUser = new ClientUser();
|
||||
newUser.setMobile(mobile);
|
||||
newUser.setUsername(mobile);
|
||||
String nickname = mobile.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
|
||||
newUser.setNickname(nickname);
|
||||
// 验证码登录场景下用户未设置密码,生成随机密码,后续可在重置密码中修改
|
||||
newUser.setPassword(passwordEncoder.encode(IdUtil.fastSimpleUUID()));
|
||||
newUser.setStatus(1);
|
||||
newUser.setRegisterSource(1);
|
||||
userService.save(newUser);
|
||||
|
||||
securityUser = clientUserMapper.getAuthInfoByMobile(mobile);
|
||||
}
|
||||
if (!Objects.equals(securityUser.getStatus(), 1)) {
|
||||
throw new BusinessException("账号已被禁用");
|
||||
}
|
||||
|
||||
// 登录成功,删除验证码
|
||||
redisTemplate.delete(cacheKey);
|
||||
|
||||
// 更新最后登录时间
|
||||
ClientUser update = new ClientUser();
|
||||
update.setId(securityUser.getUserId());
|
||||
update.setLastLoginTime(java.time.LocalDateTime.now());
|
||||
userService.updateById(update);
|
||||
|
||||
return generateToken(securityUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号密码登录
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param password 明文密码
|
||||
* @return 认证令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken login(String username, String password) {
|
||||
SecurityUser securityUser = clientUserMapper.getAuthInfoByUsername(username);
|
||||
if (securityUser == null) {
|
||||
throw new BusinessException("用户名或密码错误");
|
||||
}
|
||||
if (!Objects.equals(securityUser.getStatus(), 1)) {
|
||||
throw new BusinessException("账号已被禁用");
|
||||
}
|
||||
if (StrUtil.isBlank(securityUser.getPassword())
|
||||
|| !passwordEncoder.matches(password, securityUser.getPassword())) {
|
||||
throw new BusinessException("用户名或密码错误");
|
||||
}
|
||||
return generateToken(securityUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送重置密码短信验证码
|
||||
*
|
||||
* @param mobile 手机号
|
||||
*/
|
||||
@Override
|
||||
public boolean sendResetPwdSmsCode(String mobile) {
|
||||
// 检查手机号是否已注册(未注册用户无需重置密码)
|
||||
long count = userService.count(new LambdaQueryWrapper<ClientUser>()
|
||||
.eq(ClientUser::getMobile, mobile));
|
||||
Assert.isTrue(count == 0, "该手机号已被注册");
|
||||
if (count == 0) {
|
||||
throw new BusinessException("该手机号尚未注册");
|
||||
}
|
||||
|
||||
// 3. 创建新用户
|
||||
ClientUser user = new ClientUser();
|
||||
user.setUsername(mobile); // 使用手机号作为用户名
|
||||
user.setMobile(mobile);
|
||||
user.setPassword(passwordEncoder.encode(password));
|
||||
user.setNickname(StrUtil.isNotBlank(nickname) ? nickname : mobile.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"));
|
||||
user.setStatus(1); // 正常状态
|
||||
String codeStr = CodeGeneratorUtil.generateNumericCode(6);
|
||||
|
||||
boolean saveResult = userService.save(user);
|
||||
if (!saveResult) {
|
||||
throw new BusinessException("注册失败");
|
||||
Map<String, String> templateParams = new HashMap<>();
|
||||
templateParams.put("code", codeStr);
|
||||
|
||||
boolean success = smsService.send(mobile, SmsTypeEnum.RESET_PASSWORD, templateParams);
|
||||
if (success) {
|
||||
// 缓存验证码至Redis,用于重置密码校验
|
||||
redisTemplate.opsForValue().set(
|
||||
StrUtil.format(RedisConstants.Captcha.SMS_RESET_CODE, mobile),
|
||||
codeStr, 5, TimeUnit.MINUTES);
|
||||
} else {
|
||||
log.warn("重置密码短信发送失败,手机号: {}", mobile);
|
||||
throw new BusinessException("短信验证码发送失败,请稍后重试");
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @param code 短信验证码
|
||||
* @param password 新密码
|
||||
*/
|
||||
@Override
|
||||
public void resetPassword(String mobile, String code, String password) {
|
||||
// 1. 校验验证码
|
||||
String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_RESET_CODE, mobile);
|
||||
String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey);
|
||||
|
||||
if (StrUtil.isBlank(cachedCode)) {
|
||||
throw new BusinessException("验证码已过期");
|
||||
}
|
||||
if (!Objects.equals(code, cachedCode)) {
|
||||
throw new BusinessException("验证码错误");
|
||||
}
|
||||
|
||||
// 2. 查询用户
|
||||
ClientUser user = userService.getOne(new LambdaQueryWrapper<ClientUser>()
|
||||
.eq(ClientUser::getMobile, mobile)
|
||||
.last("LIMIT 1"));
|
||||
if (user == null) {
|
||||
throw new BusinessException("该手机号尚未注册");
|
||||
}
|
||||
|
||||
// 3. 更新密码
|
||||
ClientUser update = new ClientUser();
|
||||
update.setId(user.getId());
|
||||
update.setPassword(passwordEncoder.encode(password));
|
||||
boolean updated = userService.updateById(update);
|
||||
if (!updated) {
|
||||
throw new BusinessException("重置密码失败");
|
||||
}
|
||||
|
||||
// 4. 删除验证码
|
||||
redisTemplate.delete(cacheKey);
|
||||
|
||||
// 5. 自动登录并生成token
|
||||
UsernamePasswordAuthenticationToken authenticationToken =
|
||||
new UsernamePasswordAuthenticationToken(mobile, password);
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
|
||||
AuthenticationToken authenticationTokenResponse =
|
||||
tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
return authenticationTokenResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendLoginSmsCode(String mobile) {
|
||||
String code = "1234";
|
||||
|
||||
Map<String, String> templateParams = new HashMap<>();
|
||||
templateParams.put("code", code);
|
||||
|
||||
boolean success = false;
|
||||
|
||||
// 方式1: 使用阿里云短信(默认)
|
||||
// try {
|
||||
// success = aliyunSmsService.sendSms(mobile, SmsTypeEnum.LOGIN, templateParams);
|
||||
// log.info("阿里云短信发送结果: {}", success ? "成功" : "失败");
|
||||
// } catch (Exception e) {
|
||||
// log.error("阿里云短信发送异常", e);
|
||||
// }
|
||||
|
||||
// 方式2: 使用腾讯云短信(需要时取消下面注释,并注释掉上面的阿里云代码)
|
||||
try {
|
||||
success = smsService.send(mobile, SmsTypeEnum.LOGIN, templateParams);
|
||||
log.info("腾讯云短信发送结果: {}", success ? "成功" : "失败");
|
||||
} catch (Exception e) {
|
||||
log.error("腾讯云短信发送异常", e);
|
||||
public void logout(String accessToken) {
|
||||
if (StrUtil.isNotBlank(accessToken)) {
|
||||
tokenManager.invalidateToken(accessToken);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
redisTemplate.opsForValue().set(StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile), code, 5, TimeUnit.MINUTES);
|
||||
} else {
|
||||
log.warn("短信发送失败,手机号: {}", mobile);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationToken loginBySms(String mobile, String code) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationToken login(String username, String password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationToken refreshToken(String refreshToken) {
|
||||
return null;
|
||||
return tokenManager.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 SecurityUser 构造 Authentication 并签发令牌(C 端用户本地认证)
|
||||
*/
|
||||
private AuthenticationToken generateToken(SecurityUser securityUser) {
|
||||
SecurityUserDetails userDetails = new SecurityUserDetails(securityUser);
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
||||
userDetails, null, userDetails.getAuthorities());
|
||||
return tokenManager.generateToken(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将短信供应商返回的错误信息转换为用户友好提示。
|
||||
* <p>重点识别流控、触发频次等常见限制,无法识别时透传原始信息。</p>
|
||||
*/
|
||||
private String resolveSmsErrorMessage(String message) {
|
||||
if (StrUtil.isBlank(message)) {
|
||||
return "短信验证码发送失败,请稍后重试";
|
||||
}
|
||||
// 触发流控 / 发送频率限制
|
||||
if (message.contains("流控") || message.contains("BUSINESS_LIMIT_CONTROL")
|
||||
|| message.contains("限流") || message.contains("频率") || message.contains("过于频繁")) {
|
||||
return "操作过于频繁,请稍后再试";
|
||||
}
|
||||
// 触发验证码发送次数上限
|
||||
if (message.contains("触发") && message.contains("Permits")) {
|
||||
return "验证码发送次数已达上限,请稍后再试";
|
||||
}
|
||||
// 其它情况透传原始错误信息,便于前端直接展示
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
33
src/main/java/com/youlai/boot/support/sms/SmsResult.java
Normal file
33
src/main/java/com/youlai/boot/support/sms/SmsResult.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.youlai.boot.support.sms;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 短信发送结果
|
||||
* <p>携带发送是否成功以及供应商返回的具体错误信息,便于上层透传给调用方。</p>
|
||||
*
|
||||
* @author TongTong Studio
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class SmsResult {
|
||||
|
||||
/**
|
||||
* 是否发送成功
|
||||
*/
|
||||
private final boolean success;
|
||||
|
||||
/**
|
||||
* 供应商返回的具体信息(失败时为错误描述,如流控提示)
|
||||
*/
|
||||
private final String message;
|
||||
|
||||
public static SmsResult ok() {
|
||||
return new SmsResult(true, null);
|
||||
}
|
||||
|
||||
public static SmsResult fail(String message) {
|
||||
return new SmsResult(false, message);
|
||||
}
|
||||
}
|
||||
@@ -22,4 +22,14 @@ public interface SmsService {
|
||||
*/
|
||||
boolean send(String mobile, SmsTypeEnum smsType, Map<String, String> templateParams);
|
||||
|
||||
/**
|
||||
* 发送短信并返回带错误信息的结果。
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @param smsType 短信类型,对应 {@code sms.templates.*} 配置中的模板
|
||||
* @param templateParams 模板参数,用于替换短信模板中的变量
|
||||
* @return 发送结果(包含成功状态与供应商返回的具体信息)
|
||||
*/
|
||||
SmsResult sendWithResult(String mobile, SmsTypeEnum smsType, Map<String, String> templateParams);
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,12 @@ public enum SmsTypeEnum implements IBaseEnum<String> {
|
||||
/**
|
||||
* 修改手机号短信验证码
|
||||
*/
|
||||
CHANGE_MOBILE("change-mobile", "修改手机号短信验证码");
|
||||
CHANGE_MOBILE("change-mobile", "修改手机号短信验证码"),
|
||||
|
||||
/**
|
||||
* 重置密码短信验证码
|
||||
*/
|
||||
RESET_PASSWORD("reset-password", "重置密码短信验证码");
|
||||
|
||||
private final String value;
|
||||
private final String label;
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.aliyuncs.IAcsClient;
|
||||
import com.aliyuncs.exceptions.ClientException;
|
||||
import com.aliyuncs.http.MethodType;
|
||||
import com.aliyuncs.profile.DefaultProfile;
|
||||
import com.youlai.boot.support.sms.SmsResult;
|
||||
import com.youlai.boot.support.sms.SmsTypeEnum;
|
||||
import com.youlai.boot.config.property.AliyunSmsProperties;
|
||||
import com.youlai.boot.support.sms.SmsService;
|
||||
@@ -36,6 +37,11 @@ public class AliyunSmsServiceImpl implements SmsService {
|
||||
|
||||
@Override
|
||||
public boolean send(String mobile, SmsTypeEnum smsType, Map<String, String> templateParams) {
|
||||
return sendWithResult(mobile, smsType, templateParams).isSuccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmsResult sendWithResult(String mobile, SmsTypeEnum smsType, Map<String, String> templateParams) {
|
||||
// 根据 smsType 从配置中获取对应的阿里云短信模板编码
|
||||
String templateCode = aliyunSmsProperties.getTemplates().get(smsType.getValue());
|
||||
|
||||
@@ -67,13 +73,13 @@ public class AliyunSmsServiceImpl implements SmsService {
|
||||
boolean success = "OK".equals(code);
|
||||
if (!success) {
|
||||
log.error("阿里云短信发送失败,手机号: {}, Code: {}, Message: {}", mobile, code, message);
|
||||
} else {
|
||||
log.info("阿里云短信发送成功,手机号: {}", mobile);
|
||||
return SmsResult.fail(message);
|
||||
}
|
||||
return success;
|
||||
log.info("阿里云短信发送成功,手机号: {}", mobile);
|
||||
return SmsResult.ok();
|
||||
} catch (ClientException e) {
|
||||
log.error("阿里云短信发送异常,手机号: {}, 错误信息: {}", mobile, e.getMessage(), e);
|
||||
return SmsResult.fail(e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user