Merge branch 'temp'

This commit is contained in:
Ray.Hao
2025-01-13 18:16:05 +08:00
51 changed files with 779 additions and 410 deletions

View File

@@ -6,7 +6,7 @@
<groupId>com.youlai</groupId> <groupId>com.youlai</groupId>
<artifactId>youlai-boot</artifactId> <artifactId>youlai-boot</artifactId>
<version>2.19.0</version> <version>2.21.0</version>
<description>基于 Java 17 + SpringBoot 3 + Spring Security 构建的权限管理系统。</description> <description>基于 Java 17 + SpringBoot 3 + Spring Security 构建的权限管理系统。</description>
<parent> <parent>

View File

@@ -416,9 +416,9 @@ CREATE TABLE `sys_user` (
-- ---------------------------- -- ----------------------------
-- Records of sys_user -- Records of sys_user
-- ---------------------------- -- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668888', 1, 'youlaitech@163.com', NULL, NULL, NULL, NULL, 0,NULL); INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345677', 1, 'youlaitech@163.com', NULL, NULL, NULL, NULL, 0,NULL);
INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668887', 1, '', '2019-10-10 13:41:22', NULL, '2022-07-31 12:39:30', NULL, 0,NULL); INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345678', 1, '', '2019-10-10 13:41:22', NULL, '2022-07-31 12:39:30', NULL, 0,NULL);
INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668886', 1, 'youlaitech@163.com', '2021-06-05 01:31:29', NULL, '2021-06-05 01:31:29', NULL, 0,NULL); INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345679', 1, 'youlaitech@163.com', '2021-06-05 01:31:29', NULL, '2021-06-05 01:31:29', NULL, 0,NULL);
-- ---------------------------- -- ----------------------------
-- Table structure for sys_user_role -- Table structure for sys_user_role

View File

@@ -374,9 +374,9 @@ CREATE TABLE `sys_user` (
-- ---------------------------- -- ----------------------------
-- Records of sys_user -- Records of sys_user
-- ---------------------------- -- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668888', 1, 'youlaitech@163.com', NULL, NULL, NULL, NULL, 0,NULL); INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345677', 1, 'youlaitech@163.com', NULL, NULL, NULL, NULL, 0,NULL);
INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668887', 1, '', '2019-10-10 13:41:22', NULL, '2022-07-31 12:39:30', NULL, 0,NULL); INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345678', 1, '', '2019-10-10 13:41:22', NULL, '2022-07-31 12:39:30', NULL, 0,NULL);
INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668886', 1, 'youlaitech@163.com', '2021-06-05 01:31:29', NULL, '2021-06-05 01:31:29', NULL, 0,NULL); INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345679', 1, 'youlaitech@163.com', '2021-06-05 01:31:29', NULL, '2021-06-05 01:31:29', NULL, 0,NULL);
-- ---------------------------- -- ----------------------------
-- Table structure for sys_user_role -- Table structure for sys_user_role

View File

@@ -9,35 +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_CHANGE_CODE_PREFIX = "code:sms:change:";
String MOBILE_VERIFICATION_CODE_PREFIX = "VERIFICATION_CODE:MOBILE:";
/** /**
* 邮箱验证码缓存前缀 * 绑定或更换邮箱验证码 Redis 键前缀
*/ */
String EMAIL_VERIFICATION_CODE_PREFIX = "VERIFICATION_CODE:EMAIL:"; String EMAIL_CHANGE_CODE_PREFIX = "code:email:change:";
} }

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,19 +21,15 @@ import java.nio.charset.StandardCharsets;
@Slf4j @Slf4j
public class ResponseUtils { public class ResponseUtils {
/** /**
* 异常消息返回(适用过滤器中处理异常响应) * 异常消息返回(适用过滤器中处理异常响应)
* *
* @param response HttpServletResponse * @param response HttpServletResponse
* @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

@@ -16,6 +16,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.util.AntPathMatcher;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -82,10 +83,12 @@ public class OpenApiConfig {
if (openApi.getPaths() != null) { if (openApi.getPaths() != null) {
openApi.getPaths().forEach((path, pathItem) -> { openApi.getPaths().forEach((path, pathItem) -> {
// 忽略认证的请求无需携带Authorization // 忽略认证的请求无需携带 Authorization
String[] ignoreUrls = securityProperties.getIgnoreUrls(); String[] ignoreUrls = securityProperties.getIgnoreUrls();
if (ArrayUtil.isNotEmpty(ignoreUrls)) { if (ArrayUtil.isNotEmpty(ignoreUrls)) {
if (Stream.of(ignoreUrls).anyMatch(path::equals)) { // Ant 匹配忽略的路径不添加Authorization
AntPathMatcher antPathMatcher = new AntPathMatcher();
if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) {
return; return;
} }
} }

View File

@@ -7,11 +7,12 @@ 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;
import com.youlai.boot.shared.auth.service.impl.JwtTokenService; import com.youlai.boot.core.security.manager.JwtTokenManager;
import com.youlai.boot.system.service.ConfigService; import com.youlai.boot.system.service.ConfigService;
import com.youlai.boot.system.service.UserService; import com.youlai.boot.system.service.UserService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -47,7 +48,7 @@ public class SecurityConfig {
private final RedisTemplate<String, Object> redisTemplate; private final RedisTemplate<String, Object> redisTemplate;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final JwtTokenService jwtTokenService; private final JwtTokenManager jwtTokenService;
private final WxMaService wxMaService; private final WxMaService wxMaService;
private final UserService userService; private final UserService userService;
private final SysUserDetailsService userDetailsService; private final SysUserDetailsService userDetailsService;
@@ -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

@@ -43,8 +43,8 @@ public class AliyunSmsProperties {
private String signName; private String signName;
/** /**
* 模板编码 * 短信模板集合
*/ */
private Map<String, String> templateCodes; private Map<String, String> templates;
} }

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

