refactor: 发送验证码代码重构优化;扩展Spring Security 支持短信验证码;

This commit is contained in:
Ray.Hao
2025-01-13 18:14:52 +08:00
parent b107bb5315
commit 4ecb25147f
39 changed files with 457 additions and 362 deletions

View File

@@ -9,40 +9,32 @@ package com.youlai.boot.common.constant;
public interface RedisConstants { public interface RedisConstants {
/** /**
* 系统配置Redis-key * 系统配置 Redis
*/ */
String SYSTEM_CONFIG_KEY = "system:config"; String SYSTEM_CONFIG_KEY = "system:config";
/** /**
* IP限流Redis-key * IP 限流 Redis
*/ */
String IP_RATE_LIMITER_KEY = "ip:rate:limiter:"; String IP_RATE_LIMITER_KEY = "rate:limiter:ip:";
/** /**
* 防重复提交Redis-key * 防重复提交 Redis 键前缀
*/ */
String RESUBMIT_LOCK_PREFIX = "resubmit:lock:"; String RESUBMIT_LOCK_PREFIX = "lock:resubmit:";
/** /**
* 单个IP请求的最大每秒查询数QPS阈值Key * 登录手机验证码 Redis 键前缀
*/ */
String IP_QPS_THRESHOLD_LIMIT_KEY = "IP_QPS_THRESHOLD_LIMIT"; String SMS_LOGIN_CODE_PREFIX= "code:sms:login:";
/** /**
* 手机验证码缓存前缀 * 绑定或更换手机验证码 Redis 键前缀
*/ */
String SMS_LOGIN_VERIFY_CODE_PREFIX = "sms_login:mobile:"; String SMS_CHANGE_CODE_PREFIX = "code:sms:change:";
/** /**
* 重置密码验证码缓存前缀 * 绑定或更换邮箱验证码 Redis 键前缀
*/ */
String EMAIL_CHANGE_CODE_PREFIX = "code:email:change:";
String SMS_RESET_PASSWORD_VERIFY_CODE_PREFIX = "sms_reset_password:mobile:";
/**
* 邮箱验证码缓存前缀
*/
String EMAIL_VERIFICATION_CODE_PREFIX = "VERIFICATION_CODE:EMAIL:";
} }

View File

@@ -3,7 +3,7 @@ package com.youlai.boot.common.constant;
/** /**
* 系统常量 * 系统常量
* *
* @author haoxr * @author Ray.Hao
* @since 1.0.0 * @since 1.0.0
*/ */
public interface SystemConstants { public interface SystemConstants {
@@ -24,5 +24,9 @@ public interface SystemConstants {
String ROOT_ROLE_CODE = "ROOT"; String ROOT_ROLE_CODE = "ROOT";
/**
* 系统配置 IP的QPS限流的KEY
*/
String SYSTEM_CONFIG_IP_QPS_LIMIT_KEY = "IP_QPS_THRESHOLD_LIMIT";
} }

View File

@@ -1,5 +1,6 @@
package com.youlai.boot.common.result; package com.youlai.boot.common.result;
import cn.hutool.core.util.StrUtil;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@@ -52,7 +53,7 @@ public class Result<T> implements Serializable {
} }
public static <T> Result<T> failed(IResultCode resultCode, String msg) { public static <T> Result<T> failed(IResultCode resultCode, String msg) {
return result(resultCode.getCode(), msg, null); return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), null);
} }
private static <T> Result<T> result(IResultCode resultCode, T data) { private static <T> Result<T> result(IResultCode resultCode, T data) {

View File

@@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets;
@Slf4j @Slf4j
public class ResponseUtils { public class ResponseUtils {
/** /**
* 异常消息返回(适用过滤器中处理异常响应) * 异常消息返回(适用过滤器中处理异常响应)
* *
@@ -28,12 +29,7 @@ public class ResponseUtils {
* @param resultCode 响应结果码 * @param resultCode 响应结果码
*/ */
public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {
// 根据不同的结果码设置HTTP状态 int status = getHttpStatus(resultCode);
int status = switch (resultCode) {
case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID , REFRESH_TOKEN_INVALID
-> HttpStatus.UNAUTHORIZED.value();
default -> HttpStatus.BAD_REQUEST.value();
};
response.setStatus(status); response.setStatus(status);
response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setContentType(MediaType.APPLICATION_JSON_VALUE);
@@ -48,4 +44,40 @@ public class ResponseUtils {
} }
} }
/**
* 异常消息返回(适用过滤器中处理异常响应)
*
* @param response HttpServletResponse
* @param resultCode 响应结果码
*/
public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) {
int status = getHttpStatus(resultCode);
response.setStatus(status);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
try (PrintWriter writer = response.getWriter()) {
String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message));
writer.print(jsonResponse);
writer.flush(); // 确保将响应内容写入到输出流
} catch (IOException e) {
log.error("响应异常处理失败", e);
}
}
/**
* 根据结果码获取HTTP状态码
*
* @param resultCode 结果码
* @return HTTP状态码
*/
private static int getHttpStatus(ResultCode resultCode) {
return switch (resultCode) {
case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
default -> HttpStatus.BAD_REQUEST.value();
};
}
} }

View File

@@ -7,7 +7,8 @@ import com.youlai.boot.config.property.SecurityProperties;
import com.youlai.boot.core.filter.RateLimiterFilter; import com.youlai.boot.core.filter.RateLimiterFilter;
import com.youlai.boot.core.security.exception.MyAccessDeniedHandler; import com.youlai.boot.core.security.exception.MyAccessDeniedHandler;
import com.youlai.boot.core.security.exception.MyAuthenticationEntryPoint; import com.youlai.boot.core.security.exception.MyAuthenticationEntryPoint;
import com.youlai.boot.core.security.extension.WechatAuthenticationProvider; import com.youlai.boot.core.security.extension.sms.SmsAuthenticationProvider;
import com.youlai.boot.core.security.extension.wechat.WechatAuthenticationProvider;
import com.youlai.boot.core.security.filter.CaptchaValidationFilter; import com.youlai.boot.core.security.filter.CaptchaValidationFilter;
import com.youlai.boot.core.security.filter.JwtAuthenticationFilter; import com.youlai.boot.core.security.filter.JwtAuthenticationFilter;
import com.youlai.boot.core.security.service.SysUserDetailsService; import com.youlai.boot.core.security.service.SysUserDetailsService;
@@ -131,6 +132,10 @@ public class SecurityConfig {
return new WechatAuthenticationProvider(userService, wxMaService); return new WechatAuthenticationProvider(userService, wxMaService);
} }
public SmsAuthenticationProvider smsAuthenticationProvider() {
return new SmsAuthenticationProvider(userService, redisTemplate);
}
/** /**
* 手动注入 AuthenticationManager支持多种认证方式 * 手动注入 AuthenticationManager支持多种认证方式
* - DaoAuthenticationProvider用户名密码认证 * - DaoAuthenticationProvider用户名密码认证
@@ -138,6 +143,10 @@ public class SecurityConfig {
*/ */
@Bean @Bean
public AuthenticationManager authenticationManager() { public AuthenticationManager authenticationManager() {
return new ProviderManager(daoAuthenticationProvider(), weChatAuthenticationProvider()); return new ProviderManager(
daoAuthenticationProvider(),
weChatAuthenticationProvider(),
smsAuthenticationProvider()
);
} }
} }

View File

@@ -2,6 +2,7 @@ package com.youlai.boot.core.filter;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SystemConstants;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.util.IPUtils; import com.youlai.boot.common.util.IPUtils;
import com.youlai.boot.common.util.ResponseUtils; import com.youlai.boot.common.util.ResponseUtils;
@@ -49,13 +50,13 @@ public class RateLimiterFilter extends OncePerRequestFilter {
if (count == null || count == 1) { if (count == null || count == 1) {
redisTemplate.expire(key,1, TimeUnit.SECONDS); redisTemplate.expire(key,1, TimeUnit.SECONDS);
} }
Object systemConfig = configService.getSystemConfig(RedisConstants.IP_QPS_THRESHOLD_LIMIT_KEY); Object systemConfig = configService.getSystemConfig(SystemConstants.SYSTEM_CONFIG_IP_QPS_LIMIT_KEY);
long limit = 10; long limit = 10;
if(systemConfig != null){ if(systemConfig != null){
limit = Convert.toLong(systemConfig,50L); limit = Convert.toLong(systemConfig,50L);
}else{ }else{
log.warn("[RedisRateLimiterFilter.rateLimit]系统配置中未配置IP请求限制QPS阈值配置,使用默认值:{},请检查配置项:{}", log.warn("[RedisRateLimiterFilter.rateLimit]系统配置中未配置IP请求限制QPS阈值配置,使用默认值:{},请检查配置项:{}",
limit,RedisConstants.IP_QPS_THRESHOLD_LIMIT_KEY); limit,SystemConstants.SYSTEM_CONFIG_IP_QPS_LIMIT_KEY);
} }
return count != null && count > limit; return count != null && count > limit;
} }

