From 7ecf34cf437e4eded217b6f8418e70ced58ba4cd Mon Sep 17 00:00:00 2001 From: Theo <971366405@qq.com> Date: Fri, 13 Jun 2025 16:09:23 +0800 Subject: [PATCH 1/5] =?UTF-8?q?refactor(sql):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF=E8=A1=A8=E7=B4=A2=E5=BC=95?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=20SQL=20=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将用户信息表中的 UNIQUE INDEX `login_name` 修改为非唯一索引 KEY `login_name` --- sql/mysql/youlai_boot.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sql/mysql/youlai_boot.sql b/sql/mysql/youlai_boot.sql index d8a7927b..c47f1559 100644 --- a/sql/mysql/youlai_boot.sql +++ b/sql/mysql/youlai_boot.sql @@ -375,7 +375,7 @@ CREATE TABLE `sys_user` ( `is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)', `openid` char(28) COMMENT '微信 openid', PRIMARY KEY (`id`) USING BTREE, - UNIQUE INDEX `login_name`(`username` ASC) USING BTREE + KEY `login_name` (`username`) ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '用户信息表'; -- ---------------------------- @@ -560,4 +560,4 @@ INSERT INTO `sys_user_notice` VALUES (8, 8, 2, 1, NULL, now(), now(), 0); INSERT INTO `sys_user_notice` VALUES (9, 9, 2, 1, NULL, now(), now(), 0); INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0); -SET FOREIGN_KEY_CHECKS = 1; +SET FOREIGN_KEY_CHECKS = 1; From 2af4581f2d416f865e2777fd36b87f682c86e6e3 Mon Sep 17 00:00:00 2001 From: Theo <971366405@qq.com> Date: Fri, 13 Jun 2025 17:15:31 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat(auth):=20=E6=94=B9=E8=BF=9B=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E4=BB=A4=E7=89=8C=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 JWT 中添加 tokenType 字段,用于区分访问令牌和刷新令牌 - 重新实现刷新令牌验证逻辑,增加 tokenType 校验 - 优化 refreshToken 方法,直接使用 validateRefreshToken进行验证 - 移除不必要的代码,提高代码可读性和维护性 --- .../auth/service/impl/AuthServiceImpl.java | 25 ++--- .../common/constant/JwtClaimConstants.java | 5 + .../core/security/token/JwtTokenManager.java | 92 +++++++++++++------ 3 files changed, 78 insertions(+), 44 deletions(-) diff --git a/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java index a1bd7740..2e844a6d 100644 --- a/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java @@ -7,18 +7,18 @@ import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import com.youlai.boot.auth.enums.CaptchaTypeEnum; import com.youlai.boot.auth.model.CaptchaInfo; +import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO; import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO; +import com.youlai.boot.auth.service.AuthService; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SecurityConstants; -import com.youlai.boot.common.exception.BusinessException; -import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.config.property.CaptchaProperties; import com.youlai.boot.core.security.extension.sms.SmsAuthenticationToken; -import com.youlai.boot.core.security.util.SecurityUtils; +import com.youlai.boot.core.security.extension.wx.WxMiniAppCodeAuthenticationToken; +import com.youlai.boot.core.security.extension.wx.WxMiniAppPhoneAuthenticationToken; import com.youlai.boot.core.security.model.AuthenticationToken; -import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO; -import com.youlai.boot.auth.service.AuthService; import com.youlai.boot.core.security.token.TokenManager; +import com.youlai.boot.core.security.util.SecurityUtils; import com.youlai.boot.shared.sms.enums.SmsTypeEnum; import com.youlai.boot.shared.sms.service.SmsService; import lombok.RequiredArgsConstructor; @@ -29,8 +29,6 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; -import com.youlai.boot.core.security.extension.wx.WxMiniAppCodeAuthenticationToken; -import com.youlai.boot.core.security.extension.wx.WxMiniAppPhoneAuthenticationToken; import java.awt.*; import java.util.HashMap; @@ -220,13 +218,6 @@ public class AuthServiceImpl implements AuthService { */ @Override public AuthenticationToken refreshToken(String refreshToken) { - // 验证刷新令牌 - boolean isValidate = tokenManager.validateRefreshToken(refreshToken); - - if (!isValidate) { - throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); - } - // 刷新令牌有效,生成新的访问令牌 return tokenManager.refreshToken(refreshToken); } @@ -265,14 +256,14 @@ public class AuthServiceImpl implements AuthService { loginDTO.getEncryptedData(), loginDTO.getIv() ); - + // 执行认证 Authentication authentication = authenticationManager.authenticate(authenticationToken); - + // 认证成功后生成JWT令牌,并存入Security上下文 AuthenticationToken token = tokenManager.generateToken(authentication); SecurityContextHolder.getContext().setAuthentication(authentication); - + return token; } diff --git a/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java b/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java index a74ac0a3..8f6e2d4f 100644 --- a/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java +++ b/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java @@ -10,6 +10,11 @@ package com.youlai.boot.common.constant; */ public interface JwtClaimConstants { + /** + * 令牌类型 + */ + String TOKEN_TYPE = "tokenType"; + /** * 用户ID */ diff --git a/src/main/java/com/youlai/boot/core/security/token/JwtTokenManager.java b/src/main/java/com/youlai/boot/core/security/token/JwtTokenManager.java index 9df71182..d1470211 100644 --- a/src/main/java/com/youlai/boot/core/security/token/JwtTokenManager.java +++ b/src/main/java/com/youlai/boot/core/security/token/JwtTokenManager.java @@ -14,8 +14,8 @@ import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.config.property.SecurityProperties; -import com.youlai.boot.core.security.model.SysUserDetails; import com.youlai.boot.core.security.model.AuthenticationToken; +import com.youlai.boot.core.security.model.SysUserDetails; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -65,7 +65,7 @@ public class JwtTokenManager implements TokenManager { int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); String accessToken = generateToken(authentication, accessTokenTimeToLive); - String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive, true); return AuthenticationToken.builder() .accessToken(accessToken) @@ -109,26 +109,54 @@ public class JwtTokenManager implements TokenManager { */ @Override public boolean validateToken(String token) { - JWT jwt = JWTUtil.parseToken(token); - // 检查 Token 是否有效(验签 + 是否过期) - boolean isValid = jwt.setKey(secretKey).validate(0); - - if (isValid) { - // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) - JSONObject payloads = jwt.getPayloads(); - String jti = payloads.getStr(JWTPayload.JWT_ID); - - // 判断是否在黑名单中,如果在,则返回 false 标识Token无效 - if (Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, jti)))) { - return false; - } - } - return isValid; + return validateToken(token,false); } + /** + * 校验刷新令牌 + * + * @param refreshToken JWT Token + * @return 验证结果 + */ @Override public boolean validateRefreshToken(String refreshToken) { - return this.validateToken(refreshToken); + return validateToken(refreshToken,true); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @param validateRefreshToken 是否校验刷新令牌 + * @return 是否有效 + */ + private boolean validateToken(String token, boolean validateRefreshToken) { + try { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + if(validateRefreshToken) { + //刷新token需要校验token类别 + boolean isRefreshToken = payloads.getBool(JwtClaimConstants.TOKEN_TYPE); + if (!isRefreshToken) { + return false; + } + } + // 判断是否在黑名单中,如果在,则返回 false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, jti)))) { + return false; + } + } + return isValid; + } catch (Exception gitignore) { + // token 验证 + } + return false; } /** @@ -141,12 +169,9 @@ public class JwtTokenManager implements TokenManager { if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); } - JWT jwt = JWTUtil.parseToken(token); JSONObject payloads = jwt.getPayloads(); - Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); - // 黑名单Token Key String blacklistTokenKey = StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, payloads.getStr(JWTPayload.JWT_ID)); @@ -174,16 +199,13 @@ public class JwtTokenManager implements TokenManager { */ @Override public AuthenticationToken refreshToken(String refreshToken) { - - boolean isValid = validateToken(refreshToken); + boolean isValid = validateRefreshToken(refreshToken); if (!isValid) { throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); } - Authentication authentication = parseToken(refreshToken); int accessTokenExpiration = securityProperties.getSession().getAccessTokenTimeToLive(); String newAccessToken = generateToken(authentication, accessTokenExpiration); - return AuthenticationToken.builder() .accessToken(newAccessToken) .refreshToken(refreshToken) @@ -196,13 +218,24 @@ public class JwtTokenManager implements TokenManager { * 生成 JWT Token * * @param authentication 认证信息 - * @param ttl 过期时间 + * @param ttl 过期时间 * @return JWT Token */ private String generateToken(Authentication authentication, int ttl) { + return generateToken(authentication, ttl, false); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @param isRefreshToken 类型是否为刷新token + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl, boolean isRefreshToken) { SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); - Map payload = new HashMap<>(); payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID @@ -216,6 +249,10 @@ public class JwtTokenManager implements TokenManager { Date now = new Date(); payload.put(JWTPayload.ISSUED_AT, now); + payload.put(JwtClaimConstants.TOKEN_TYPE, false); + if (isRefreshToken) { + payload.put(JwtClaimConstants.TOKEN_TYPE, true); + } // 设置过期时间 -1 表示永不过期 if (ttl != -1) { @@ -227,4 +264,5 @@ public class JwtTokenManager implements TokenManager { return JWTUtil.createToken(payload, secretKey); } + } From 02f835e59e4e7f33cbb816bdf5d8e8b753e16647 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Sat, 14 Jun 2025 21:04:48 +0800 Subject: [PATCH 3/5] =?UTF-8?q?build(Dockerfile):=20=E6=9B=B4=E6=96=B0=20D?= =?UTF-8?q?ocker=20=E7=9A=84JDK=E9=95=9C=E5=83=8F=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E9=80=82=E9=85=8D=20SpringBoot=203.5.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #ICF6LW --- Dockerfile | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index cf008264..842060dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,23 @@ # 基础镜像 -FROM openjdk:17-jdk-alpine +FROM openjdk:17 # 维护者信息 MAINTAINER youlai -# 设置国内镜像源(中国科技大学镜像源),修改容器时区(alpine镜像需安装tzdata来设置时区),安装字体库(验证码) -RUN echo -e https://mirrors.ustc.edu.cn/alpine/v3.7/main/ > /etc/apk/repositories \ - && apk --no-cache add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo "Asia/Shanghai" > /etc/timezone \ - && apk --no-cache add ttf-dejavu fontconfig +# 设置时区(Debian直接使用环境变量) +ENV TZ=Asia/Shanghai -# 在运行时自动挂载 /tmp 目录为匿名卷,提高可移植性。如果 /tmp 目录没有挂载为卷,这些文件会写入容器的可写层,可能导致容器镜像膨胀。 +# 在运行时自动挂载 /tmp 目录为匿名卷 VOLUME /tmp -# 将构建的 Spring Boot 可执行 JAR 复制到容器中,重命名为 app.jar +# 添加应用 ADD target/youlai-boot.jar app.jar -# 指定容器启动时执行的命令 +# 启动命令 CMD java \ -Xms512m -Xmx512m \ -Djava.security.egd=file:/dev/./urandom \ -jar /app.jar -# 暴露容器的端口 +# 暴露端口 EXPOSE 8989 From 48ec38e0761f21ab1999133d50e3c30974d5537a Mon Sep 17 00:00:00 2001 From: yms <572114639@qq.com> Date: Wed, 18 Jun 2025 17:01:08 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=99=AE=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=88=96=E5=85=B6=E4=BB=96=E6=9D=83=E9=99=90?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=83=BD=E5=9C=A8swagger=E4=B8=8B=E6=9B=B4?= =?UTF-8?q?=E6=94=B9=E7=B3=BB=E7=BB=9F=E7=AE=A1=E7=90=86=E5=91=98=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E7=8A=B6=E6=80=81=E7=9A=84=E5=AE=89=E5=85=A8=E6=BC=8F?= =?UTF-8?q?=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/youlai/boot/system/controller/RoleController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/youlai/boot/system/controller/RoleController.java b/src/main/java/com/youlai/boot/system/controller/RoleController.java index eaabbdf1..cc8e6f7f 100644 --- a/src/main/java/com/youlai/boot/system/controller/RoleController.java +++ b/src/main/java/com/youlai/boot/system/controller/RoleController.java @@ -91,6 +91,7 @@ public class RoleController { @Operation(summary = "修改角色状态") @PutMapping(value = "/{roleId}/status") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") public Result updateRoleStatus( @Parameter(description = "角色ID") @PathVariable Long roleId, @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status From 86a9b3e212ff5a5537b0c7050c7802aea7fe486f Mon Sep 17 00:00:00 2001 From: yms <572114639@qq.com> Date: Wed, 18 Jun 2025 17:24:15 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=99=AE=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=88=96=E5=85=B6=E4=BB=96=E6=9D=83=E9=99=90?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=83=BD=E5=9C=A8swagger=E4=B8=8B=E6=9B=B4?= =?UTF-8?q?=E6=94=B9=E7=B3=BB=E7=BB=9F=E7=AE=A1=E7=90=86=E5=91=98=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E7=8A=B6=E6=80=81,=E8=8E=B7=E5=8F=96=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=A1=A8=E5=8D=95=E6=95=B0=E6=8D=AE,=E6=9B=B4?= =?UTF-8?q?=E6=94=B9=E8=8F=9C=E5=8D=95=E6=98=BE=E7=A4=BA=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=9A=84=E5=AE=89=E5=85=A8=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/youlai/boot/system/controller/MenuController.java | 1 + .../java/com/youlai/boot/system/controller/UserController.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/main/java/com/youlai/boot/system/controller/MenuController.java b/src/main/java/com/youlai/boot/system/controller/MenuController.java index f5e2f62a..9e12a798 100644 --- a/src/main/java/com/youlai/boot/system/controller/MenuController.java +++ b/src/main/java/com/youlai/boot/system/controller/MenuController.java @@ -102,6 +102,7 @@ public class MenuController { @Operation(summary = "修改菜单显示状态") @PatchMapping("/{menuId}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") public Result updateMenuVisible( @Parameter(description = "菜单ID") @PathVariable Long menuId, @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible diff --git a/src/main/java/com/youlai/boot/system/controller/UserController.java b/src/main/java/com/youlai/boot/system/controller/UserController.java index 69a4d41b..a1a1bd80 100644 --- a/src/main/java/com/youlai/boot/system/controller/UserController.java +++ b/src/main/java/com/youlai/boot/system/controller/UserController.java @@ -80,6 +80,7 @@ public class UserController { @Operation(summary = "获取用户表单数据") @GetMapping("/{userId}/form") + @PreAuthorize("@ss.hasPerm('sys:user:edit')") @Log(value = "用户表单数据", module = LogModuleEnum.USER) public Result getUserForm( @Parameter(description = "用户ID") @PathVariable Long userId @@ -113,6 +114,7 @@ public class UserController { @Operation(summary = "修改用户状态") @PatchMapping(value = "/{userId}/status") + @PreAuthorize("@ss.hasPerm('sys:user:edit')") @Log(value = "修改用户状态", module = LogModuleEnum.USER) public Result updateUserStatus( @Parameter(description = "用户ID") @PathVariable Long userId,