diff --git a/README.md b/README.md
index ba547feb..b1b79660 100644
--- a/README.md
+++ b/README.md
@@ -58,9 +58,6 @@ youlai-boot
├── XxlJobConfig # XXL-JOB 自动装配配置
├── controller # 控制层
├── converter # MapStruct 转换器
- ├── core # 核心模块
- ├── security # Spring Security 安全配置和扩展
- ├── mybatis # Mybatis-Plus 配置和插件
├── filter # 过滤器
├── RequestLogFilter # 请求日志过滤器
├── VerifyCodeFilter # 验证码过滤器
diff --git a/pom.xml b/pom.xml
index 20574b6d..15b60620 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
com.youlai
youlai-boot
2.6.0
- 基于 Java 17 + SpringBoot 3 构建的权限管理系统。
+ 基于 Java 17 + SpringBoot 3 + Spring Security 构建的权限管理系统。
org.springframework.boot
diff --git a/src/main/java/com/youlai/system/common/base/BaseEntity.java b/src/main/java/com/youlai/system/common/base/BaseEntity.java
index fea13d45..aead014c 100644
--- a/src/main/java/com/youlai/system/common/base/BaseEntity.java
+++ b/src/main/java/com/youlai/system/common/base/BaseEntity.java
@@ -6,11 +6,14 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
+import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
public class BaseEntity implements Serializable {
+
+ @Serial
private static final long serialVersionUID = 1L;
@TableField(fill = FieldFill.INSERT)
diff --git a/src/main/java/com/youlai/system/common/base/BasePageQuery.java b/src/main/java/com/youlai/system/common/base/BasePageQuery.java
index 4776ee0f..e6b54b2c 100644
--- a/src/main/java/com/youlai/system/common/base/BasePageQuery.java
+++ b/src/main/java/com/youlai/system/common/base/BasePageQuery.java
@@ -14,9 +14,9 @@ import lombok.Data;
@Schema
public class BasePageQuery {
- @Schema(description = "页码", required = true, example = "1")
+ @Schema(description = "页码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private int pageNum = 1;
- @Schema(description = "每页记录数", required = true, example = "10")
+ @Schema(description = "每页记录数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private int pageSize = 10;
}
diff --git a/src/main/java/com/youlai/system/common/base/BaseVO.java b/src/main/java/com/youlai/system/common/base/BaseVO.java
index c33d932e..51cc6f77 100644
--- a/src/main/java/com/youlai/system/common/base/BaseVO.java
+++ b/src/main/java/com/youlai/system/common/base/BaseVO.java
@@ -3,6 +3,7 @@ package com.youlai.system.common.base;
import lombok.Data;
import lombok.ToString;
+import java.io.Serial;
import java.io.Serializable;
/**
@@ -15,5 +16,6 @@ import java.io.Serializable;
@ToString
public class BaseVO implements Serializable {
+ @Serial
private static final long serialVersionUID = 1L;
}
diff --git a/src/main/java/com/youlai/system/plugin/captcha/CaptchaConfig.java b/src/main/java/com/youlai/system/config/CaptchaConfig.java
similarity index 94%
rename from src/main/java/com/youlai/system/plugin/captcha/CaptchaConfig.java
rename to src/main/java/com/youlai/system/config/CaptchaConfig.java
index 198eed28..8f55637d 100644
--- a/src/main/java/com/youlai/system/plugin/captcha/CaptchaConfig.java
+++ b/src/main/java/com/youlai/system/config/CaptchaConfig.java
@@ -1,8 +1,9 @@
-package com.youlai.system.plugin.captcha;
+package com.youlai.system.config;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.RandomGenerator;
+import com.youlai.system.plugin.captcha.CaptchaProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/src/main/java/com/youlai/system/config/SecurityConfig.java b/src/main/java/com/youlai/system/config/SecurityConfig.java
index 2d0de6c0..ed7455ce 100644
--- a/src/main/java/com/youlai/system/config/SecurityConfig.java
+++ b/src/main/java/com/youlai/system/config/SecurityConfig.java
@@ -1,10 +1,11 @@
package com.youlai.system.config;
-import com.youlai.system.common.constant.SecurityConstants;
+import cn.hutool.captcha.generator.CodeGenerator;
+import com.youlai.system.security.constant.SecurityConstants;
import com.youlai.system.security.exception.MyAccessDeniedHandler;
import com.youlai.system.security.exception.MyAuthenticationEntryPoint;
-import com.youlai.system.filter.JwtTokenFilter;
-import com.youlai.system.filter.VerifyCodeFilter;
+import com.youlai.system.filter.JwtValidationFilter;
+import com.youlai.system.filter.CaptchaValidationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -37,6 +38,8 @@ public class SecurityConfig {
private final MyAuthenticationEntryPoint authenticationEntryPoint;
private final MyAccessDeniedHandler accessDeniedHandler;
private final RedisTemplate redisTemplate;
+ private final CodeGenerator codeGenerator;
+
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
@@ -56,9 +59,9 @@ public class SecurityConfig {
;
// 验证码校验过滤器
- http.addFilterBefore(new VerifyCodeFilter(), UsernamePasswordAuthenticationFilter.class);
+ http.addFilterBefore(new CaptchaValidationFilter(redisTemplate,codeGenerator), UsernamePasswordAuthenticationFilter.class);
// JWT 校验过滤器
- http.addFilterBefore(new JwtTokenFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class);
+ http.addFilterBefore(new JwtValidationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
diff --git a/src/main/java/com/youlai/system/controller/SysRoleController.java b/src/main/java/com/youlai/system/controller/SysRoleController.java
index a39d321a..aff591a1 100644
--- a/src/main/java/com/youlai/system/controller/SysRoleController.java
+++ b/src/main/java/com/youlai/system/controller/SysRoleController.java
@@ -10,7 +10,6 @@ import com.youlai.system.model.query.RolePageQuery;
import com.youlai.system.model.vo.RolePageVO;
import com.youlai.system.service.SysRoleService;
import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
@@ -76,7 +75,7 @@ public class SysRoleController {
@DeleteMapping("/{ids}")
@PreAuthorize("@ss.hasPerm('sys:role:delete')")
public Result deleteRoles(
- @Parameter(description = "删除角色,多个以英文逗号(,)分割") @PathVariable String ids
+ @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids
) {
boolean result = roleService.deleteRoles(ids);
return Result.judge(result);
diff --git a/src/main/java/com/youlai/system/controller/SysUserController.java b/src/main/java/com/youlai/system/controller/SysUserController.java
index 86ebc436..001994b6 100644
--- a/src/main/java/com/youlai/system/controller/SysUserController.java
+++ b/src/main/java/com/youlai/system/controller/SysUserController.java
@@ -102,7 +102,7 @@ public class SysUserController {
@Operation(summary = "修改用户密码")
@PatchMapping(value = "/{userId}/password")
- @PreAuthorize("@ss.hasPerm('sys:user:reset_pwd')")
+ @PreAuthorize("@ss.hasPerm('sys:user:password:edit')")
public Result updatePassword(
@Parameter(description = "用户ID") @PathVariable Long userId,
@RequestParam String password
diff --git a/src/main/java/com/youlai/system/filter/VerifyCodeFilter.java b/src/main/java/com/youlai/system/filter/CaptchaValidationFilter.java
similarity index 68%
rename from src/main/java/com/youlai/system/filter/VerifyCodeFilter.java
rename to src/main/java/com/youlai/system/filter/CaptchaValidationFilter.java
index d08ffdc2..f85088b1 100644
--- a/src/main/java/com/youlai/system/filter/VerifyCodeFilter.java
+++ b/src/main/java/com/youlai/system/filter/CaptchaValidationFilter.java
@@ -1,17 +1,16 @@
package com.youlai.system.filter;
-import cn.hutool.captcha.generator.MathGenerator;
+import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.StrUtil;
-import cn.hutool.extra.spring.SpringUtil;
import com.youlai.system.common.constant.CacheConstants;
-import com.youlai.system.common.constant.SecurityConstants;
+import com.youlai.system.security.constant.SecurityConstants;
import com.youlai.system.common.result.ResultCode;
import com.youlai.system.common.util.ResponseUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
-import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
@@ -19,39 +18,47 @@ import java.io.IOException;
/**
- * 验证码校验过滤器
+ * 图形验证码校验过滤器
*
* @author haoxr
* @since 2022/10/1
*/
-public class VerifyCodeFilter extends OncePerRequestFilter {
+public class CaptchaValidationFilter extends OncePerRequestFilter {
private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, "POST");
public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode";
public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey";
+ private final RedisTemplate redisTemplate;
+
+ private final CodeGenerator codeGenerator;
+
+ public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) {
+ this.redisTemplate = redisTemplate;
+ this.codeGenerator = codeGenerator;
+ }
+
+
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// 检验登录接口的验证码
if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
// 请求中的验证码
- String verifyCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME);
+ String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME);
// TODO 兼容没有验证码的版本(线上请移除这个判断)
- if (StrUtil.isBlank(verifyCode)) {
+ if (StrUtil.isBlank(captchaCode)) {
chain.doFilter(request, response);
return;
}
// 缓存中的验证码
- StringRedisTemplate redisTemplate = SpringUtil.getBean("stringRedisTemplate", StringRedisTemplate.class);
String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME);
- String cacheVerifyCode = redisTemplate.opsForValue().get(CacheConstants.CAPTCHA_CODE_PREFIX + verifyCodeKey);
+ String cacheVerifyCode = (String) redisTemplate.opsForValue().get(CacheConstants.CAPTCHA_CODE_PREFIX + verifyCodeKey);
if (cacheVerifyCode == null) {
ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_TIMEOUT);
} else {
// 验证码比对
- MathGenerator mathGenerator = new MathGenerator();
- if (mathGenerator.verify(cacheVerifyCode, verifyCode)) {
+ if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
chain.doFilter(request, response);
} else {
ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_ERROR);
diff --git a/src/main/java/com/youlai/system/filter/JwtTokenFilter.java b/src/main/java/com/youlai/system/filter/JwtValidationFilter.java
similarity index 93%
rename from src/main/java/com/youlai/system/filter/JwtTokenFilter.java
rename to src/main/java/com/youlai/system/filter/JwtValidationFilter.java
index e5b72150..15ddfc85 100644
--- a/src/main/java/com/youlai/system/filter/JwtTokenFilter.java
+++ b/src/main/java/com/youlai/system/filter/JwtValidationFilter.java
@@ -22,16 +22,16 @@ import java.io.IOException;
import java.util.Map;
/**
- * JWT token 过滤器
+ * JWT token 校验过滤器
*
* @author haoxr
* @since 2023/9/13
*/
-public class JwtTokenFilter extends OncePerRequestFilter {
+public class JwtValidationFilter extends OncePerRequestFilter {
private final RedisTemplate redisTemplate;
- public JwtTokenFilter(RedisTemplate redisTemplate) {
+ public JwtValidationFilter(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
diff --git a/src/main/java/com/youlai/system/common/constant/JwtClaimConstants.java b/src/main/java/com/youlai/system/security/constant/JwtClaimConstants.java
similarity index 82%
rename from src/main/java/com/youlai/system/common/constant/JwtClaimConstants.java
rename to src/main/java/com/youlai/system/security/constant/JwtClaimConstants.java
index 3c312dcd..e662ddb9 100644
--- a/src/main/java/com/youlai/system/common/constant/JwtClaimConstants.java
+++ b/src/main/java/com/youlai/system/security/constant/JwtClaimConstants.java
@@ -1,4 +1,4 @@
-package com.youlai.system.common.constant;
+package com.youlai.system.security.constant;
/**
* JWT Claims声明常量
@@ -15,11 +15,6 @@ public interface JwtClaimConstants {
*/
String USER_ID = "userId";
- /**
- * 用户名
- */
- String USERNAME = "username";
-
/**
* 部门ID
*/
diff --git a/src/main/java/com/youlai/system/common/constant/SecurityConstants.java b/src/main/java/com/youlai/system/security/constant/SecurityConstants.java
similarity index 81%
rename from src/main/java/com/youlai/system/common/constant/SecurityConstants.java
rename to src/main/java/com/youlai/system/security/constant/SecurityConstants.java
index 89f35d92..689fcfcd 100644
--- a/src/main/java/com/youlai/system/common/constant/SecurityConstants.java
+++ b/src/main/java/com/youlai/system/security/constant/SecurityConstants.java
@@ -1,4 +1,4 @@
-package com.youlai.system.common.constant;
+package com.youlai.system.security.constant;
/**
* Security 常量
diff --git a/src/main/java/com/youlai/system/security/service/PermissionService.java b/src/main/java/com/youlai/system/security/service/PermissionService.java
index bd50508a..c4dbe68f 100644
--- a/src/main/java/com/youlai/system/security/service/PermissionService.java
+++ b/src/main/java/com/youlai/system/security/service/PermissionService.java
@@ -13,8 +13,7 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.PatternMatchUtils;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
/**
* SpringSecurity 权限校验
@@ -27,10 +26,105 @@ import java.util.Set;
@Slf4j
public class PermissionService {
- private final RedisTemplate redisTemplate;
+ private final RedisTemplate redisTemplate;
private final SysRoleMenuService roleMenuService;
+
+ /**
+ * 初始化权限缓存
+ */
+ @PostConstruct
+ public void initRolePermsCache() {
+ refreshRolePermsCache();
+ }
+
+ /**
+ * 刷新权限缓存
+ */
+ public void refreshRolePermsCache() {
+ // 清理权限缓存
+ redisTemplate.opsForHash().delete(CacheConstants.ROLE_PERMS_PREFIX, "*");
+
+ List list = roleMenuService.getRolePermsList(null);
+ if (CollectionUtil.isNotEmpty(list)) {
+ list.forEach(item -> {
+ String roleCode = item.getRoleCode();
+ Set perms = item.getPerms();
+ redisTemplate.opsForHash().put(CacheConstants.ROLE_PERMS_PREFIX, roleCode, perms);
+ });
+ }
+ }
+
+ /**
+ * 刷新权限缓存
+ */
+ public void refreshRolePermsCache(String roleCode) {
+ // 清理权限缓存
+ redisTemplate.opsForHash().delete(CacheConstants.ROLE_PERMS_PREFIX, roleCode);
+
+ List list = roleMenuService.getRolePermsList(roleCode);
+ if (CollectionUtil.isNotEmpty(list)) {
+ RolePermsBO rolePerms = list.get(0);
+ if (rolePerms == null) {
+ return;
+ }
+
+ Set perms = rolePerms.getPerms();
+ redisTemplate.opsForHash().put(CacheConstants.ROLE_PERMS_PREFIX, roleCode, perms);
+ }
+ }
+
+ /**
+ * 刷新权限缓存 (角色编码变更时调用)
+ */
+ public void refreshRolePermsCache(String oldRoleCode,String newRoleCode) {
+ // 清理旧角色权限缓存
+ redisTemplate.opsForHash().delete(CacheConstants.ROLE_PERMS_PREFIX, oldRoleCode);
+
+ // 添加新角色权限缓存
+ List list = roleMenuService.getRolePermsList(newRoleCode);
+ if (CollectionUtil.isNotEmpty(list)) {
+ RolePermsBO rolePerms = list.get(0);
+ if (rolePerms == null) {
+ return;
+ }
+
+ Set perms = rolePerms.getPerms();
+ redisTemplate.opsForHash().put(CacheConstants.ROLE_PERMS_PREFIX, newRoleCode, perms);
+ }
+ }
+
+
+ /**
+ * 获取角色权限列表
+ *
+ * @param roleCodes 角色编码集合
+ * @return 角色权限列表
+ */
+ public Set getRolePermsFormCache(Set roleCodes) {
+ // 检查输入是否为空
+ if (CollectionUtil.isEmpty(roleCodes)) {
+ return Collections.emptySet();
+ }
+
+ Set perms = new HashSet<>();
+ // 从缓存中一次性获取所有角色的权限
+ Collection