View File

@@ -30,10 +30,10 @@ public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
} else { } else {
if (authException instanceof BadCredentialsException) { if (authException instanceof BadCredentialsException) {
// 用户名或密码错误 // 用户名或密码错误
ResponseUtils.writeErrMsg(response, ResultCode.USER_PASSWORD_ERROR); ResponseUtils.writeErrMsg(response, ResultCode.USER_PASSWORD_ERROR, authException.getMessage());
} else { } else {
// 未认证或者token过期 // 未认证或者token过期
ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_TOKEN_INVALID); ResponseUtils.writeErrMsg(response, ResultCode.USER_LOGIN_EXCEPTION, authException.getMessage());
} }
} }
} }

View File

@@ -3,17 +3,17 @@ package com.youlai.boot.core.security.extension.sms;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.core.security.extension.WechatAuthenticationToken;
import com.youlai.boot.core.security.model.SysUserDetails; import com.youlai.boot.core.security.model.SysUserDetails;
import com.youlai.boot.system.model.dto.UserAuthInfo; import com.youlai.boot.system.model.dto.UserAuthInfo;
import com.youlai.boot.system.service.UserService; import com.youlai.boot.system.service.UserService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/** /**
@@ -27,10 +27,10 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
private final UserService userService; private final UserService userService;
private final StringRedisTemplate redisTemplate; private final RedisTemplate<String, Object> redisTemplate;
public SmsAuthenticationProvider(UserService userService, StringRedisTemplate redisTemplate) { public SmsAuthenticationProvider(UserService userService, RedisTemplate<String, Object> redisTemplate) {
this.userService = userService; this.userService = userService;
this.redisTemplate = redisTemplate; this.redisTemplate = redisTemplate;
} }
@@ -46,22 +46,28 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
@Override @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String mobile = (String) authentication.getPrincipal(); String mobile = (String) authentication.getPrincipal();
String verifyCode = (String) authentication.getCredentials(); String inputVerifyCode = (String) authentication.getCredentials();
// 根据手机号获取用户信息 // 根据手机号获取用户信息
UserAuthInfo userAuthInfo = userService.getUserAuthInfoByMobile(mobile); UserAuthInfo userAuthInfo = userService.getUserAuthInfoByMobile(mobile);
if (userAuthInfo == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 检查用户状态是否有效 // 检查用户状态是否有效
if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) { if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
throw new DisabledException("用户已被禁用"); throw new DisabledException("用户已被禁用");
} }
// 校验发送短信验证码的手机号是否与当前登录用户一致 // 校验发送短信验证码的手机号是否与当前登录用户一致
String cachedVerifyCode = (String) redisTemplate.opsForValue().get(RedisConstants.SMS_LOGIN_CODE_PREFIX + mobile);
String cachedVerifyCode= redisTemplate.opsForValue().get(RedisConstants.SMS_LOGIN_VERIFY_CODE_PREFIX + mobile); if (!StrUtil.equals(inputVerifyCode, cachedVerifyCode)) {
throw new BadCredentialsException("验证码错误");
if ( !StrUtil.equals(verifyCode, cachedVerifyCode)) { } else {
throw new CredentialsExpiredException("验证码错误"); // 验证成功后删除验证码
redisTemplate.delete(RedisConstants.SMS_LOGIN_CODE_PREFIX + mobile);
} }
// 构建认证后的用户详情信息 // 构建认证后的用户详情信息
@@ -76,6 +82,6 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
@Override @Override
public boolean supports(Class<?> authentication) { public boolean supports(Class<?> authentication) {
return WechatAuthenticationToken.class.isAssignableFrom(authentication); return SmsAuthenticationToken.class.isAssignableFrom(authentication);
} }
} }

View File

@@ -15,18 +15,27 @@ import java.util.Collection;
public class SmsAuthenticationToken extends AbstractAuthenticationToken { public class SmsAuthenticationToken extends AbstractAuthenticationToken {
@Serial @Serial
private static final long serialVersionUID = 621L; private static final long serialVersionUID = 621L;
/**
* 认证信息 (手机号)
*/
private final Object principal; private final Object principal;
private Object credentials;
/**
* 凭证信息 (短信验证码)
*/
private final Object credentials;
/** /**
* 短信验证码认证 Token (未认证) * 短信验证码认证 Token (未认证)
* *
* @param principal 微信用户信息 * @param principal 微信用户信息
*/ */
public SmsAuthenticationToken(Object principal) { public SmsAuthenticationToken(Object principal, Object credentials) {
// 没有授权信息时,设置为 null // 没有授权信息时,设置为 null
super(null); super(null);
this.principal = principal; this.principal = principal;
this.credentials = credentials;
// 默认未认证 // 默认未认证
this.setAuthenticated(false); this.setAuthenticated(false);
} }
@@ -40,6 +49,7 @@ public class SmsAuthenticationToken extends AbstractAuthenticationToken {
public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities); super(authorities);
this.principal = principal; this.principal = principal;
this.credentials = null;
// 认证通过 // 认证通过
super.setAuthenticated(true); super.setAuthenticated(true);
} }
@@ -58,7 +68,7 @@ public class SmsAuthenticationToken extends AbstractAuthenticationToken {
@Override @Override
public Object getCredentials() { public Object getCredentials() {
return this.credentials ; return this.credentials;
} }
@Override @Override

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.security.extension; package com.youlai.boot.core.security.extension.wechat;
import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.security.extension; package com.youlai.boot.core.security.extension.wechat;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;

View File

@@ -13,7 +13,7 @@ import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.config.property.SecurityProperties; import com.youlai.boot.config.property.SecurityProperties;
import com.youlai.boot.core.security.model.SysUserDetails; import com.youlai.boot.core.security.model.SysUserDetails;
import com.youlai.boot.core.security.model.AuthToken; import com.youlai.boot.core.security.model.AuthenticationToken;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -57,14 +57,14 @@ public class JwtTokenManager implements TokenManager {
* @return 令牌响应对象 * @return 令牌响应对象
*/ */
@Override @Override
public AuthToken generateToken(Authentication authentication) { public AuthenticationToken generateToken(Authentication authentication) {
int accessTokenTimeToLive = securityProperties.getJwt().getAccessTokenTimeToLive(); int accessTokenTimeToLive = securityProperties.getJwt().getAccessTokenTimeToLive();
int refreshTokenTimeToLive = securityProperties.getJwt().getRefreshTokenTimeToLive(); int refreshTokenTimeToLive = securityProperties.getJwt().getRefreshTokenTimeToLive();
String accessToken = generateToken(authentication, accessTokenTimeToLive); String accessToken = generateToken(authentication, accessTokenTimeToLive);
String refreshToken = generateToken(authentication, refreshTokenTimeToLive); String refreshToken = generateToken(authentication, refreshTokenTimeToLive);
return AuthToken.builder() return AuthenticationToken.builder()
.accessToken(accessToken) .accessToken(accessToken)
.refreshToken(refreshToken) .refreshToken(refreshToken)
.tokenType("Bearer") .tokenType("Bearer")
@@ -163,7 +163,7 @@ public class JwtTokenManager implements TokenManager {
*/ */
@Override @Override
public AuthToken refreshToken(String refreshToken) { public AuthenticationToken refreshToken(String refreshToken) {
boolean isValid = validateToken(refreshToken); boolean isValid = validateToken(refreshToken);
if (!isValid) { if (!isValid) {
@@ -174,7 +174,7 @@ public class JwtTokenManager implements TokenManager {
int accessTokenExpiration = securityProperties.getJwt().getRefreshTokenTimeToLive(); int accessTokenExpiration = securityProperties.getJwt().getRefreshTokenTimeToLive();
String newAccessToken = generateToken(authentication, accessTokenExpiration); String newAccessToken = generateToken(authentication, accessTokenExpiration);
return AuthToken.builder() return AuthenticationToken.builder()
.accessToken(newAccessToken) .accessToken(newAccessToken)
.refreshToken(refreshToken) .refreshToken(refreshToken)
.tokenType("Bearer") .tokenType("Bearer")

View File

@@ -1,6 +1,6 @@
package com.youlai.boot.core.security.manager; package com.youlai.boot.core.security.manager;
import com.youlai.boot.core.security.model.AuthToken; import com.youlai.boot.core.security.model.AuthenticationToken;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -22,7 +22,7 @@ public class RedisTokenManager implements TokenManager {
* @return * @return
*/ */
@Override @Override
public AuthToken generateToken(Authentication authentication) { public AuthenticationToken generateToken(Authentication authentication) {
return null; return null;
} }
@@ -55,7 +55,7 @@ public class RedisTokenManager implements TokenManager {
* @return * @return
*/ */
@Override @Override
public AuthToken refreshToken(String token) { public AuthenticationToken refreshToken(String token) {
return null; return null;
} }
} }

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.core.security.manager; package com.youlai.boot.core.security.manager;
import com.youlai.boot.core.security.model.AuthToken; import com.youlai.boot.core.security.model.AuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
/** /**
@@ -18,7 +18,7 @@ public interface TokenManager {
* @param authentication 用户认证信息 * @param authentication 用户认证信息
* @return 认证 Token 响应 * @return 认证 Token 响应
*/ */
AuthToken generateToken(Authentication authentication); AuthenticationToken generateToken(Authentication authentication);
/** /**
* 解析 Token 获取认证信息 * 解析 Token 获取认证信息
@@ -44,7 +44,7 @@ public interface TokenManager {
* @param token 刷新令牌 * @param token 刷新令牌
* @return 认证 Token 响应 * @return 认证 Token 响应
*/ */
AuthToken refreshToken(String token); AuthenticationToken refreshToken(String token);
/** /**
* 将 Token 加入黑名单 * 将 Token 加入黑名单

View File

@@ -7,13 +7,13 @@ import lombok.Data;
/** /**
* 认证令牌响应对象 * 认证令牌响应对象
* *
* @author Ray * @author Ray.Hao
* @since 0.0.1 * @since 0.0.1
*/ */
@Schema(description = "认证令牌响应对象") @Schema(description = "认证令牌响应对象")
@Data @Data
@Builder @Builder
public class AuthToken { public class AuthenticationToken {
@Schema(description = "令牌类型", example = "Bearer") @Schema(description = "令牌类型", example = "Bearer")
private String tokenType; private String tokenType;

View File

@@ -2,10 +2,9 @@ package com.youlai.boot.shared.auth.controller;
import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.result.Result; import com.youlai.boot.common.result.Result;
import com.youlai.boot.shared.auth.model.RefreshTokenRequest;
import com.youlai.boot.shared.auth.service.AuthService; import com.youlai.boot.shared.auth.service.AuthService;
import com.youlai.boot.shared.auth.model.CaptchaResponse; import com.youlai.boot.shared.auth.model.CaptchaInfo;
import com.youlai.boot.core.security.model.AuthToken; import com.youlai.boot.core.security.model.AuthenticationToken;
import com.youlai.boot.common.annotation.Log; import com.youlai.boot.common.annotation.Log;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@@ -29,18 +28,25 @@ public class AuthController {
private final AuthService authService; private final AuthService authService;
@Operation(summary = "登录") @Operation(summary = "获取登录验证码")
@GetMapping("/captcha")
public Result<CaptchaInfo> getCaptcha() {
CaptchaInfo captcha = authService.getCaptcha();
return Result.success(captcha);
}
@Operation(summary = "账号密码登录")
@PostMapping("/login") @PostMapping("/login")
@Log(value = "登录", module = LogModuleEnum.LOGIN) @Log(value = "登录", module = LogModuleEnum.LOGIN)
public Result<AuthToken> login( public Result<AuthenticationToken> login(
@Parameter(description = "用户名", example = "admin") @RequestParam String username, @Parameter(description = "用户名", example = "admin") @RequestParam String username,
@Parameter(description = "密码", example = "123456") @RequestParam String password @Parameter(description = "密码", example = "123456") @RequestParam String password
) { ) {
AuthToken authToken = authService.login(username, password); AuthenticationToken authenticationToken = authService.login(username, password);
return Result.success(authToken); return Result.success(authenticationToken);
} }
@Operation(summary = "注销") @Operation(summary = "注销登录")
@DeleteMapping("/logout") @DeleteMapping("/logout")
@Log(value = "注销", module = LogModuleEnum.LOGIN) @Log(value = "注销", module = LogModuleEnum.LOGIN)
public Result<?> logout() { public Result<?> logout() {
@@ -48,48 +54,42 @@ public class AuthController {
return Result.success(); return Result.success();
} }
@Operation(summary = "获取验证码") @Operation(summary = "刷新访问令牌")
@GetMapping("/captcha")
public Result<CaptchaResponse> getCaptcha() {
CaptchaResponse captcha = authService.getCaptcha();
return Result.success(captcha);
}
@Operation(summary = "刷新token")
@PostMapping("/refresh-token") @PostMapping("/refresh-token")
public Result<?> refreshToken(@RequestBody RefreshTokenRequest request) { public Result<?> refreshToken(
AuthToken authToken = authService.refreshToken(request); @Parameter(description = "刷新令牌", example = "xxx.xxx.xxx") @RequestParam String refreshToken
return Result.success(authToken); ) {
AuthenticationToken authenticationToken = authService.refreshToken(refreshToken);
return Result.success(authenticationToken);
} }
@Operation(summary = "微信登录") @Operation(summary = "微信授权登录")
@PostMapping("/wechat-login") @PostMapping("/login/wechat")
@Log(value = "微信登录", module = LogModuleEnum.LOGIN) @Log(value = "微信登录", module = LogModuleEnum.LOGIN)
public Result<AuthToken> wechatLogin( public Result<AuthenticationToken> loginByWechat(
@Parameter(description = "微信授权码", example = "code") @RequestParam String code @Parameter(description = "微信授权码", example = "code") @RequestParam String code
) { ) {
AuthToken loginResult = authService.wechatLogin(code); AuthenticationToken loginResult = authService.loginByWechat(code);
return Result.success(loginResult); return Result.success(loginResult);
} }
@Operation(summary = "发送登录短信验证码")
@Operation(summary = "短信验证码登录") @PostMapping("/login/sms/code")
@PostMapping("/sms-login")
@Log(value = "短信验证码登录", module = LogModuleEnum.LOGIN)
public Result<AuthToken> smsLogin(
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile,
@Parameter(description = "验证码", example = "123456") @RequestParam String code
) {
AuthToken loginResult = authService.smsLogin(mobile, code);
return Result.success(loginResult);
}
@Operation(summary = "短信验证码登录发送短信")
@PostMapping("/sms-login/verify-code")
public Result<?> sendLoginVerifyCode( public Result<?> sendLoginVerifyCode(
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile @Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile
) { ) {
authService.sendLoginVerifyCode(mobile); authService.sendSmsLoginCode(mobile);
return Result.success(); return Result.success();
} }
@Operation(summary = "短信验证码登录")
@PostMapping("/login/sms")
@Log(value = "短信验证码登录", module = LogModuleEnum.LOGIN)
public Result<AuthenticationToken> loginBySms(
@Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile,
@Parameter(description = "验证码", example = "123456") @RequestParam String code
) {
AuthenticationToken loginResult = authService.loginBySms(mobile, code);
return Result.success(loginResult);
}
} }

View File

@@ -5,17 +5,17 @@ import lombok.Builder;
import lombok.Data; import lombok.Data;
/** /**
* 验证码响应对象 * 验证码信息
* *
* @author Ray Hao * @author RayHao
* @since 2023/03/24 * @since 2023/03/24
*/ */
@Schema(description = "验证码响应对象") @Schema(description = "验证码信息")
@Data @Data
@Builder @Builder
public class CaptchaResponse { public class CaptchaInfo {
@Schema(description = "验证码ID") @Schema(description = "验证码缓存 Key")
private String captchaKey; private String captchaKey;
@Schema(description = "验证码图片Base64字符串") @Schema(description = "验证码图片Base64字符串")

View File

@@ -1,21 +0,0 @@
package com.youlai.boot.shared.auth.model;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 刷新令牌请求参数
*
* @author haoxr
* @since 2024/11/11
*/
@Schema(description = "刷新令牌请求参数")
@Data
public class RefreshTokenRequest {
@Schema(description = "刷新令牌")
@NotBlank(message = "刷新令牌不能为空")
private String refreshToken;
}

View File

@@ -1,13 +1,12 @@
package com.youlai.boot.shared.auth.service; package com.youlai.boot.shared.auth.service;
import com.youlai.boot.shared.auth.model.CaptchaResponse; import com.youlai.boot.shared.auth.model.CaptchaInfo;
import com.youlai.boot.core.security.model.AuthToken; import com.youlai.boot.core.security.model.AuthenticationToken;
import com.youlai.boot.shared.auth.model.RefreshTokenRequest;
/** /**
* 认证服务接口 * 认证服务接口
* *
* @author haoxr * @author Ray.Hao
* @since 2.4.0 * @since 2.4.0
*/ */
public interface AuthService { public interface AuthService {
@@ -19,7 +18,7 @@ public interface AuthService {
* @param password 密码 * @param password 密码
* @return 登录结果 * @return 登录结果
*/ */
AuthToken login(String username, String password); AuthenticationToken login(String username, String password);
/** /**
* 登出 * 登出
@@ -31,15 +30,15 @@ public interface AuthService {
* *
* @return 验证码 * @return 验证码
*/ */
CaptchaResponse getCaptcha(); CaptchaInfo getCaptcha();
/** /**
* 刷新令牌 * 刷新令牌
* *
* @param request 刷新令牌请求参数 * @param refreshToken 刷新令牌
* @return 登录结果 * @return 登录结果
*/ */
AuthToken refreshToken(RefreshTokenRequest request); AuthenticationToken refreshToken(String refreshToken);
/** /**
* 微信小程序登录 * 微信小程序登录
@@ -47,12 +46,21 @@ public interface AuthService {
* @param code 微信登录code * @param code 微信登录code
* @return 登录结果 * @return 登录结果
*/ */
AuthToken wechatLogin(String code); AuthenticationToken loginByWechat(String code);
/** /**
* 发送短信验证码 * 发送短信验证码
* *
* @param mobile 手机号 * @param mobile 手机号
*/ */
void sendLoginVerifyCode(String mobile); void sendSmsLoginCode(String mobile);
/**
* 短信验证码登录
*
* @param mobile 手机号
* @param code 验证码
* @return 登录结果
*/
AuthenticationToken loginBySms(String mobile, String code);
} }

View File

@@ -5,16 +5,17 @@ import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.config.property.CaptchaProperties; import com.youlai.boot.config.property.CaptchaProperties;
import com.youlai.boot.core.security.extension.WechatAuthenticationToken; import com.youlai.boot.core.security.extension.sms.SmsAuthenticationToken;
import com.youlai.boot.core.security.extension.wechat.WechatAuthenticationToken;
import com.youlai.boot.core.security.util.SecurityUtils; import com.youlai.boot.core.security.util.SecurityUtils;
import com.youlai.boot.shared.auth.enums.CaptchaTypeEnum; import com.youlai.boot.shared.auth.enums.CaptchaTypeEnum;
import com.youlai.boot.core.security.model.AuthToken; import com.youlai.boot.core.security.model.AuthenticationToken;
import com.youlai.boot.shared.auth.model.CaptchaResponse; import com.youlai.boot.shared.auth.model.CaptchaInfo;
import com.youlai.boot.shared.auth.model.RefreshTokenRequest;
import com.youlai.boot.shared.auth.service.AuthService; import com.youlai.boot.shared.auth.service.AuthService;
import com.youlai.boot.core.security.manager.TokenManager; import com.youlai.boot.core.security.manager.TokenManager;
import com.youlai.boot.shared.sms.enums.SmsTypeEnum; import com.youlai.boot.shared.sms.enums.SmsTypeEnum;
@@ -29,12 +30,14 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.awt.*; import java.awt.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* 认证服务实现类 * 认证服务实现类
* *
* @author haoxr * @author Ray.Hao
* @since 2.4.0 * @since 2.4.0
*/ */
@Service @Service
@@ -43,14 +46,16 @@ import java.util.concurrent.TimeUnit;
public class AuthServiceImpl implements AuthService { public class AuthServiceImpl implements AuthService {
private final AuthenticationManager authenticationManager; private final AuthenticationManager authenticationManager;
private final RedisTemplate<String, Object> redisTemplate;
private final CodeGenerator codeGenerator;
private final Font captchaFont;
private final CaptchaProperties captchaProperties;
private final TokenManager tokenManager; private final TokenManager tokenManager;
private final Font captchaFont;
private final CaptchaProperties captchaProperties;
private final CodeGenerator codeGenerator;
private final SmsService smsService; private final SmsService smsService;
private final RedisTemplate<String, Object> redisTemplate;
/** /**
* 用户名密码登录 * 用户名密码登录
* *
@@ -59,7 +64,7 @@ public class AuthServiceImpl implements AuthService {
* @return 访问令牌 * @return 访问令牌
*/ */
@Override @Override
public AuthToken login(String username, String password) { public AuthenticationToken login(String username, String password) {
// 1. 创建用于密码认证的令牌(未认证) // 1. 创建用于密码认证的令牌(未认证)
UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username.trim(), password); new UsernamePasswordAuthenticationToken(username.trim(), password);
@@ -68,10 +73,10 @@ public class AuthServiceImpl implements AuthService {
Authentication authentication = authenticationManager.authenticate(authenticationToken); Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
AuthToken authTokenResponse = AuthenticationToken authenticationTokenResponse =
tokenManager.generateToken(authentication); tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
return authTokenResponse; return authenticationTokenResponse;
} }
/** /**
@@ -81,42 +86,69 @@ public class AuthServiceImpl implements AuthService {
* @return 访问令牌 * @return 访问令牌
*/ */
@Override @Override
public AuthToken wechatLogin(String code) { public AuthenticationToken loginByWechat(String code) {
// 1. 创建用户微信认证的令牌(未认证) // 1. 创建用户微信认证的令牌(未认证)
WechatAuthenticationToken authenticationToken = new WechatAuthenticationToken(code); WechatAuthenticationToken wechatAuthenticationToken = new WechatAuthenticationToken(code);
// 2. 执行认证(认证中) // 2. 执行认证(认证中)
Authentication authentication = authenticationManager.authenticate(authenticationToken); Authentication authentication = authenticationManager.authenticate(wechatAuthenticationToken);
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
AuthToken authTokenResponse = tokenManager.generateToken(authentication); AuthenticationToken authenticationToken = tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
return authTokenResponse; return authenticationToken;
} }
/** /**
* 发送短信验证码 * 发送短信验证码
* *
* @param mobile 手机号 * @param mobile 手机号
*/ */
@Override @Override
public void sendLoginVerifyCode(String mobile) { public void sendSmsLoginCode(String mobile) {
// 随机生成4位验证码 // 随机生成4位验证码
String verifyCode = String.valueOf((int) ((Math.random() * 9 + 1) * 1000)); // String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
// TODO 为了方便测试,验证码固定为 1234实际开发中在配置了厂商短信服务后可以使用上面的随机验证码
String code = "1234";
// 发送短信验证码 // 发送短信验证码
smsService.sendSms(mobile, SmsTypeEnum.LOGIN, verifyCode); Map<String, String> templateParams = new HashMap<>();
templateParams.put("code", code);
try {
smsService.sendSms(mobile, SmsTypeEnum.LOGIN, templateParams);
} catch (Exception e) {
log.error("发送短信验证码失败", e);
}
// 缓存验证码至Redis用于登录校验
redisTemplate.opsForValue().set(RedisConstants.SMS_LOGIN_CODE_PREFIX + mobile, code, 5, TimeUnit.MINUTES);
} }
/** /**
* 注销 * 短信验证码登录
*
* @param mobile 手机号
* @param code 验证码
* @return 访问令牌
*/
@Override
public AuthenticationToken loginBySms(String mobile, String code) {
// 1. 创建用户微信认证的令牌(未认证)
SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(mobile, code);
// 2. 执行认证(认证中)
Authentication authentication = authenticationManager.authenticate(smsAuthenticationToken);
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
AuthenticationToken authenticationToken = tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
return authenticationToken;
}
/**
* 注销登录
*/ */
@Override @Override
public void logout() { public void logout() {
@@ -136,7 +168,7 @@ public class AuthServiceImpl implements AuthService {
* @return 验证码 * @return 验证码
*/ */
@Override @Override
public CaptchaResponse getCaptcha() { public CaptchaInfo getCaptcha() {
String captchaType = captchaProperties.getType(); String captchaType = captchaProperties.getType();
int width = captchaProperties.getWidth(); int width = captchaProperties.getWidth();
@@ -168,30 +200,27 @@ public class AuthServiceImpl implements AuthService {
redisTemplate.opsForValue().set(SecurityConstants.CAPTCHA_CODE_PREFIX + captchaKey, captchaCode, redisTemplate.opsForValue().set(SecurityConstants.CAPTCHA_CODE_PREFIX + captchaKey, captchaCode,
captchaProperties.getExpireSeconds(), TimeUnit.SECONDS); captchaProperties.getExpireSeconds(), TimeUnit.SECONDS);
return CaptchaResponse.builder() return CaptchaInfo.builder()
.captchaKey(captchaKey) .captchaKey(captchaKey)
.captchaBase64(imageBase64Data) .captchaBase64(imageBase64Data)
.build(); .build();
} }
/** /**
* 刷新令牌 * 刷新token
* *
* @param request 刷新令牌请求参数 * @param refreshToken 刷新令牌
* @return 新的访问令牌 * @return 新的访问令牌
*/ */
@Override @Override
public AuthToken refreshToken(RefreshTokenRequest request) { public AuthenticationToken refreshToken(String refreshToken) {
// 验证刷新令牌 // 验证刷新令牌
String refreshToken = request.getRefreshToken();
boolean isValidate = tokenManager.validateToken(refreshToken); boolean isValidate = tokenManager.validateToken(refreshToken);
if (!isValidate) { if (!isValidate) {
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
} }
// 刷新令牌有效,生成新的访问令牌
return tokenManager.refreshToken(refreshToken); return tokenManager.refreshToken(refreshToken);
} }

View File

@@ -5,12 +5,29 @@ import lombok.Getter;
/** /**
* 短信类型枚举 * 短信类型枚举
* <p>
* value 值对应 application-*.yml 中的 sms.templates.* 配置
*
* @author Ray.Hao
* @since 2.21.0
*/ */
@Getter @Getter
public enum SmsTypeEnum implements IBaseEnum<String> { public enum SmsTypeEnum implements IBaseEnum<String> {
/**
* 注册短信验证码
*/
REGISTER("register", "注册短信验证码"), REGISTER("register", "注册短信验证码"),
/**
* 登录短信验证码
*/
LOGIN("login", "登录短信验证码"), LOGIN("login", "登录短信验证码"),
RESET_PASSWORD("reset-password", "重置密码短信验证码");
/**
* 修改手机号短信验证码
*/
CHANGE_MOBILE("change-mobile", "修改手机号短信验证码");
private final String value; private final String value;
private final String label; private final String label;

View File

@@ -2,6 +2,8 @@ package com.youlai.boot.shared.sms.service;
import com.youlai.boot.shared.sms.enums.SmsTypeEnum; import com.youlai.boot.shared.sms.enums.SmsTypeEnum;
import java.util.Map;
/** /**
* 短信服务接口层 * 短信服务接口层
* *
@@ -14,9 +16,9 @@ public interface SmsService {
* 发送短信 * 发送短信
* *
* @param mobile 手机号 13388886666 * @param mobile 手机号 13388886666
* @param smsType 短信模板 SMS_194640010 * @param smsType 短信模板 SMS_194640010,模板内容:您的验证码为:${code}请在5分钟内使用
* @param templateParam 模板参数 "[{"code":"123456"}]" * @param templateParams 模板参数 [{"code":"123456"}] ,用于替换短信模板中的变量
* @return boolean 是否发送成功 * @return boolean 是否发送成功
*/ */
boolean sendSms(String mobile, SmsTypeEnum smsType, String templateParam); boolean sendSms(String mobile, SmsTypeEnum smsType, Map<String, String> templateParams);
} }

View File

@@ -1,11 +1,11 @@
package com.youlai.boot.shared.sms.service.impl; package com.youlai.boot.shared.sms.service.impl;
import cn.hutool.json.JSONUtil;
import com.aliyuncs.CommonRequest; import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse; import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient; import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType; import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile; import com.aliyuncs.profile.DefaultProfile;
import com.youlai.boot.config.property.AliyunSmsProperties; import com.youlai.boot.config.property.AliyunSmsProperties;
@@ -14,6 +14,8 @@ import com.youlai.boot.shared.sms.service.SmsService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Map;
/** /**
* 阿里云短信业务类 * 阿里云短信业务类
* *
@@ -31,12 +33,11 @@ public class AliyunSmsService implements SmsService {
* *
* @param mobile 手机号 13388886666 * @param mobile 手机号 13388886666
* @param smsType 短信模板 SMS_194640010 * @param smsType 短信模板 SMS_194640010
* @param templateParam 模板参数 [{"code":"123456"}] * @param templateParams 模板参数 [{"code":"123456"}]
*
* @return boolean 是否发送成功 * @return boolean 是否发送成功
*/ */
@Override @Override
public boolean sendSms(String mobile, SmsTypeEnum smsType, String templateParam) { public boolean sendSms(String mobile, SmsTypeEnum smsType, Map<String, String> templateParams) {
String templateCode = aliyunSmsProperties.getTemplates().get(smsType.getValue()); String templateCode = aliyunSmsProperties.getTemplates().get(smsType.getValue());
@@ -63,7 +64,7 @@ public class AliyunSmsService implements SmsService {
// 您申请的模板 code // 您申请的模板 code
request.putQueryParameter("TemplateCode", templateCode); request.putQueryParameter("TemplateCode", templateCode);
request.putQueryParameter("TemplateParam", templateParam); request.putQueryParameter("TemplateParam", JSONUtil.toJsonStr(templateParams));
try { try {
CommonResponse response = client.getCommonResponse(request); CommonResponse response = client.getCommonResponse(request);

View File

@@ -22,11 +22,10 @@ import jakarta.validation.Valid;
import java.util.List; import java.util.List;
/** /**
* 角色控制层 * 角色控制层
* *
* @author Ray * @author Ray.Hao
* @since 2022/10/16 * @since 2022/10/16
*/ */
@Tag(name = "03.角色接口") @Tag(name = "03.角色接口")
@@ -39,7 +38,7 @@ public class RoleController {
@Operation(summary = "角色分页列表") @Operation(summary = "角色分页列表")
@GetMapping("/page") @GetMapping("/page")
@Log( value = "角色分页列表",module = LogModuleEnum.ROLE) @Log(value = "角色分页列表", module = LogModuleEnum.ROLE)
public PageResult<RolePageVO> getRolePage( public PageResult<RolePageVO> getRolePage(
RolePageQuery queryParams RolePageQuery queryParams
) { ) {
@@ -83,11 +82,11 @@ public class RoleController {
@Operation(summary = "删除角色") @Operation(summary = "删除角色")
@DeleteMapping("/{ids}") @DeleteMapping("/{ids}")
@PreAuthorize("@ss.hasPerm('sys:role:delete')") @PreAuthorize("@ss.hasPerm('sys:role:delete')")
public Result<?> deleteRoles( public Result<Void> deleteRoles(
@Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids
) { ) {
boolean result = roleService.deleteRoles(ids); roleService.deleteRoles(ids);
return Result.judge(result); return Result.success();
} }
@Operation(summary = "修改角色状态") @Operation(summary = "修改角色状态")
@@ -111,11 +110,11 @@ public class RoleController {
@Operation(summary = "分配菜单(包括按钮权限)给角色") @Operation(summary = "分配菜单(包括按钮权限)给角色")
@PutMapping("/{roleId}/menus") @PutMapping("/{roleId}/menus")
public Result<?> assignMenusToRole( public Result<Void> assignMenusToRole(
@PathVariable Long roleId, @PathVariable Long roleId,
@RequestBody List<Long> menuIds @RequestBody List<Long> menuIds
) { ) {
boolean result = roleService.assignMenusToRole(roleId, menuIds); roleService.assignMenusToRole(roleId, menuIds);
return Result.judge(result); return Result.success();
} }
} }

View File

@@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.youlai.boot.common.annotation.Log; import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.common.annotation.RepeatSubmit; import com.youlai.boot.common.annotation.RepeatSubmit;
import com.youlai.boot.system.enums.ContactType;
import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.model.Option; import com.youlai.boot.common.model.Option;
import com.youlai.boot.common.result.PageResult; import com.youlai.boot.common.result.PageResult;
@@ -45,7 +44,7 @@ import java.util.List;
/** /**
* 用户控制层 * 用户控制层
* *
* @author Ray * @author Ray.Hao
* @since 2022/10/16 * @since 2022/10/16
*/ */
@Tag(name = "02.用户接口") @Tag(name = "02.用户接口")
@@ -203,39 +202,46 @@ public class UserController {
@Operation(summary = "修改密码") @Operation(summary = "修改密码")
@PutMapping(value = "/password") @PutMapping(value = "/password")
public Result<?> changePassword( public Result<?> changePassword(
@RequestBody PasswordChangeForm data @RequestBody PasswordUpdateForm data
) { ) {
Long currUserId = SecurityUtils.getUserId(); Long currUserId = SecurityUtils.getUserId();
boolean result = userService.changePassword(currUserId, data); boolean result = userService.changePassword(currUserId, data);
return Result.judge(result); return Result.judge(result);
} }
@Operation(summary = "发送短信/邮箱验证码") @Operation(summary = "发送短信验证码(绑定或更换手机号)")
@PostMapping(value = "/send-verification-code") @PostMapping(value = "/mobile/code")
public Result<?> sendVerificationCode( public Result<?> sendMobileCode(
@Parameter(description = "联系方式(手机号码或邮箱地址)", required = true) @RequestParam String contact, @Parameter(description = "手机号码", required = true) @RequestParam String mobile
@Parameter(description = "联系方式类型Mobile或Email", required = true) @RequestParam ContactType contactType
) { ) {
boolean result = userService.sendVerificationCode(contact, contactType); boolean result = userService.sendMobileCode(mobile);
return Result.judge(result); return Result.judge(result);
} }
@Operation(summary = "个人中心绑定用户手机号") @Operation(summary = "绑定或更换手机号")
@PutMapping(value = "/mobile") @PutMapping(value = "/mobile")
public Result<?> bindMobile( public Result<?> bindOrChangeMobile(
@RequestBody @Validated MobileBindingForm data @RequestBody @Validated MobileUpdateForm data
) { ) {
boolean result = userService.bindMobile(data); boolean result = userService.bindOrChangeMobile(data);
return Result.judge(result); return Result.judge(result);
} }
@Operation(summary = "发送邮箱验证码(绑定或更换邮箱)")
@Operation(summary = "个人中心绑定用户邮箱") @PostMapping(value = "/email/code")
@PutMapping(value = "/email") public Result<Void> sendEmailCode(
public Result<?> bindEmail( @Parameter(description = "邮箱地址", required = true) @RequestParam String email
@RequestBody @Validated EmailBindingForm data
) { ) {
boolean result = userService.bindEmail(data); userService.sendEmailCode(email);
return Result.success();
}
@Operation(summary = "绑定或更换邮箱")
@PutMapping(value = "/email")
public Result<?> bindOrChangeEmail(
@RequestBody @Validated EmailUpdateForm data
) {
boolean result = userService.bindOrChangeEmail(data);
return Result.judge(result); return Result.judge(result);
} }

View File

@@ -26,9 +26,9 @@ public interface RoleConverter {
@Mapping(target = "value", source = "id"), @Mapping(target = "value", source = "id"),
@Mapping(target = "label", source = "name") @Mapping(target = "label", source = "name")
}) })
Option<Long> entity2Option(Role role); Option<Long> toOption(Role role);
List<Option<Long>> entities2Options(List<Role> roles); List<Option<Long>> toOptions(List<Role> roles);
Role toEntity(RoleForm roleForm); Role toEntity(RoleForm roleForm);

View File

@@ -1,6 +1,7 @@
package com.youlai.boot.system.converter; package com.youlai.boot.system.converter;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.system.model.entity.User; import com.youlai.boot.system.model.entity.User;
import com.youlai.boot.system.model.vo.UserInfoVO; import com.youlai.boot.system.model.vo.UserInfoVO;
import com.youlai.boot.system.model.vo.UserPageVO; import com.youlai.boot.system.model.vo.UserPageVO;
@@ -14,10 +15,12 @@ import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import org.mapstruct.Mappings; import org.mapstruct.Mappings;
import java.util.List;
/** /**
* 用户对象转换器 * 用户对象转换器
* *
* @author haoxr * @author Ray.Hao
* @since 2022/6/8 * @since 2022/6/8
*/ */
@Mapper(componentModel = "spring") @Mapper(componentModel = "spring")
@@ -43,4 +46,12 @@ public interface UserConverter {
UserProfileVO toProfileVO(UserBO bo); UserProfileVO toProfileVO(UserBO bo);
User toEntity(UserProfileForm formData); User toEntity(UserProfileForm formData);
@Mappings({
@Mapping(target = "label", source = "nickname"),
@Mapping(target = "value", source = "id")
})
Option<String> toOption(User entity);
List<Option<String>> toOptions(List<User> list);
} }

View File

@@ -1,19 +0,0 @@
package com.youlai.boot.system.enums;
/**
* 联系方式类型
*
* @author Ray
* @since 2.10.0
*/
public enum ContactType {
/**
* 手机
*/
MOBILE,
/**
* 邮箱
*/
EMAIL
}

View File

@@ -6,7 +6,7 @@ import lombok.Getter;
/** /**
* 字典编码枚举 * 字典编码枚举
* *
* @author Ray * @author Ray.Hao
* @since 2024/10/30 * @since 2024/10/30
*/ */
@Getter @Getter

View File

@@ -7,7 +7,7 @@ import lombok.Getter;
/** /**
* 菜单类型枚举 * 菜单类型枚举
* *
* @author haoxr * @author Ray.Hao
* @since 2022/4/23 9:36 * @since 2022/4/23 9:36
*/ */
@Getter @Getter

View File

@@ -7,7 +7,7 @@ import lombok.Getter;
/** /**
* 通告发布状态枚举 * 通告发布状态枚举
* *
* @author haoxr * @author Ray.Hao
* @since 2024/10/14 * @since 2024/10/14
*/ */
@Getter @Getter

View File

@@ -7,8 +7,8 @@ import lombok.Getter;
/** /**
* 通知目标类型枚举 * 通知目标类型枚举
* *
* @author haoxr * @author Ray.Hao
* @since 2022/10/14 * @since 2024/10/14
*/ */
@Getter @Getter
@Schema(enumAsRef = true) @Schema(enumAsRef = true)

View File

@@ -4,14 +4,14 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
/** /**
* 绑定邮箱表单 * 修改邮箱表单
* *
* @author Ray.Hao * @author Ray.Hao
* @since 2024/8/19 * @since 2024/8/19
*/ */
@Schema(description = "绑定邮箱表单") @Schema(description = "修改邮箱表单")
@Data @Data
public class EmailBindingForm { public class EmailUpdateForm {
@Schema(description = "邮箱") @Schema(description = "邮箱")
private String email; private String email;

View File

@@ -4,14 +4,14 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
/** /**
* 绑定手机表单 * 修改手机表单
* *
* @author Ray * @author Ray.Hao
* @since 2024/8/19 * @since 2024/8/19
*/ */
@Schema(description = "绑定手机表单") @Schema(description = "修改手机表单")
@Data @Data
public class MobileBindingForm { public class MobileUpdateForm {
@Schema(description = "手机号码") @Schema(description = "手机号码")
private String mobile; private String mobile;

View File

@@ -6,12 +6,12 @@ import lombok.Data;
/** /**
* 修改密码表单 * 修改密码表单
* *
* @author Ray * @author Ray.Hao
* @since 2024/8/13 * @since 2024/8/13
*/ */
@Schema(description = "修改密码表单") @Schema(description = "修改密码表单")
@Data @Data
public class PasswordChangeForm { public class PasswordUpdateForm {
@Schema(description = "原密码") @Schema(description = "原密码")
private String oldPassword; private String oldPassword;

View File

@@ -64,10 +64,8 @@ public interface RoleService extends IService<Role> {
* 批量删除角色 * 批量删除角色
* *
* @param ids 角色ID多个使用英文逗号(,)分割 * @param ids 角色ID多个使用英文逗号(,)分割
* @return
*/ */
boolean deleteRoles(String ids); void deleteRoles(String ids);
/** /**
* 获取角色的菜单ID集合 * 获取角色的菜单ID集合
@@ -77,15 +75,13 @@ public interface RoleService extends IService<Role> {
*/ */
List<Long> getRoleMenuIds(Long roleId); List<Long> getRoleMenuIds(Long roleId);
/** /**
* 修改角色的资源权限 * 修改角色的资源权限
* *
* @param roleId * @param roleId 角色ID
* @param menuIds * @param menuIds 菜单ID集合
* @return
*/ */
boolean assignMenusToRole(Long roleId, List<Long> menuIds); void assignMenusToRole(Long roleId, List<Long> menuIds);
/** /**
* 获取最大范围的数据权限 * 获取最大范围的数据权限

View File

@@ -1,9 +1,7 @@
package com.youlai.boot.system.service; package com.youlai.boot.system.service;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.boot.system.enums.ContactType;
import com.youlai.boot.common.model.Option; import com.youlai.boot.common.model.Option;
import com.youlai.boot.system.model.dto.UserAuthInfo; import com.youlai.boot.system.model.dto.UserAuthInfo;
import com.youlai.boot.system.model.dto.UserExportDTO; import com.youlai.boot.system.model.dto.UserExportDTO;
@@ -27,16 +25,15 @@ public interface UserService extends IService<User> {
/** /**
* 用户分页列表 * 用户分页列表
* *
* @return * @return {@link IPage<UserPageVO>} 用户分页列表
*/ */
IPage<UserPageVO> getUserPage(UserPageQuery queryParams); IPage<UserPageVO> getUserPage(UserPageQuery queryParams);
/** /**
* 获取用户表单数据 * 获取用户表单数据
* *
* @param userId * @param userId 用户ID
* @return * @return {@link UserForm} 用户表单数据
*/ */
UserForm getUserFormData(Long userId); UserForm getUserFormData(Long userId);
@@ -45,7 +42,7 @@ public interface UserService extends IService<User> {
* 新增用户 * 新增用户
* *
* @param userForm 用户表单对象 * @param userForm 用户表单对象
* @return * @return {@link Boolean} 是否新增成功
*/ */
boolean saveUser(UserForm userForm); boolean saveUser(UserForm userForm);
@@ -54,7 +51,7 @@ public interface UserService extends IService<User> {
* *
* @param userId 用户ID * @param userId 用户ID
* @param userForm 用户表单对象 * @param userForm 用户表单对象
* @return * @return {@link Boolean} 是否修改成功
*/ */
boolean updateUser(Long userId, UserForm userForm); boolean updateUser(Long userId, UserForm userForm);
@@ -63,7 +60,7 @@ public interface UserService extends IService<User> {
* 删除用户 * 删除用户
* *
* @param idsStr 用户ID多个以英文逗号(,)分割 * @param idsStr 用户ID多个以英文逗号(,)分割
* @return * @return {@link Boolean} 是否删除成功
*/ */
boolean deleteUsers(String idsStr); boolean deleteUsers(String idsStr);
@@ -82,7 +79,7 @@ public interface UserService extends IService<User> {
* 获取导出用户列表 * 获取导出用户列表
* *
* @param queryParams 查询参数 * @param queryParams 查询参数
* @return * @return {@link List<UserExportDTO>} 导出用户列表
*/ */
List<UserExportDTO> listExportUsers(UserPageQuery queryParams); List<UserExportDTO> listExportUsers(UserPageQuery queryParams);
@@ -90,14 +87,14 @@ public interface UserService extends IService<User> {
/** /**
* 获取登录用户信息 * 获取登录用户信息
* *
* @return * @return {@link UserInfoVO} 登录用户信息
*/ */
UserInfoVO getCurrentUserInfo(); UserInfoVO getCurrentUserInfo();
/** /**
* 获取个人中心用户信息 * 获取个人中心用户信息
* *
* @return * @return {@link UserProfileVO} 个人中心用户信息
*/ */
UserProfileVO getUserProfile(Long userId); UserProfileVO getUserProfile(Long userId);
@@ -105,7 +102,7 @@ public interface UserService extends IService<User> {
* 修改个人中心用户信息 * 修改个人中心用户信息
* *
* @param formData 表单数据 * @param formData 表单数据
* @return * @return {@link Boolean} 是否修改成功
*/ */
boolean updateUserProfile(UserProfileForm formData); boolean updateUserProfile(UserProfileForm formData);
@@ -114,43 +111,49 @@ public interface UserService extends IService<User> {
* *
* @param userId 用户ID * @param userId 用户ID
* @param data 修改密码表单数据 * @param data 修改密码表单数据
* @return * @return {@link Boolean} 是否修改成功
*/ */
boolean changePassword(Long userId, PasswordChangeForm data); boolean changePassword(Long userId, PasswordUpdateForm data);
/** /**
* 重置用户密码 * 重置用户密码
* *
* @param userId 用户ID * @param userId 用户ID
* @param password 重置后的密码 * @param password 重置后的密码
* @return * @return {@link Boolean} 是否重置成功
*/ */
boolean resetPassword(Long userId, String password); boolean resetPassword(Long userId, String password);
/** /**
* 发送验证码 * 发送短信验证码(绑定或更换手机号)
* *
* @param contact 联系方式 * @param mobile 手机号
* @param type 联系方式类型 * @return {@link Boolean} 是否发送成功
* @return
*/ */
boolean sendVerificationCode(String contact, ContactType type); boolean sendMobileCode(String mobile);
/** /**
* 修改当前用户手机号 * 修改当前用户手机号
* *
* @param data 表单数据 * @param data 表单数据
* @return * @return {@link Boolean} 是否修改成功
*/ */
boolean bindMobile(MobileBindingForm data); boolean bindOrChangeMobile(MobileUpdateForm data);
/** /**
* 修改当前用户邮箱 * 发送邮箱验证码(绑定或更换邮箱)
*
* @param email 邮箱
*/
void sendEmailCode(String email);
/**
* 绑定或更换邮箱
* *
* @param data 表单数据 * @param data 表单数据
* @return {@link Boolean} 是否绑定成功 * @return {@link Boolean} 是否绑定成功
*/ */
boolean bindEmail(EmailBindingForm data); boolean bindOrChangeEmail(EmailUpdateForm data);
/** /**
* 获取用户选项列表 * 获取用户选项列表
@@ -182,4 +185,6 @@ public interface UserService extends IService<User> {
* @return {@link UserAuthInfo} * @return {@link UserAuthInfo}
*/ */
UserAuthInfo getUserAuthInfoByMobile(String mobile); UserAuthInfo getUserAuthInfoByMobile(String mobile);
} }

View File

@@ -7,6 +7,7 @@ import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.system.converter.RoleConverter; import com.youlai.boot.system.converter.RoleConverter;
import com.youlai.boot.system.mapper.RoleMapper; import com.youlai.boot.system.mapper.RoleMapper;
import com.youlai.boot.system.model.entity.Role; import com.youlai.boot.system.model.entity.Role;
@@ -88,7 +89,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
); );
// 实体转换 // 实体转换
return roleConverter.entities2Options(roleList); return roleConverter.toOptions(roleList);
} }
/** /**
@@ -157,7 +158,9 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
public boolean updateRoleStatus(Long roleId, Integer status) { public boolean updateRoleStatus(Long roleId, Integer status) {
Role role = this.getById(roleId); Role role = this.getById(roleId);
Assert.isTrue(role != null, "角色不存在"); if (role == null) {
throw new BusinessException("角色不存在");
}
role.setStatus(status); role.setStatus(status);
boolean result = this.updateById(role); boolean result = this.updateById(role);
@@ -172,10 +175,9 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
* 批量删除角色 * 批量删除角色
* *
* @param ids 角色ID多个使用英文逗号(,)分割 * @param ids 角色ID多个使用英文逗号(,)分割
* @return {@link Boolean}
*/ */
@Override @Override
public boolean deleteRoles(String ids) { public void deleteRoles(String ids) {
Assert.isTrue(StrUtil.isNotBlank(ids), "删除的角色ID不能为空"); Assert.isTrue(StrUtil.isNotBlank(ids), "删除的角色ID不能为空");
List<Long> roleIds = Arrays.stream(ids.split(",")) List<Long> roleIds = Arrays.stream(ids.split(","))
.map(Long::parseLong) .map(Long::parseLong)
@@ -195,7 +197,6 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
roleMenuService.refreshRolePermsCache(role.getCode()); roleMenuService.refreshRolePermsCache(role.getCode());
} }
} }
return true;
} }
/** /**
@@ -214,15 +215,15 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
* *
* @param roleId 角色ID * @param roleId 角色ID
* @param menuIds 菜单ID集合 * @param menuIds 菜单ID集合
* @return {@link Boolean}
*/ */
@Override @Override
@Transactional @Transactional
@CacheEvict(cacheNames = "menu", key = "'routes'") @CacheEvict(cacheNames = "menu", key = "'routes'")
public boolean assignMenusToRole(Long roleId, List<Long> menuIds) { public void assignMenusToRole(Long roleId, List<Long> menuIds) {
Role role = this.getById(roleId); Role role = this.getById(roleId);
Assert.isTrue(role != null, "角色不存在"); if (role == null) {
throw new RuntimeException("角色不存在");
}
// 删除角色菜单 // 删除角色菜单
roleMenuService.remove( roleMenuService.remove(
new LambdaQueryWrapper<RoleMenu>() new LambdaQueryWrapper<RoleMenu>()
@@ -239,8 +240,6 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
// 刷新角色的权限缓存 // 刷新角色的权限缓存
roleMenuService.refreshRolePermsCache(role.getCode()); roleMenuService.refreshRolePermsCache(role.getCode());
return true;
} }
/** /**

View File

@@ -11,14 +11,13 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SystemConstants; import com.youlai.boot.common.constant.SystemConstants;
import com.youlai.boot.core.security.manager.TokenManager; import com.youlai.boot.core.security.manager.TokenManager;
import com.youlai.boot.system.enums.ContactType;
import com.youlai.boot.common.model.Option; import com.youlai.boot.common.model.Option;
import com.youlai.boot.shared.mail.service.MailService; import com.youlai.boot.shared.mail.service.MailService;
import com.youlai.boot.shared.sms.enums.SmsTypeEnum;
import com.youlai.boot.shared.sms.service.SmsService; import com.youlai.boot.shared.sms.service.SmsService;
import com.youlai.boot.system.model.entity.User; import com.youlai.boot.system.model.entity.User;
import com.youlai.boot.system.model.entity.UserRole; import com.youlai.boot.system.model.entity.UserRole;
import com.youlai.boot.system.model.form.*; import com.youlai.boot.system.model.form.*;
import com.youlai.boot.config.property.AliyunSmsProperties;
import com.youlai.boot.system.converter.UserConverter; import com.youlai.boot.system.converter.UserConverter;
import com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.system.model.vo.UserProfileVO; import com.youlai.boot.system.model.vo.UserProfileVO;
@@ -40,17 +39,14 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays; import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* 用户业务实现类 * 用户业务实现类
* *
* @author haoxr * @author Ray.Hao
* @since 2022/1/14 * @since 2022/1/14
*/ */
@Service @Service
@@ -69,8 +65,6 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
private final MailService mailService; private final MailService mailService;
private final AliyunSmsProperties aliyunSmsProperties;
private final StringRedisTemplate redisTemplate; private final StringRedisTemplate redisTemplate;
private final TokenManager tokenManager; private final TokenManager tokenManager;
@@ -101,7 +95,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
* 获取用户表单数据 * 获取用户表单数据
* *
* @param userId 用户ID * @param userId 用户ID
* @return * @return {@link UserForm} 用户表单数据
*/ */
@Override @Override
public UserForm getUserFormData(Long userId) { public UserForm getUserFormData(Long userId) {
@@ -112,7 +106,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
* 新增用户 * 新增用户
* *
* @param userForm 用户表单对象 * @param userForm 用户表单对象
* @return * @return true|false
*/ */
@Override @Override
public boolean saveUser(UserForm userForm) { public boolean saveUser(UserForm userForm) {
@@ -144,7 +138,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
* *
* @param userId 用户ID * @param userId 用户ID
* @param userForm 用户表单对象 * @param userForm 用户表单对象
* @return * @return true|false
*/ */
@Override @Override
@Transactional @Transactional
@@ -206,7 +200,6 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
return userAuthInfo; return userAuthInfo;
} }
/** /**
* 根据 openid 获取用户认证信息 * 根据 openid 获取用户认证信息
* *
@@ -225,7 +218,6 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
return userAuthInfo; return userAuthInfo;
} }
/** /**
* 根据手机号获取用户认证信息 * 根据手机号获取用户认证信息
* *
@@ -235,8 +227,13 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
@Override @Override
public UserAuthInfo getUserAuthInfoByMobile(String mobile) { public UserAuthInfo getUserAuthInfoByMobile(String mobile) {
UserAuthInfo userAuthInfo = this.baseMapper.getUserAuthInfoByMobile(mobile); UserAuthInfo userAuthInfo = this.baseMapper.getUserAuthInfoByMobile(mobile);
if (userAuthInfo != null) {
return null; Set<String> roles = userAuthInfo.getRoles();
// 获取最大范围的数据权限
Integer dataScope = roleService.getMaximumDataScope(roles);
userAuthInfo.setDataScope(dataScope);
}
return userAuthInfo;
} }
@@ -319,7 +316,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
* 获取个人中心用户信息 * 获取个人中心用户信息
* *
* @param userId 用户ID * @param userId 用户ID
* @return * @return {@link UserProfileVO} 个人中心用户信息
*/ */
@Override @Override
public UserProfileVO getUserProfile(Long userId) { public UserProfileVO getUserProfile(Long userId) {
@@ -331,7 +328,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
* 修改个人中心用户信息 * 修改个人中心用户信息
* *
* @param formData 表单数据 * @param formData 表单数据
* @return * @return true|false
*/ */
@Override @Override
public boolean updateUserProfile(UserProfileForm formData) { public boolean updateUserProfile(UserProfileForm formData) {
@@ -347,10 +344,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
* *
* @param userId 用户ID * @param userId 用户ID
* @param data 密码修改表单数据 * @param data 密码修改表单数据
* @return * @return true|false
*/ */
@Override @Override
public boolean changePassword(Long userId, PasswordChangeForm data) { public boolean changePassword(Long userId, PasswordUpdateForm data) {
User user = this.getById(userId); User user = this.getById(userId);
if (user == null) { if (user == null) {
@@ -387,7 +384,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
* *
* @param userId 用户ID * @param userId 用户ID
* @param password 密码重置表单数据 * @param password 密码重置表单数据
* @return * @return true|false
*/ */
@Override @Override
public boolean resetPassword(Long userId, String password) { public boolean resetPassword(Long userId, String password) {
@@ -398,47 +395,38 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
} }
/** /**
* 发送验证码 * 发送短信验证码(绑定或更换手机号)
* *
* @param contact 联系方式 手机号/邮箱 * @param mobile 手机号
* @param type 联系方式类型 {@link ContactType} * @return true|false
* @return
*/ */
@Override @Override
public boolean sendVerificationCode(String contact, ContactType type) { public boolean sendMobileCode(String mobile) {
// 随机生成4位验证码 // String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000)); // TODO 为了方便测试,验证码固定为 1234实际开发中在配置了厂商短信服务后可以使用上面的随机验证码
// 发送验证码 String code = "1234";
String verificationCodePrefix = null; Map<String, String> templateParams = new HashMap<>();
switch (type) { templateParams.put("code", code);
case MOBILE: boolean result = smsService.sendSms(mobile, SmsTypeEnum.CHANGE_MOBILE, templateParams);
// 获取修改密码的模板code if (result) {
String changePasswordSmsTemplateCode = aliyunSmsProperties.getTemplateCodes().get("changePassword"); // 缓存验证码5分钟有效用于更换手机号校验
smsService.sendSms(contact, changePasswordSmsTemplateCode, "[{\"code\":\"" + code + "\"}]"); String redisCacheKey = RedisConstants.SMS_CHANGE_CODE_PREFIX + mobile;
verificationCodePrefix = RedisConstants.MOBILE_VERIFICATION_CODE_PREFIX; redisTemplate.opsForValue().set(redisCacheKey, code, 5, TimeUnit.MINUTES);
break;
case EMAIL:
mailService.sendMail(contact, "验证码", "您的验证码是:" + code);
verificationCodePrefix = RedisConstants.EMAIL_VERIFICATION_CODE_PREFIX;
break;
default:
throw new BusinessException("不支持的联系方式类型");
} }
// 存入 redis 用于校验, 5分钟有效 return result;
redisTemplate.opsForValue().set(verificationCodePrefix + contact, code, 5, TimeUnit.MINUTES);
return true;
} }
/** /**
* 修改当前用户手机号 * 绑定或更换手机号
* *
* @param form 表单数据 * @param form 表单数据
* @return * @return true|false
*/ */
@Override @Override
public boolean bindMobile(MobileBindingForm form) { public boolean bindOrChangeMobile(MobileUpdateForm form) {
Long currentUserId = SecurityUtils.getUserId(); Long currentUserId = SecurityUtils.getUserId();
User currentUser = this.getById(currentUserId); User currentUser = this.getById(currentUserId);
@@ -447,13 +435,13 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
} }
// 校验验证码 // 校验验证码
String inputVerificationCode = form.getCode(); String inputVerifyCode = form.getCode();
String mobile = form.getMobile(); String mobile = form.getMobile();
String redisCacheKey = RedisConstants.MOBILE_VERIFICATION_CODE_PREFIX + mobile; String redisCacheKey = RedisConstants.SMS_CHANGE_CODE_PREFIX + mobile;
String cachedVerificationCode = redisTemplate.opsForValue().get(redisCacheKey); String cachedVerifyCode = redisTemplate.opsForValue().get(redisCacheKey);
if (!inputVerificationCode.equals(cachedVerificationCode)) { if (!inputVerifyCode.equals(cachedVerifyCode)) {
throw new BusinessException("验证码错误"); throw new BusinessException("验证码错误");
} }
@@ -465,6 +453,24 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
); );
} }
/**
* 发送邮箱验证码(绑定或更换邮箱)
*
* @param email 邮箱
*/
@Override
public void sendEmailCode(String email) {
// String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
// TODO 为了方便测试,验证码固定为 1234实际开发中在配置了邮箱服务后可以使用上面的随机验证码
String code = "1234";
mailService.sendMail(email, "邮箱验证码", "您的验证码为:" + code + "请在5分钟内使用");
// 缓存验证码5分钟有效用于更换邮箱校验
String redisCacheKey = RedisConstants.EMAIL_CHANGE_CODE_PREFIX + email;
redisTemplate.opsForValue().set(redisCacheKey, code, 5, TimeUnit.MINUTES);
}
/** /**
* 修改当前用户邮箱 * 修改当前用户邮箱
* *
@@ -472,7 +478,8 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
* @return true|false * @return true|false
*/ */
@Override @Override
public boolean bindEmail(EmailBindingForm form) { public boolean bindOrChangeEmail(EmailUpdateForm form) {
Long currentUserId = SecurityUtils.getUserId(); Long currentUserId = SecurityUtils.getUserId();
User currentUser = this.getById(currentUserId); User currentUser = this.getById(currentUserId);
@@ -480,14 +487,15 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
throw new BusinessException("用户不存在"); throw new BusinessException("用户不存在");
} }
// 校验验证码 // 获取前端输入的验证码
String inputVerificationCode = form.getCode(); String inputVerifyCode = form.getCode();
String email = form.getEmail();
String redisCacheKey = RedisConstants.EMAIL_VERIFICATION_CODE_PREFIX + email; // 获取缓存的验证码
String email = form.getEmail();
String redisCacheKey = RedisConstants.EMAIL_CHANGE_CODE_PREFIX + email;
String cachedVerifyCode = redisTemplate.opsForValue().get(redisCacheKey); String cachedVerifyCode = redisTemplate.opsForValue().get(redisCacheKey);
if (!inputVerificationCode.equals(cachedVerifyCode)) { if (!inputVerifyCode.equals(cachedVerifyCode)) {
throw new BusinessException("验证码错误"); throw new BusinessException("验证码错误");
} }
@@ -506,11 +514,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
*/ */
@Override @Override
public List<Option<String>> listUserOptions() { public List<Option<String>> listUserOptions() {
List<User> list = this.list(); List<User> list = this.list(new LambdaQueryWrapper<User>()
if (CollectionUtil.isNotEmpty(list)) { .eq(User::getStatus, 1)
return list.stream().map(user -> new Option<>(user.getId().toString(), user.getNickname())).collect(Collectors.toList()); );
} return userConverter.toOptions(list);
return Collections.emptyList();
} }
} }