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 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; 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 8b0d5d02..805000a0 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,9 +14,9 @@ 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 org.apache.commons.lang3.StringUtils; +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; @@ -66,7 +66,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) @@ -110,26 +110,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; } /** @@ -146,12 +174,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)); @@ -179,16 +204,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) @@ -201,13 +223,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 @@ -221,6 +254,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) { @@ -232,4 +269,5 @@ public class JwtTokenManager implements TokenManager { return JWTUtil.createToken(payload, secretKey); } + } 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/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 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,