@@ -0,0 +1,87 @@
package com.youlai.boot.core.security.extension.sms;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.core.security.model.SysUserDetails;
import com.youlai.boot.system.model.dto.UserAuthInfo;
import com.youlai.boot.system.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
* 短信验证码认证 Provider
*
* @author Ray.Hao
* @since 2.17.0
*/
@Slf4j
public class SmsAuthenticationProvider implements AuthenticationProvider {
private final UserService userService;
private final RedisTemplate<String, Object> redisTemplate;
public SmsAuthenticationProvider(UserService userService, RedisTemplate<String, Object> redisTemplate) {
this.userService = userService;
this.redisTemplate = redisTemplate;
}
/**
* 短信验证码认证逻辑,参考 Spring Security 认证密码校验流程
*
* @param authentication 认证对象
* @return 认证后的 Authentication 对象
* @throws AuthenticationException 认证异常
* @see org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate(Authentication)
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String mobile = (String) authentication.getPrincipal();
String inputVerifyCode = (String) authentication.getCredentials();
// 根据手机号获取用户信息
UserAuthInfo userAuthInfo = userService.getUserAuthInfoByMobile(mobile);
if (userAuthInfo == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 检查用户状态是否有效
if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
throw new DisabledException("用户已被禁用");
}
// 校验发送短信验证码的手机号是否与当前登录用户一致
String cachedVerifyCode = (String) redisTemplate.opsForValue().get(RedisConstants.SMS_LOGIN_CODE_PREFIX + mobile);
if (!StrUtil.equals(inputVerifyCode, cachedVerifyCode)) {
throw new BadCredentialsException("验证码错误");
} else {
// 验证成功后删除验证码
redisTemplate.delete(RedisConstants.SMS_LOGIN_CODE_PREFIX + mobile);
}
// 构建认证后的用户详情信息
SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
// 创建已认证的 WeChatAuthenticationToken
return SmsAuthenticationToken.authenticated(
userDetails,
userDetails.getAuthorities()
);
}
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@@ -0,0 +1,78 @@
package com.youlai.boot.core.security.extension.sms;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.io.Serial;
import java.util.Collection;
/**
* 短信验证码认证 Token
*
* @author Ray.Hao
* @since 2.20.0
*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = 621L;
/**
* 认证信息 (手机号)
*/
private final Object principal;
/**
* 凭证信息 (短信验证码)
*/
private final Object credentials;
/**
* 短信验证码认证 Token (未认证)
*
* @param principal 微信用户信息
*/
public SmsAuthenticationToken(Object principal, Object credentials) {
// 没有授权信息时,设置为 null
super(null);
this.principal = principal;
this.credentials = credentials;
// 默认未认证
this.setAuthenticated(false);
}
/**
* 短信验证码认证 Token (已认证)
*
* @param principal 用户信息
* @param authorities 授权信息
*/
public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = null;
// 认证通过
super.setAuthenticated(true);
}
/**
* 认证通过
*
* @param principal 用户信息
* @param authorities 授权信息
* @return
*/
public static SmsAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
return new SmsAuthenticationToken(principal, authorities);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
}

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

