From c4463cfcc16bd06687c67b7115729467fc35606a Mon Sep 17 00:00:00 2001 From: haoxr <1490493387@qq.com> Date: Wed, 29 Nov 2023 22:17:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BB=A3=E7=A0=81=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E4=BC=98=E5=8C=96=EF=BC=8C=E7=94=A8=E6=88=B7=E6=9D=83?= =?UTF-8?q?=E9=99=90=E7=BC=93=E5=AD=98=E8=B0=83=E6=95=B4=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E6=9D=83=E9=99=90=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/constant/CacheConstants.java | 27 +++++ .../common/constant/ExcelConstants.java | 16 --- .../common/constant/JwtClaimConstants.java | 38 +++++++ .../common/constant/SecurityConstants.java | 23 ---- .../common/constant/SystemConstants.java | 7 -- .../system/controller/SysMenuController.java | 16 +-- .../system/controller/SysRoleController.java | 18 +-- .../system/controller/SysUserController.java | 3 +- .../system/converter/MenuConverter.java | 1 - .../mybatisplus/config/MybatisPlusConfig.java | 8 +- .../handler/MyMetaObjectHandler.java | 5 +- .../core/security/jwt/JwtTokenFilter.java | 14 ++- .../core/security/jwt/JwtTokenProvider.java | 103 ++++++++++++----- .../core/security/model/SysUserDetails.java | 9 +- .../security/service/PermissionService.java | 85 ++++++++++++-- .../system/filter/VerifyCodeFilter.java | 3 +- .../system/mapper/SysRoleMenuMapper.java | 12 +- .../youlai/system/model/bo/RolePermsBO.java | 26 +++++ .../system/plugin/captcha/CaptchaConfig.java | 75 +++++++++++++ .../plugin/captcha/CaptchaProperties.java | 87 ++++++++++++++ .../aspect/DuplicateSubmitAspect.java | 3 + .../plugin/websocket/WebSocketConfig.java | 1 - .../system/plugin/xxljob/XxlJobConfig.java | 3 +- .../system/service/SysRoleMenuService.java | 21 +++- .../system/service/impl/AuthServiceImpl.java | 25 +++-- .../service/impl/SysDeptServiceImpl.java | 106 +++++++++++++----- .../service/impl/SysMenuServiceImpl.java | 43 ++++--- .../service/impl/SysRoleMenuServiceImpl.java | 23 +++- .../service/impl/SysRoleServiceImpl.java | 57 +++++----- .../service/impl/SysUserServiceImpl.java | 22 ++-- .../resources/mapper/SysRoleMenuMapper.xml | 24 ++++ 31 files changed, 665 insertions(+), 239 deletions(-) create mode 100644 src/main/java/com/youlai/system/common/constant/CacheConstants.java delete mode 100644 src/main/java/com/youlai/system/common/constant/ExcelConstants.java create mode 100644 src/main/java/com/youlai/system/common/constant/JwtClaimConstants.java create mode 100644 src/main/java/com/youlai/system/model/bo/RolePermsBO.java create mode 100644 src/main/java/com/youlai/system/plugin/captcha/CaptchaConfig.java create mode 100644 src/main/java/com/youlai/system/plugin/captcha/CaptchaProperties.java diff --git a/src/main/java/com/youlai/system/common/constant/CacheConstants.java b/src/main/java/com/youlai/system/common/constant/CacheConstants.java new file mode 100644 index 00000000..a702630a --- /dev/null +++ b/src/main/java/com/youlai/system/common/constant/CacheConstants.java @@ -0,0 +1,27 @@ +package com.youlai.system.common.constant; + +/** + * 缓存常量 + * + * @author haoxr + * @since 2023/11/24 + */ +public interface CacheConstants { + + /** + * 验证码缓存前缀 + */ + String CAPTCHA_CODE_PREFIX = "captcha_code:"; + + /** + * 角色和权限缓存前缀 + */ + String ROLE_PERMS_PREFIX = "role_perms:"; + + /** + * 黑名单Token缓存前缀 + */ + String BLACKLIST_TOKEN_PREFIX = "blacklist_token:"; + + +} diff --git a/src/main/java/com/youlai/system/common/constant/ExcelConstants.java b/src/main/java/com/youlai/system/common/constant/ExcelConstants.java deleted file mode 100644 index fcee2003..00000000 --- a/src/main/java/com/youlai/system/common/constant/ExcelConstants.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.youlai.system.common.constant; - -/** - * Excel 常量 - * - * @author haoxr - * @since 3.0.0 - */ -public interface ExcelConstants { - - /** - * Excel 模板目录 - */ - String EXCEL_TEMPLATE_DIR="excel-templates"; - -} diff --git a/src/main/java/com/youlai/system/common/constant/JwtClaimConstants.java b/src/main/java/com/youlai/system/common/constant/JwtClaimConstants.java new file mode 100644 index 00000000..3c312dcd --- /dev/null +++ b/src/main/java/com/youlai/system/common/constant/JwtClaimConstants.java @@ -0,0 +1,38 @@ +package com.youlai.system.common.constant; + +/** + * JWT Claims声明常量 + *

+ * JWT Claims 属于 Payload 的一部分,包含了一些实体(通常指的用户)的状态和额外的元数据。 + * + * @author haoxr + * @since 2023/11/24 + */ +public interface JwtClaimConstants { + + /** + * 用户ID + */ + String USER_ID = "userId"; + + /** + * 用户名 + */ + String USERNAME = "username"; + + /** + * 部门ID + */ + String DEPT_ID = "deptId"; + + /** + * 数据权限 + */ + String DATA_SCOPE = "dataScope"; + + /** + * 权限(角色Code)集合 + */ + String AUTHORITIES = "authorities"; + +} diff --git a/src/main/java/com/youlai/system/common/constant/SecurityConstants.java b/src/main/java/com/youlai/system/common/constant/SecurityConstants.java index 84ddcbb8..89f35d92 100644 --- a/src/main/java/com/youlai/system/common/constant/SecurityConstants.java +++ b/src/main/java/com/youlai/system/common/constant/SecurityConstants.java @@ -13,28 +13,5 @@ public interface SecurityConstants { */ String LOGIN_PATH = "/api/v1/auth/login"; - /** - * Token 前缀 - */ - String TOKEN_PREFIX = "Bearer "; - /** - * 请求头Token的Key - */ - String TOKEN_KEY = "Authorization"; - - /** - * 验证码缓存前缀 - */ - String CAPTCHA_CODE_CACHE_PREFIX = "captcha_code:"; - - /** - * 用户权限集合缓存前缀 - */ - String USER_PERMS_CACHE_PREFIX = "user_perms:"; - - /** - * 黑名单Token缓存前缀 - */ - String BLACK_TOKEN_CACHE_PREFIX = "blacklist_token:"; } diff --git a/src/main/java/com/youlai/system/common/constant/SystemConstants.java b/src/main/java/com/youlai/system/common/constant/SystemConstants.java index b39b3280..6f49580b 100644 --- a/src/main/java/com/youlai/system/common/constant/SystemConstants.java +++ b/src/main/java/com/youlai/system/common/constant/SystemConstants.java @@ -23,11 +23,4 @@ public interface SystemConstants { * 超级管理员角色编码 */ String ROOT_ROLE_CODE = "ROOT"; - - /** - * 超级管理员用户名 - */ - String ROOT_USER_NAME = "root"; - - } diff --git a/src/main/java/com/youlai/system/controller/SysMenuController.java b/src/main/java/com/youlai/system/controller/SysMenuController.java index cc699c5a..8640f602 100644 --- a/src/main/java/com/youlai/system/controller/SysMenuController.java +++ b/src/main/java/com/youlai/system/controller/SysMenuController.java @@ -36,28 +36,28 @@ public class SysMenuController { private final SysMenuService menuService; - @Operation(summary = "菜单列表",security = {@SecurityRequirement(name = "Authorization")}) + @Operation(summary = "菜单列表") @GetMapping public Result> listMenus( @ParameterObject MenuQuery queryParams) { List menuList = menuService.listMenus(queryParams); return Result.success(menuList); } - @Operation(summary = "菜单下拉列表",security = {@SecurityRequirement(name = "Authorization")}) + @Operation(summary = "菜单下拉列表") @GetMapping("/options") public Result listMenuOptions() { List

+ * 如果合法则将 Authentication 设置到 Spring Security Context 上下文中 + * 如果不合法则清空 Spring Security Context 上下文,并直接返回响应 + */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = jwtTokenProvider.resolveToken(request); @@ -35,7 +47,7 @@ public class JwtTokenFilter extends OncePerRequestFilter { } catch (BusinessException ex) { //this is very important, since it guarantees the user is not authenticated at all SecurityContextHolder.clearContext(); - ResponseUtils.writeErrMsg(response,(ResultCode)ex.getResultCode()); + ResponseUtils.writeErrMsg(response, (ResultCode) ex.getResultCode()); return; } diff --git a/src/main/java/com/youlai/system/core/security/jwt/JwtTokenProvider.java b/src/main/java/com/youlai/system/core/security/jwt/JwtTokenProvider.java index e96af11d..0bc5fffc 100644 --- a/src/main/java/com/youlai/system/core/security/jwt/JwtTokenProvider.java +++ b/src/main/java/com/youlai/system/core/security/jwt/JwtTokenProvider.java @@ -1,7 +1,7 @@ package com.youlai.system.core.security.jwt; import cn.hutool.core.convert.Convert; -import com.youlai.system.common.constant.SecurityConstants; +import com.youlai.system.common.constant.JwtClaimConstants; import com.youlai.system.core.security.model.SysUserDetails; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; @@ -10,10 +10,9 @@ import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.DecodingException; import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; -import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -25,27 +24,36 @@ import java.util.*; import java.util.stream.Collectors; /** - * JWT token 管理器 + * JWT token 工具类 + *

+ * 用于生成/校验/解析 JWT Token * * @author haoxr * @since 2023/9/13 */ - @Component public class JwtTokenProvider { - @Resource - private RedisTemplate redisTemplate; - + /** + * 签名密钥,用于签名 Access Token + */ @Value("${jwt.secret-key:123456}") private String secretKey; @Value("${jwt.expiration:7200}") private int expiration; + /** + * Base64 编码后的签名密钥,用于校验/解析 Access Token + */ private byte[] secretKeyBytes; + /** + * 初始化方法 + *

+ * 对签名密钥进行 Base64 编码 + */ @PostConstruct protected void init() { secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); @@ -53,27 +61,25 @@ public class JwtTokenProvider { /** * 创建Token + *

+ * 认证成功后的用户信息会被封装到 Authentication 对象中,然后通过 JwtTokenProvider#createToken(Authentication) 方法创建 Token 字符串 * - * @param authentication - * @return + * @param authentication 用户认证信息 + * @return Token 字符串 */ public String createToken(Authentication authentication) { Claims claims = Jwts.claims().setSubject(authentication.getName()); SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); - claims.put("userId", userDetails.getUserId()); - claims.put("username", claims.getSubject()); - claims.put("deptId", userDetails.getDeptId()); - claims.put("dataScope", userDetails.getDataScope()); + claims.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + claims.put(JwtClaimConstants.USERNAME, claims.getSubject()); // 用户名 + claims.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + claims.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + // claims 中添加角色信息 Set roles = userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); - claims.put("authorities", roles); - - // 权限数据多放入Redis - Set perms = userDetails.getPerms(); - redisTemplate.opsForValue().set(SecurityConstants.USER_PERMS_CACHE_PREFIX + userDetails.getUserId(), perms); - + claims.put(JwtClaimConstants.AUTHORITIES, roles); Date now = new Date(); Date expirationTime = new Date(now.getTime() + expiration * 1000L); @@ -84,47 +90,82 @@ public class JwtTokenProvider { .signWith(Keys.hmacShaKeyFor(getSecretKeyBytes()), SignatureAlgorithm.HS256).compact(); } + + /** + * 根据给定的令牌解析出用户认证信息 + * + * @param token JWT Token + * @return 用户认证信息 + */ public Authentication getAuthentication(String token) { Claims claims = this.getTokenClaims(token); SysUserDetails userDetails = new SysUserDetails(); - userDetails.setUserId(Convert.toLong(claims.get("userId"))); // 用户ID - userDetails.setUsername(Convert.toStr(claims.get("username"))); // 用户名 - userDetails.setDeptId(Convert.toLong(claims.get("deptId"))); // 部门ID - userDetails.setDataScope(Convert.toInt(claims.get("dataScope"))); // 数据权限范围 + userDetails.setUserId(Convert.toLong(claims.get(JwtClaimConstants.USER_ID))); // 用户ID + userDetails.setUsername(Convert.toStr(claims.get(JwtClaimConstants.USERNAME))); // 用户名 + userDetails.setDeptId(Convert.toLong(claims.get(JwtClaimConstants.DEPT_ID))); // 部门ID + userDetails.setDataScope(Convert.toInt(claims.get(JwtClaimConstants.DATA_SCOPE))); // 数据权限范围 - List authorities = ((ArrayList) claims.get("authorities")) + // 角色集合 + Set authorities = ((Set) claims.get(JwtClaimConstants.AUTHORITIES)) .stream() .map(SimpleGrantedAuthority::new) - .toList(); + .collect(Collectors.toSet()); return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); } + + /** + * 从请求头中获取Token + * + * @param req 请求对象 + * @return Token 字符串 + */ public String resolveToken(HttpServletRequest req) { - String bearerToken = req.getHeader("Authorization"); + String bearerToken = req.getHeader(HttpHeaders.AUTHORIZATION); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } + /** + * 校验Token是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ public boolean validateToken(String token) { Jwts.parserBuilder().setSigningKey(getSecretKeyBytes()).build().parseClaimsJws(token); return true; } + /** + * 获取Token中的用户名 + * + * @param token Token + * @return + */ public String getUsername(String token) { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); } - + /** + * 获取Token的Claims,claims中包含了用户的基本信息 + * + * @param token + * @return + */ public Claims getTokenClaims(String token) { - Claims claims = Jwts.parserBuilder().setSigningKey(this.getSecretKeyBytes()).build().parseClaimsJws(token).getBody(); - return claims; + return Jwts.parserBuilder().setSigningKey(this.getSecretKeyBytes()).build().parseClaimsJws(token).getBody(); } - + /** + * 获取签名密钥的字节数组 + * + * @return 签名密钥的字节数组 + */ public byte[] getSecretKeyBytes() { if (secretKeyBytes == null) { try { diff --git a/src/main/java/com/youlai/system/core/security/model/SysUserDetails.java b/src/main/java/com/youlai/system/core/security/model/SysUserDetails.java index fc24af88..578d3a2a 100644 --- a/src/main/java/com/youlai/system/core/security/model/SysUserDetails.java +++ b/src/main/java/com/youlai/system/core/security/model/SysUserDetails.java @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjectUtil; import com.youlai.system.model.dto.UserAuthInfo; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -14,11 +15,13 @@ import java.util.Set; import java.util.stream.Collectors; /** - * Spring Security + * Spring Security 用户对象 * * @author haoxr + * @since 3.0.0 */ @Data +@NoArgsConstructor public class SysUserDetails implements UserDetails { private Long userId; @@ -37,10 +40,6 @@ public class SysUserDetails implements UserDetails { private Integer dataScope; - public SysUserDetails() { - - } - public SysUserDetails(UserAuthInfo user) { this.userId = user.getUserId(); Set roles = user.getRoles(); diff --git a/src/main/java/com/youlai/system/core/security/service/PermissionService.java b/src/main/java/com/youlai/system/core/security/service/PermissionService.java index 99c7e117..98fbf05c 100644 --- a/src/main/java/com/youlai/system/core/security/service/PermissionService.java +++ b/src/main/java/com/youlai/system/core/security/service/PermissionService.java @@ -2,14 +2,18 @@ package com.youlai.system.core.security.service; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; -import com.youlai.system.common.constant.SecurityConstants; +import com.youlai.system.common.constant.CacheConstants; import com.youlai.system.common.util.SecurityUtils; +import com.youlai.system.model.bo.RolePermsBO; +import com.youlai.system.service.SysRoleMenuService; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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; /** @@ -25,15 +29,17 @@ public class PermissionService { private final RedisTemplate redisTemplate; + private final SysRoleMenuService roleMenuService; + /** * 判断当前登录用户是否拥有操作权限 * - * @param perm 权限标识(eg: sys:user:add) + * @param requiredPerm 权限标识(eg: sys:user:add) * @return */ - public boolean hasPerm(String perm) { + public boolean hasPerm(String requiredPerm) { - if (StrUtil.isBlank(perm)) { + if (StrUtil.isBlank(requiredPerm)) { return false; } // 超级管理员放行 @@ -41,21 +47,78 @@ public class PermissionService { return true; } - Long userId = SecurityUtils.getUserId(); - - Set perms = (Set) redisTemplate.opsForValue().get(SecurityConstants.USER_PERMS_CACHE_PREFIX + userId); // 权限数据用户登录成功节点存入redis,详见 JwtTokenManager#createToken() - - if (CollectionUtil.isEmpty(perms)) { + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { return false; } - boolean hasPermission = perms.stream() - .anyMatch(item -> PatternMatchUtils.simpleMatch(perm, item)); // *号匹配任意字符 + boolean hasPermission = false; + for (String roleCode : roleCodes) { + Set rolePerms = (Set) redisTemplate.opsForHash().get(CacheConstants.ROLE_PERMS_PREFIX, roleCode); + if (CollectionUtil.isEmpty(rolePerms)) { + // 无权限 ,判断下一个角色是否有权限 + continue; + } + // 匹配权限,支持通配符 + hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + //rolePerm=sys:user:* requiredPerm=sys:user:add 返回true + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (hasPermission) { + // 匹配到权限,退出循环 + break; + } + } if (!hasPermission) { log.error("用户无访问权限"); } return hasPermission; } + /** + * 初始化权限缓存 + */ + @PostConstruct + public void initPermissionCache() { + refreshPermissionCache(); + } + + /** + * 刷新权限缓存 + */ + public void refreshPermissionCache() { + // 清理权限缓存 + 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 refreshPermissionCache(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); + } + } } diff --git a/src/main/java/com/youlai/system/filter/VerifyCodeFilter.java b/src/main/java/com/youlai/system/filter/VerifyCodeFilter.java index d911938c..d08ffdc2 100644 --- a/src/main/java/com/youlai/system/filter/VerifyCodeFilter.java +++ b/src/main/java/com/youlai/system/filter/VerifyCodeFilter.java @@ -3,6 +3,7 @@ package com.youlai.system.filter; import cn.hutool.captcha.generator.MathGenerator; 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.common.result.ResultCode; import com.youlai.system.common.util.ResponseUtils; @@ -44,7 +45,7 @@ public class VerifyCodeFilter extends OncePerRequestFilter { // 缓存中的验证码 StringRedisTemplate redisTemplate = SpringUtil.getBean("stringRedisTemplate", StringRedisTemplate.class); String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); - String cacheVerifyCode = redisTemplate.opsForValue().get(SecurityConstants.CAPTCHA_CODE_CACHE_PREFIX + verifyCodeKey); + String cacheVerifyCode = redisTemplate.opsForValue().get(CacheConstants.CAPTCHA_CODE_PREFIX + verifyCodeKey); if (cacheVerifyCode == null) { ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_TIMEOUT); } else { diff --git a/src/main/java/com/youlai/system/mapper/SysRoleMenuMapper.java b/src/main/java/com/youlai/system/mapper/SysRoleMenuMapper.java index 7bcaf893..47419189 100644 --- a/src/main/java/com/youlai/system/mapper/SysRoleMenuMapper.java +++ b/src/main/java/com/youlai/system/mapper/SysRoleMenuMapper.java @@ -1,13 +1,14 @@ package com.youlai.system.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.youlai.system.model.bo.RolePermsBO; import com.youlai.system.model.entity.SysRoleMenu; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** - * 角色菜单持久层 + * 角色菜单访问层 * * @author haoxr * @since 2022/6/4 @@ -18,8 +19,13 @@ public interface SysRoleMenuMapper extends BaseMapper { /** * 获取角色拥有的菜单ID集合 * - * @param roleId - * @return + * @param roleId 角色ID + * @return 菜单ID集合 */ List listMenuIdsByRoleId(Long roleId); + + /** + * 获取权限和拥有权限的角色列表 + */ + List getRolePermsList(String roleCode); } diff --git a/src/main/java/com/youlai/system/model/bo/RolePermsBO.java b/src/main/java/com/youlai/system/model/bo/RolePermsBO.java new file mode 100644 index 00000000..6e8c764a --- /dev/null +++ b/src/main/java/com/youlai/system/model/bo/RolePermsBO.java @@ -0,0 +1,26 @@ +package com.youlai.system.model.bo; + +import lombok.Data; + +import java.util.Set; + +/** + * 角色权限业务对象 + * + * @author haoxr + * @since 2023/11/29 + */ +@Data +public class RolePermsBO { + + /** + * 角色编码 + */ + private String roleCode; + + /** + * 权限标识集合 + */ + private Set perms; + +} diff --git a/src/main/java/com/youlai/system/plugin/captcha/CaptchaConfig.java b/src/main/java/com/youlai/system/plugin/captcha/CaptchaConfig.java new file mode 100644 index 00000000..f7c89fd8 --- /dev/null +++ b/src/main/java/com/youlai/system/plugin/captcha/CaptchaConfig.java @@ -0,0 +1,75 @@ +package com.youlai.system.plugin.captcha; + +import cn.hutool.captcha.AbstractCaptcha; +import cn.hutool.captcha.CircleCaptcha; +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 验证码自动装配配置 + * + * @author haoxr + * @since 2023/11/24 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator captchaGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha generator type: " + codeType); + } + } + + /** + * 验证码类 + * + * @return AbstractCaptcha + */ + @Bean + public AbstractCaptcha abstractCaptcha() { + AbstractCaptcha captcha = null; + + String type = captchaProperties.getType(); + int width = captchaProperties.getWidth(); + int height = captchaProperties.getHeight(); + int interfereCount = captchaProperties.getInterfereCount(); + int codeLength = captchaProperties.getCode().getLength(); + + + if ("circle".equalsIgnoreCase(type)) { + captcha = new CircleCaptcha(width, height, codeLength, interfereCount); + } else if ("gif".equalsIgnoreCase(type)) { + return null; + } else if ("line".equalsIgnoreCase(type)) { + return null; + } else if ("shear".equalsIgnoreCase(type)) { + return null; + } else { + throw new IllegalArgumentException("Invalid captcha type: " + type); + } + + captcha.setGenerator(captchaGenerator()); + return captcha; + } + + +} diff --git a/src/main/java/com/youlai/system/plugin/captcha/CaptchaProperties.java b/src/main/java/com/youlai/system/plugin/captcha/CaptchaProperties.java new file mode 100644 index 00000000..1653a708 --- /dev/null +++ b/src/main/java/com/youlai/system/plugin/captcha/CaptchaProperties.java @@ -0,0 +1,87 @@ +package com.youlai.system.plugin.captcha; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 验证码配置 + * + * @author haoxr + * @since 2023/11/24 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/src/main/java/com/youlai/system/plugin/dupsubmit/aspect/DuplicateSubmitAspect.java b/src/main/java/com/youlai/system/plugin/dupsubmit/aspect/DuplicateSubmitAspect.java index 0c5a3fad..23777e92 100644 --- a/src/main/java/com/youlai/system/plugin/dupsubmit/aspect/DuplicateSubmitAspect.java +++ b/src/main/java/com/youlai/system/plugin/dupsubmit/aspect/DuplicateSubmitAspect.java @@ -34,6 +34,9 @@ public class DuplicateSubmitAspect { private final RedissonClient redissonClient; + /** + * JWT token 工具类 + */ private final JwtTokenProvider jwtTokenProvider; private static final String RESUBMIT_LOCK_PREFIX = "LOCK:RESUBMIT:"; diff --git a/src/main/java/com/youlai/system/plugin/websocket/WebSocketConfig.java b/src/main/java/com/youlai/system/plugin/websocket/WebSocketConfig.java index df70524f..85f865c8 100644 --- a/src/main/java/com/youlai/system/plugin/websocket/WebSocketConfig.java +++ b/src/main/java/com/youlai/system/plugin/websocket/WebSocketConfig.java @@ -16,7 +16,6 @@ import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerCo * @since 2.4.0 */ @Configuration -@ConditionalOnProperty(name = "system.config.websocket-enabled")// system.config.websocket-enabled = true 才会自动装配 @EnableWebSocketMessageBroker // 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 @RequiredArgsConstructor public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { diff --git a/src/main/java/com/youlai/system/plugin/xxljob/XxlJobConfig.java b/src/main/java/com/youlai/system/plugin/xxljob/XxlJobConfig.java index 541efbf9..d326dd6e 100644 --- a/src/main/java/com/youlai/system/plugin/xxljob/XxlJobConfig.java +++ b/src/main/java/com/youlai/system/plugin/xxljob/XxlJobConfig.java @@ -13,8 +13,7 @@ import org.springframework.context.annotation.Configuration; * @author xuxueli 2017-04-28 */ @Configuration -// system.config.xxl-job-enabled = true 才会自动装配 -@ConditionalOnProperty(name = "system.config.xxl-job-enabled") +@ConditionalOnProperty(name = "xxl.job.enabled") // xxl.job.enabled = true 才会自动装配 @Slf4j public class XxlJobConfig { diff --git a/src/main/java/com/youlai/system/service/SysRoleMenuService.java b/src/main/java/com/youlai/system/service/SysRoleMenuService.java index d4fb25f5..03786a00 100644 --- a/src/main/java/com/youlai/system/service/SysRoleMenuService.java +++ b/src/main/java/com/youlai/system/service/SysRoleMenuService.java @@ -2,18 +2,33 @@ package com.youlai.system.service; import com.baomidou.mybatisplus.extension.service.IService; +import com.youlai.system.model.bo.RolePermsBO; import com.youlai.system.model.entity.SysRoleMenu; import java.util.List; +import java.util.Set; +/** + * 角色菜单业务接口 + * + * @author haoxr + * @since 2.5.0 + */ public interface SysRoleMenuService extends IService { - /** * 获取角色拥有的菜单ID集合 * - * @param roleId - * @return + * @param roleId 角色ID + * @return 菜单ID集合 */ List listMenuIdsByRoleId(Long roleId); + + + /** + * 获取角色和权限的列表 + * + * @return 角色权限的列表 + */ + List getRolePermsList(String roleCode); } diff --git a/src/main/java/com/youlai/system/service/impl/AuthServiceImpl.java b/src/main/java/com/youlai/system/service/impl/AuthServiceImpl.java index cd548dcb..6ab6fd0a 100644 --- a/src/main/java/com/youlai/system/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/youlai/system/service/impl/AuthServiceImpl.java @@ -1,17 +1,21 @@ package com.youlai.system.service.impl; +import cn.hutool.captcha.AbstractCaptcha; import cn.hutool.captcha.CircleCaptcha; +import cn.hutool.captcha.ICaptcha; import cn.hutool.captcha.generator.MathGenerator; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; -import com.youlai.system.common.constant.SecurityConstants; +import com.youlai.system.common.constant.CacheConstants; import com.youlai.system.core.security.jwt.JwtTokenProvider; import com.youlai.system.model.dto.CaptchaResult; import com.youlai.system.model.dto.LoginResult; +import com.youlai.system.plugin.captcha.CaptchaProperties; import com.youlai.system.service.AuthService; import io.jsonwebtoken.Claims; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -40,6 +44,8 @@ public class AuthServiceImpl implements AuthService { private final AuthenticationManager authenticationManager; private final StringRedisTemplate redisTemplate; private final JwtTokenProvider jwtTokenProvider; + private final AbstractCaptcha abstractCaptcha; + private final CaptchaProperties captchaProperties; /** * 登录 @@ -73,9 +79,9 @@ public class AuthServiceImpl implements AuthService { Date expiration = claims.getExpiration(); if (expiration != null) { long ttl = expiration.getTime() - System.currentTimeMillis(); - redisTemplate.opsForValue().set(SecurityConstants.BLACK_TOKEN_CACHE_PREFIX + jti, null, ttl, TimeUnit.MILLISECONDS); + redisTemplate.opsForValue().set(CacheConstants.BLACKLIST_TOKEN_PREFIX + jti, null, ttl, TimeUnit.MILLISECONDS); } else { - redisTemplate.opsForValue().set(SecurityConstants.BLACK_TOKEN_CACHE_PREFIX + jti, null); + redisTemplate.opsForValue().set(CacheConstants.BLACKLIST_TOKEN_PREFIX + jti, null); } } SecurityContextHolder.clearContext(); @@ -88,18 +94,13 @@ public class AuthServiceImpl implements AuthService { */ @Override public CaptchaResult getCaptcha() { - - MathGenerator mathGenerator=new MathGenerator(1); - CircleCaptcha circleCaptcha =new CircleCaptcha(120,25,4,3); - circleCaptcha.setGenerator(mathGenerator); - circleCaptcha.setFont(new Font(SANS_SERIF, Font.BOLD, 18)); - String captchaCode = circleCaptcha.getCode(); // 验证码 - String captchaBase64 = circleCaptcha.getImageBase64Data(); // 验证码图片Base64 + String captchaCode = abstractCaptcha.getCode(); // 验证码 + String captchaBase64 = abstractCaptcha.getImageBase64Data(); // 验证码图片Base64 // 验证码文本缓存至Redis,用于登录校验 String captchaKey = IdUtil.fastSimpleUUID(); - redisTemplate.opsForValue().set(SecurityConstants.CAPTCHA_CODE_CACHE_PREFIX + captchaKey, captchaCode, - 120, TimeUnit.SECONDS); + redisTemplate.opsForValue().set(CacheConstants.CAPTCHA_CODE_PREFIX + captchaKey, captchaCode, + captchaProperties.getExpireSeconds(), TimeUnit.SECONDS); return CaptchaResult.builder() .captchaKey(captchaKey) diff --git a/src/main/java/com/youlai/system/service/impl/SysDeptServiceImpl.java b/src/main/java/com/youlai/system/service/impl/SysDeptServiceImpl.java index 59a02123..0b703a08 100644 --- a/src/main/java/com/youlai/system/service/impl/SysDeptServiceImpl.java +++ b/src/main/java/com/youlai/system/service/impl/SysDeptServiceImpl.java @@ -1,6 +1,7 @@ package com.youlai.system.service.impl; import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; @@ -17,7 +18,7 @@ import com.youlai.system.service.SysDeptService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -36,7 +37,7 @@ public class SysDeptServiceImpl extends ServiceImpl impl private final DeptConverter deptConverter; /** - * 部门列表 + * 获取部门列表 */ @Override public List listDepartments(DeptQuery queryParams) { @@ -52,29 +53,33 @@ public class SysDeptServiceImpl extends ServiceImpl impl .orderByAsc(SysDept::getSort) ); + if (CollectionUtil.isEmpty(deptList)) { + return Collections.EMPTY_LIST; + } + + // 获取所有部门ID Set deptIds = deptList.stream() .map(SysDept::getId) .collect(Collectors.toSet()); - + // 获取父节点ID Set parentIds = deptList.stream() .map(SysDept::getParentId) .collect(Collectors.toSet()); - + // 获取根节点ID(递归的起点),即父节点ID中不包含在部门ID中的节点,注意这里不能拿顶级部门 O 作为根节点,因为部门筛选的时候 O 会被过滤掉 List rootIds = CollectionUtil.subtractToList(parentIds, deptIds); - List list = new ArrayList<>(); - for (Long rootId : rootIds) { - list.addAll(recurDeptList(rootId, deptList)); - } - return list; + // 递归生成部门树形列表 + return rootIds.stream() + .flatMap(rootId -> recurDeptList(rootId, deptList).stream()) + .toList(); } /** * 递归生成部门树形列表 * - * @param parentId - * @param deptList - * @return + * @param parentId 父ID + * @param deptList 部门列表 + * @return 部门树形列表 */ public List recurDeptList(Long parentId, List deptList) { return deptList.stream() @@ -100,54 +105,93 @@ public class SysDeptServiceImpl extends ServiceImpl impl .select(SysDept::getId, SysDept::getParentId, SysDept::getName) .orderByAsc(SysDept::getSort) ); - - Set parentIds = deptList.stream() - .map(SysDept::getParentId) - .collect(Collectors.toSet()); + if (CollectionUtil.isEmpty(deptList)) { + return Collections.EMPTY_LIST; + } Set deptIds = deptList.stream() .map(SysDept::getId) .collect(Collectors.toSet()); + Set parentIds = deptList.stream() + .map(SysDept::getParentId) + .collect(Collectors.toSet()); + List rootIds = CollectionUtil.subtractToList(parentIds, deptIds); - List