@@ -4,7 +4,7 @@ import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.util.ResponseUtils; import com.youlai.boot.common.util.ResponseUtils;
import com.youlai.boot.shared.auth.service.impl.JwtTokenService; import com.youlai.boot.core.security.manager.JwtTokenManager;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@@ -24,10 +24,10 @@ import java.io.IOException;
*/ */
public class JwtAuthenticationFilter extends OncePerRequestFilter { public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenService jwtTokenService; private final JwtTokenManager jwtTokenService;
public JwtAuthenticationFilter(JwtTokenService jwtTokenService) { public JwtAuthenticationFilter(JwtTokenManager jwtTokenService) {
this.jwtTokenService = jwtTokenService; this.jwtTokenService = jwtTokenService;
} }

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.shared.auth.service.impl; package com.youlai.boot.core.security.manager;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
@@ -13,8 +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.shared.auth.model.AuthTokenResponse; import com.youlai.boot.core.security.model.AuthenticationToken;
import com.youlai.boot.shared.auth.service.TokenService;
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;
@@ -38,14 +37,14 @@ import java.util.stream.Collectors;
*/ */
@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") @ConditionalOnProperty(value = "security.session.type", havingValue = "jwt")
@Service @Service
public class JwtTokenService implements TokenService { public class JwtTokenManager implements TokenManager {
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
private final RedisTemplate<String, Object> redisTemplate; private final RedisTemplate<String, Object> redisTemplate;
private final byte[] secretKey; private final byte[] secretKey;
public JwtTokenService(SecurityProperties securityProperties, RedisTemplate<String, Object> redisTemplate) { public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate<String, Object> redisTemplate) {
this.securityProperties = securityProperties; this.securityProperties = securityProperties;
this.redisTemplate = redisTemplate; this.redisTemplate = redisTemplate;
this.secretKey = securityProperties.getJwt().getKey().getBytes(); this.secretKey = securityProperties.getJwt().getKey().getBytes();
@@ -58,14 +57,14 @@ public class JwtTokenService implements TokenService {
* @return 令牌响应对象 * @return 令牌响应对象
*/ */
@Override @Override
public AuthTokenResponse 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 AuthTokenResponse.builder() return AuthenticationToken.builder()
.accessToken(accessToken) .accessToken(accessToken)
.refreshToken(refreshToken) .refreshToken(refreshToken)
.tokenType("Bearer") .tokenType("Bearer")
@@ -164,7 +163,7 @@ public class JwtTokenService implements TokenService {
*/ */
@Override @Override
public AuthTokenResponse refreshToken(String refreshToken) { public AuthenticationToken refreshToken(String refreshToken) {
boolean isValid = validateToken(refreshToken); boolean isValid = validateToken(refreshToken);
if (!isValid) { if (!isValid) {
@@ -175,7 +174,7 @@ public class JwtTokenService implements TokenService {
int accessTokenExpiration = securityProperties.getJwt().getRefreshTokenTimeToLive(); int accessTokenExpiration = securityProperties.getJwt().getRefreshTokenTimeToLive();
String newAccessToken = generateToken(authentication, accessTokenExpiration); String newAccessToken = generateToken(authentication, accessTokenExpiration);
return AuthTokenResponse.builder() return AuthenticationToken.builder()
.accessToken(newAccessToken) .accessToken(newAccessToken)
.refreshToken(refreshToken) .refreshToken(refreshToken)
.tokenType("Bearer") .tokenType("Bearer")

View File

@@ -1,7 +1,6 @@
package com.youlai.boot.shared.auth.service.impl; package com.youlai.boot.core.security.manager;
import com.youlai.boot.shared.auth.model.AuthTokenResponse; import com.youlai.boot.core.security.model.AuthenticationToken;
import com.youlai.boot.shared.auth.service.TokenService;
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;
@@ -14,7 +13,7 @@ import org.springframework.stereotype.Service;
*/ */
@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") @ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token")
@Service @Service
public class RedisTokenService implements TokenService { public class RedisTokenManager implements TokenManager {
/** /**
* 生成令牌 * 生成令牌
@@ -23,7 +22,7 @@ public class RedisTokenService implements TokenService {
* @return * @return
*/ */
@Override @Override
public AuthTokenResponse generateToken(Authentication authentication) { public AuthenticationToken generateToken(Authentication authentication) {
return null; return null;
} }
@@ -56,7 +55,7 @@ public class RedisTokenService implements TokenService {
* @return * @return
*/ */
@Override @Override
public AuthTokenResponse refreshToken(String token) { public AuthenticationToken refreshToken(String token) {
return null; return null;
} }
} }

View File

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

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.shared.auth.model; package com.youlai.boot.core.security.model;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
@@ -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 AuthTokenResponse { public class AuthenticationToken {
@Schema(description = "令牌类型", example = "Bearer") @Schema(description = "令牌类型", example = "Bearer")
private String tokenType; private String tokenType;
@@ -21,7 +21,6 @@ public class AuthTokenResponse {
@Schema(description = "访问令牌") @Schema(description = "访问令牌")
private String accessToken; private String accessToken;
@Schema(description = "刷新令牌") @Schema(description = "刷新令牌")
private String refreshToken; private String refreshToken;

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.shared.auth.model.AuthTokenResponse; 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<AuthTokenResponse> 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
) { ) {
AuthTokenResponse authTokenResponse = authService.login(username, password); AuthenticationToken authenticationToken = authService.login(username, password);
return Result.success(authTokenResponse); 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,27 +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(
AuthTokenResponse authTokenResponse = authService.refreshToken(request); @Parameter(description = "刷新令牌", example = "xxx.xxx.xxx") @RequestParam String refreshToken
return Result.success(authTokenResponse); ) {
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<AuthTokenResponse> wechatLogin( public Result<AuthenticationToken> loginByWechat(
@Parameter(description = "微信授权码", example = "code") @RequestParam String code @Parameter(description = "微信授权码", example = "code") @RequestParam String code
) { ) {
AuthTokenResponse loginResult = authService.wechatLogin(code); AuthenticationToken loginResult = authService.loginByWechat(code);
return Result.success(loginResult);
}
@Operation(summary = "发送登录短信验证码")
@PostMapping("/login/sms/code")
public Result<?> sendLoginVerifyCode(
@Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile
) {
authService.sendSmsLoginCode(mobile);
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); 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.shared.auth.model.AuthTokenResponse; 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 登录结果
*/ */
AuthTokenResponse 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 登录结果
*/ */
AuthTokenResponse refreshToken(RefreshTokenRequest request); AuthenticationToken refreshToken(String refreshToken);
/** /**
* 微信小程序登录 * 微信小程序登录
@@ -47,5 +46,21 @@ public interface AuthService {
* @param code 微信登录code * @param code 微信登录code
* @return 登录结果 * @return 登录结果
*/ */
AuthTokenResponse wechatLogin(String code); AuthenticationToken loginByWechat(String code);
/**
* 发送短信验证码
*
* @param mobile 手机号
*/
void sendSmsLoginCode(String mobile);
/**
* 短信验证码登录
*
* @param mobile 手机号
* @param code 验证码
* @return 登录结果
*/
AuthenticationToken loginBySms(String mobile, String code);
} }

View File

@@ -5,18 +5,21 @@ 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.shared.auth.model.AuthTokenResponse; 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.shared.auth.service.TokenService; import com.youlai.boot.core.security.manager.TokenManager;
import com.youlai.boot.shared.sms.enums.SmsTypeEnum;
import com.youlai.boot.shared.sms.service.SmsService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
@@ -27,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
@@ -41,11 +46,15 @@ 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 TokenManager tokenManager;
private final CodeGenerator codeGenerator;
private final Font captchaFont; private final Font captchaFont;
private final CaptchaProperties captchaProperties; private final CaptchaProperties captchaProperties;
private final TokenService tokenService; private final CodeGenerator codeGenerator;
private final SmsService smsService;
private final RedisTemplate<String, Object> redisTemplate;
/** /**
* 用户名密码登录 * 用户名密码登录
@@ -55,7 +64,7 @@ public class AuthServiceImpl implements AuthService {
* @return 访问令牌 * @return 访问令牌
*/ */
@Override @Override
public AuthTokenResponse 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);
@@ -64,9 +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 使用(已认证)
AuthTokenResponse authTokenResponse = tokenService.generateToken(authentication); AuthenticationToken authenticationTokenResponse =
tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
return authTokenResponse; return authenticationTokenResponse;
} }
/** /**
@@ -76,22 +86,69 @@ public class AuthServiceImpl implements AuthService {
* @return 访问令牌 * @return 访问令牌
*/ */
@Override @Override
public AuthTokenResponse 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 使用(已认证)
AuthTokenResponse authTokenResponse = tokenService.generateToken(authentication); AuthenticationToken authenticationToken = tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
return authTokenResponse; return authenticationToken;
} }
/** /**
* 注销 * 发送短信验证码
*
* @param mobile 手机号
*/
@Override
public void sendSmsLoginCode(String mobile) {
// 随机生成4位验证码
// String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
// TODO 为了方便测试,验证码固定为 1234实际开发中在配置了厂商短信服务后可以使用上面的随机验证码
String code = "1234";
// 发送短信验证码
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() {
@@ -99,7 +156,7 @@ public class AuthServiceImpl implements AuthService {
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) { if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
// 将JWT令牌加入黑名单 // 将JWT令牌加入黑名单
tokenService.blacklistToken(token); tokenManager.blacklistToken(token);
// 清除Security上下文 // 清除Security上下文
SecurityContextHolder.clearContext(); SecurityContextHolder.clearContext();
} }
@@ -111,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();
@@ -143,31 +200,28 @@ 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 AuthTokenResponse refreshToken(RefreshTokenRequest request) { public AuthenticationToken refreshToken(String refreshToken) {
// 验证刷新令牌 // 验证刷新令牌
boolean isValidate = tokenManager.validateToken(refreshToken);
String refreshToken = request.getRefreshToken();
boolean isValidate = tokenService.validateToken(refreshToken);
if (!isValidate) { if (!isValidate) {
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
} }
// 刷新令牌有效,生成新的访问令牌
return tokenService.refreshToken(refreshToken); return tokenManager.refreshToken(refreshToken);
} }

View File

@@ -128,8 +128,8 @@ public class MinioFileService implements FileService {
/** /**
* 删除文件 * 删除文件
* *
* @param filePath 文件路径 * @param filePath 文件路径 http://localhost:9000/default/20221120/test.jpg
* https://oss.youlai.tech/default/20221120/test.jpg *
* @return * @return
*/ */
@Override @Override
@@ -151,10 +151,8 @@ public class MinioFileService implements FileService {
minioClient.removeObject(removeObjectArgs); minioClient.removeObject(removeObjectArgs);
return true; return true;
} catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | } catch (Exception e) {
InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | throw new RuntimeException("文件删除失败", e);
XmlParserException e) {
throw new RuntimeException(e);
} }
} }

View File

@@ -6,6 +6,7 @@ package com.youlai.boot.shared.sms.controller;
* @author Ray * @author Ray
* @since 2.10.0 * @since 2.10.0
*/ */
public class SmsController { public class SmsController {

View File

@@ -0,0 +1,39 @@
package com.youlai.boot.shared.sms.enums;
import com.youlai.boot.common.base.IBaseEnum;
import lombok.Getter;
/**
* 短信类型枚举
* <p>
* value 值对应 application-*.yml 中的 sms.templates.* 配置
*
* @author Ray.Hao
* @since 2.21.0
*/
@Getter
public enum SmsTypeEnum implements IBaseEnum<String> {
/**
* 注册短信验证码
*/
REGISTER("register", "注册短信验证码"),
/**
* 登录短信验证码
*/
LOGIN("login", "登录短信验证码"),
/**
* 修改手机号短信验证码
*/
CHANGE_MOBILE("change-mobile", "修改手机号短信验证码");
private final String value;
private final String label;
SmsTypeEnum(String value, String label) {
this.value = value;
this.label = label;
}
}

View File

@@ -1,11 +1,13 @@
package com.youlai.boot.shared.sms.service; package com.youlai.boot.shared.sms.service;
import com.youlai.boot.shared.sms.enums.SmsTypeEnum;
import java.util.Map;
/** /**
* 短信服务接口层 * 短信服务接口层
* <p>
* SMS = Short Message Service 短信服务
* *
* @author Ray * @author Ray.Hao
* @since 2024/8/17 * @since 2024/8/17
*/ */
public interface SmsService { public interface SmsService {
@@ -13,10 +15,10 @@ public interface SmsService {
/** /**
* 发送短信 * 发送短信
* *
* @param mobile 手机号 13388886666 * @param mobile 手机号 13388886666
* @param templateCode 短信模板 SMS_194640010 * @param smsType 短信模板 SMS_194640010,模板内容:您的验证码为:${code}请在5分钟内使用
* @param templateParam 模板参数 "[{"code":"123456"}]" * @param templateParams 模板参数 [{"code":"123456"}] ,用于替换短信模板中的变量
* @return boolean 是否发送成功 * @return boolean 是否发送成功
*/ */
boolean sendSms(String mobile, String templateCode, String templateParam); boolean sendSms(String mobile, SmsTypeEnum smsType, Map<String, String> templateParams);
} }

View File

@@ -1,23 +1,26 @@
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;
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 lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Map;
/** /**
* 阿里云短信业务类 * 阿里云短信业务类
* *
* @author Ray * @author Ray
* @since 2024/8/17 * @since 2024/8/17
*/ */
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -28,14 +31,15 @@ public class AliyunSmsService implements SmsService {
/** /**
* 发送短信验证码 * 发送短信验证码
* *
* @param mobile 手机号 13388886666 * @param mobile 手机号 13388886666
* @param templateCode 短信模板 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,String templateCode,String templateParam) { public boolean sendSms(String mobile, SmsTypeEnum smsType, Map<String, String> templateParams) {
String templateCode = aliyunSmsProperties.getTemplates().get(smsType.getValue());
DefaultProfile profile = DefaultProfile.getProfile(aliyunSmsProperties.getRegionId(), DefaultProfile profile = DefaultProfile.getProfile(aliyunSmsProperties.getRegionId(),
aliyunSmsProperties.getAccessKeyId(), aliyunSmsProperties.getAccessKeySecret()); aliyunSmsProperties.getAccessKeyId(), aliyunSmsProperties.getAccessKeySecret());
@@ -60,13 +64,11 @@ 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);
return response.getHttpResponse().isSuccess(); return response.getHttpResponse().isSuccess();
} catch (ServerException e) {
e.printStackTrace();
} catch (ClientException e) { } catch (ClientException e) {
e.printStackTrace(); e.printStackTrace();
} }

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,9 +38,9 @@ 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
) { ) {
Page<RolePageVO> result = roleService.getRolePage(queryParams); Page<RolePageVO> result = roleService.getRolePage(queryParams);
return PageResult.success(result); return PageResult.success(result);
@@ -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

@@ -56,6 +56,14 @@ public interface UserMapper extends BaseMapper<User> {
*/ */
UserAuthInfo getUserAuthInfoByOpenId(String openid); UserAuthInfo getUserAuthInfoByOpenId(String openid);
/**
* 根据手机号获取用户认证信息
*
* @param mobile
* @return
*/
UserAuthInfo getUserAuthInfoByMobile(String mobile);
/** /**
* 获取导出用户列表 * 获取导出用户列表
* *
@@ -73,5 +81,4 @@ public interface UserMapper extends BaseMapper<User> {
*/ */
UserBO getUserProfile(Long userId); UserBO getUserProfile(Long userId);
} }

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;
@@ -19,7 +17,7 @@ import java.util.List;
/** /**
* 用户业务接口 * 用户业务接口
* *
* @author haoxr * @author Ray.Hao
* @since 2022/1/14 * @since 2022/1/14
*/ */
public interface UserService extends IService<User> { public interface UserService extends IService<User> {
@@ -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);
/** /**
* 获取用户选项列表 * 获取用户选项列表
@@ -174,4 +177,14 @@ public interface UserService extends IService<User> {
* @param openId 微信 OpenID * @param openId 微信 OpenID
*/ */
void registerOrBindWechatUser(String openId); void registerOrBindWechatUser(String openId);
/**
* 根据手机号获取用户认证信息
*
* @param mobile 手机号
* @return {@link UserAuthInfo}
*/
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

@@ -10,15 +10,14 @@ 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.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.shared.auth.service.TokenService; 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;
@@ -31,7 +30,6 @@ import com.youlai.boot.system.model.dto.UserExportDTO;
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;
import com.youlai.boot.core.security.service.PermissionService; import com.youlai.boot.core.security.service.PermissionService;
import com.youlai.boot.system.service.RoleMenuService;
import com.youlai.boot.system.service.RoleService; import com.youlai.boot.system.service.RoleService;
import com.youlai.boot.system.service.UserRoleService; import com.youlai.boot.system.service.UserRoleService;
import com.youlai.boot.system.service.UserService; import com.youlai.boot.system.service.UserService;
@@ -41,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
@@ -62,8 +57,6 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
private final UserRoleService userRoleService; private final UserRoleService userRoleService;
private final RoleMenuService roleMenuService;
private final RoleService roleService; private final RoleService roleService;
private final PermissionService permissionService; private final PermissionService permissionService;
@@ -72,11 +65,9 @@ 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 TokenService tokenService; private final TokenManager tokenManager;
private final UserConverter userConverter; private final UserConverter userConverter;
@@ -104,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) {
@@ -115,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) {
@@ -147,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
@@ -209,7 +200,6 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
return userAuthInfo; return userAuthInfo;
} }
/** /**
* 根据 openid 获取用户认证信息 * 根据 openid 获取用户认证信息
* *
@@ -228,6 +218,25 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
return userAuthInfo; return userAuthInfo;
} }
/**
* 根据手机号获取用户认证信息
*
* @param mobile 手机号
* @return {@link UserAuthInfo}
*/
@Override
public UserAuthInfo getUserAuthInfoByMobile(String mobile) {
UserAuthInfo userAuthInfo = this.baseMapper.getUserAuthInfoByMobile(mobile);
if (userAuthInfo != null) {
Set<String> roles = userAuthInfo.getRoles();
// 获取最大范围的数据权限
Integer dataScope = roleService.getMaximumDataScope(roles);
userAuthInfo.setDataScope(dataScope);
}
return userAuthInfo;
}
/** /**
* 根据微信 OpenID 注册或绑定用户 * 根据微信 OpenID 注册或绑定用户
* <p> * <p>
@@ -307,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) {
@@ -319,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) {
@@ -335,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) {
@@ -365,7 +374,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
if (result) { if (result) {
// 加入黑名单,重新登录 // 加入黑名单,重新登录
String accessToken = SecurityUtils.getTokenFromRequest(); String accessToken = SecurityUtils.getTokenFromRequest();
tokenService.blacklistToken(accessToken); tokenManager.blacklistToken(accessToken);
} }
return result; return result;
} }
@@ -375,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) {
@@ -386,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);
@@ -435,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("验证码错误");
} }
@@ -453,14 +453,33 @@ 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);
}
/** /**
* 修改当前用户邮箱 * 修改当前用户邮箱
* *
* @param form 表单数据 * @param form 表单数据
* @return * @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);
@@ -468,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 email = form.getEmail();
String redisCacheKey = RedisConstants.EMAIL_CHANGE_CODE_PREFIX + email;
String cachedVerifyCode = redisTemplate.opsForValue().get(redisCacheKey);
String redisCacheKey = RedisConstants.EMAIL_VERIFICATION_CODE_PREFIX + email; if (!inputVerifyCode.equals(cachedVerifyCode)) {
String cachedVerificationCode = redisTemplate.opsForValue().get(redisCacheKey);
if (cachedVerificationCode == null || !inputVerificationCode.equals(cachedVerificationCode)) {
throw new BusinessException("验证码错误"); throw new BusinessException("验证码错误");
} }
@@ -494,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();
} }
} }

View File

@@ -87,8 +87,7 @@ security:
refresh-token-time-to-live: 604800 refresh-token-time-to-live: 604800
# 无需认证的请求路径 # 无需认证的请求路径
ignore-urls: ignore-urls:
- /api/v1/auth/login # 用户登录接口 - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录)
- /api/v1/auth/wechat-login # 微信登录接口
- /api/v1/auth/captcha # 验证码获取接口 - /api/v1/auth/captcha # 验证码获取接口
- /api/v1/auth/refresh-token # 刷新令牌接口 - /api/v1/auth/refresh-token # 刷新令牌接口
- /ws/** # WebSocket接口 - /ws/** # WebSocket接口
@@ -103,25 +102,25 @@ security:
# 文件存储配置 # 文件存储配置
oss: oss:
# OSS 类型 (目前支持aliyun、minio) # OSS 类型 (目前支持aliyun、minio、local)
type: minio type: minio
# MinIO 对象存储服务 # MinIO 对象存储服务
minio: minio:
# 服务Endpoint # MinIO 服务地址
endpoint: http://localhost:9000 endpoint: http://localhost:9000
# 访问凭据 # 访问凭据
access-key: minioadmin access-key: minioadmin
# 凭据密钥 # 凭据密钥
secret-key: minioadmin secret-key: minioadmin
# 存储桶名称 # 存储桶名称
bucket-name: public bucket-name: youlai
# (可选)自定义域名,如果配置了域名,生成的文件URL是域名格式未配置则URL则是IP格式 (eg: https://oss.youlai.tech) # (可选) 自定义域名:配置后,文件 URL 会使用该域名格式
custom-domain: custom-domain:
# 阿里云OSS对象存储服务 # 阿里云OSS对象存储服务
aliyun: aliyun:
# 服务Endpoint # 服务Endpoint
endpoint: oss-cn-hangzhou.aliyuncs.com endpoint: oss-cn-hangzhou.aliyuncs.com
# 访问凭据 # 访问凭据`
access-key-id: your-access-key-id access-key-id: your-access-key-id
# 凭据密钥 # 凭据密钥
access-key-secret: your-access-key-secret access-key-secret: your-access-key-secret
@@ -140,19 +139,19 @@ sms:
domain: dysmsapi.aliyuncs.com domain: dysmsapi.aliyuncs.com
regionId: cn-shanghai regionId: cn-shanghai
signName: 有来技术 signName: 有来技术
templateCodes: templates:
# 注册(预留) # 注册短信验证码模板
register: SMS_22xxx771 register: SMS_22xxx771
# 登录(预留) # 登录短信验证码模板
login: SMS_22xxx772 login: SMS_22xxx772
# 修改密码 # 修改手机号短信验证码模板
changePassword: SMS_22xxx773 change-mobile: SMS_22xxx773
# springdoc配置 https://springdoc.org/properties.html # springdoc配置 https://springdoc.org/properties.html
springdoc: springdoc:
swagger-ui: swagger-ui:
path: /swagger-ui.html path: /swagger-ui.html
operationsSorter: alpha operations-sorter: alpha
tags-sorter: alpha tags-sorter: alpha
api-docs: api-docs:
path: /v3/api-docs path: /v3/api-docs
@@ -195,32 +194,32 @@ xxl:
# 验证码配置 # 验证码配置
captcha: captcha:
# 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 # 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码
type: circle type: circle
# 验证码宽度 # 验证码宽度
width: 120 width: 120
# 验证码高度 # 验证码高度
height: 40 height: 40
# 验证码干扰元素个数 # 验证码干扰元素个数
interfere-count: 2 interfere-count: 2
# 文本透明度(0.0-1.0) # 文本透明度(0.0-1.0)
text-alpha: 0.8 text-alpha: 0.8
# 验证码字符配置 # 验证码字符配置
code: code:
# 验证码字符类型 math-算术|random-随机字符 # 验证码字符类型 math-算术|random-随机字符
type: math type: math
# 验证码字符长度type=算术时,表示运算位数(1:个位数运算 2:十位数运算)type=随机字符时,表示字符个数 # 验证码字符长度type=算术时,表示运算位数(1:个位数运算 2:十位数运算)type=随机字符时,表示字符个数
length: 1 length: 1
# 验证码字体 # 验证码字体
font: font:
# 字体名称 Dialog|DialogInput|Monospaced|Serif|SansSerif # 字体名称 Dialog|DialogInput|Monospaced|Serif|SansSerif
name: SansSerif name: SansSerif
# 字体样式 0-普通|1-粗体|2-斜体 # 字体样式 0-普通|1-粗体|2-斜体
weight: 1 weight: 1
# 字体大小 # 字体大小
size: 24 size: 24
# 验证码有效期(秒) # 验证码有效期(秒)
expire-seconds: 120 expire-seconds: 120
# 微信小程配置 # 微信小程配置
wx: wx:

View File

@@ -86,8 +86,7 @@ security:
refresh-token-time-to-live: 604800 refresh-token-time-to-live: 604800
# 无需认证的请求路径 # 无需认证的请求路径
ignore-urls: ignore-urls:
- /api/v1/auth/login # 用户登录接口 - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录)
- /api/v1/auth/wechat-login # 微信登录接口
- /api/v1/auth/captcha # 验证码获取接口 - /api/v1/auth/captcha # 验证码获取接口
- /api/v1/auth/refresh-token # 刷新令牌接口 - /api/v1/auth/refresh-token # 刷新令牌接口
- /ws/** # WebSocket接口 - /ws/** # WebSocket接口
@@ -138,13 +137,13 @@ sms:
domain: dysmsapi.aliyuncs.com domain: dysmsapi.aliyuncs.com
regionId: cn-shanghai regionId: cn-shanghai
signName: 有来技术 signName: 有来技术
templateCodes: templates:
# 注册(预留) # 注册短信验证码模板
register: SMS_22xxx771 register: SMS_22xxx771
# 登录(预留) # 登录短信验证码模板
login: SMS_22xxx772 login: SMS_22xxx772
# 修改密码 # 修改手机号短信验证码模板
changePassword: SMS_22xxx773 change-mobile: SMS_22xxx773
# springdoc配置 https://springdoc.org/properties.html # springdoc配置 https://springdoc.org/properties.html
springdoc: springdoc:

View File

@@ -159,6 +159,24 @@
t1.openid = #{openid} AND t1.is_deleted = 0 t1.openid = #{openid} AND t1.is_deleted = 0
</select> </select>
<!-- 根据手机号获取用户的认证信息 -->
<select id="getUserAuthInfoByMobile" resultMap="UserAuthMap">
SELECT
t1.id userId,
t1.username,
t1.nickname,
t1.PASSWORD,
t1.STATUS,
t1.dept_id ,
t3.CODE
FROM
sys_user t1
LEFT JOIN sys_user_role t2 ON t2.user_id = t1.id
LEFT JOIN sys_role t3 ON t3.id = t2.role_id
WHERE
t1.mobile = #{mobile} AND t1.is_deleted = 0
</select>
<!-- 获取用户导出列表 --> <!-- 获取用户导出列表 -->
<select id="listExportUsers" resultType="com.youlai.boot.system.model.dto.UserExportDTO"> <select id="listExportUsers" resultType="com.youlai.boot.system.model.dto.UserExportDTO">
SELECT SELECT
@@ -215,4 +233,6 @@
</select> </select>
</mapper> </mapper>