+ * Spring Security配置类 + *
+ * + * @author YLL + * @since 2025/6/17 + */ @Configuration @EnableWebSecurity @RequiredArgsConstructor diff --git a/src/main/java/asia/yulinling/workflow/constant/Const.java b/src/main/java/asia/yulinling/workflow/constant/Const.java new file mode 100644 index 0000000..e0ddd86 --- /dev/null +++ b/src/main/java/asia/yulinling/workflow/constant/Const.java @@ -0,0 +1,60 @@ +package asia.yulinling.workflow.constant; + +/** + *+ * 常量池 + *
+ * + * @author YLL + * @since 2025/6/18 + */ +public interface Const { + /** + * 启用 + */ + Integer ENABLE = 1; + /** + * 禁用 + */ + Integer DISABLE = 0; + + /** + * 页面 + */ + Integer PAGE = 1; + + /** + * 按钮 + */ + Integer BUTTON = 2; + + /** + * JWT 在 Redis 中保存的key前缀 + */ + String REDIS_JWT_KEY_PREFIX = "security:jwt:"; + + /** + * 星号 + */ + String SYMBOL_STAR = "*"; + + /** + * 邮箱符号 + */ + String SYMBOL_EMAIL = "@"; + + /** + * 默认当前页码 + */ + Integer DEFAULT_CURRENT_PAGE = 1; + + /** + * 默认每页条数 + */ + Integer DEFAULT_PAGE_SIZE = 10; + + /** + * 匿名用户 用户名 + */ + String ANONYMOUS_NAME = "匿名用户"; +} diff --git a/src/main/java/asia/yulinling/workflow/constant/Status.java b/src/main/java/asia/yulinling/workflow/constant/Status.java index ac290e5..d0578e1 100644 --- a/src/main/java/asia/yulinling/workflow/constant/Status.java +++ b/src/main/java/asia/yulinling/workflow/constant/Status.java @@ -12,11 +12,83 @@ import lombok.Getter; */ @Getter public enum Status { - /** 操作成功 */ - OK(200, "success"), + /** + * 操作成功! + */ + SUCCESS(200, "操作成功!"), /** 未知异常 */ - UNKNOWN_ERROR(500, "Unknown error"); + UNKNOWN_ERROR(500, "未知异常"), + + /** + * 退出成功! + */ + LOGOUT(200, "退出成功!"), + + /** + * 请先登录! + */ + UNAUTHORIZED(401, "请先登录!"), + + /** + * 暂无权限访问! + */ + ACCESS_DENIED(403, "权限不足!"), + + /** + * 请求不存在! + */ + REQUEST_NOT_FOUND(404, "请求不存在!"), + + /** + * 请求方式不支持! + */ + HTTP_BAD_METHOD(405, "请求方式不支持!"), + + /** + * 请求异常! + */ + BAD_REQUEST(400, "请求异常!"), + + /** + * 参数不匹配! + */ + PARAM_NOT_MATCH(400, "参数不匹配!"), + + /** + * 参数不能为空! + */ + PARAM_NOT_NULL(400, "参数不能为空!"), + + /** + * 当前用户已被锁定,请联系管理员解锁! + */ + USER_DISABLED(403, "当前用户已被锁定,请联系管理员解锁!"), + + /** + * 用户名或密码错误! + */ + USERNAME_PASSWORD_ERROR(5001, "用户名或密码错误!"), + + /** + * token 已过期,请重新登录! + */ + TOKEN_EXPIRED(5002, "token 已过期,请重新登录!"), + + /** + * token 解析失败,请尝试重新登录! + */ + TOKEN_PARSE_ERROR(5002, "token 解析失败,请尝试重新登录!"), + + /** + * 当前用户已在别处登录,请尝试更改密码或重新登录! + */ + TOKEN_OUT_OF_CTRL(5003, "当前用户已在别处登录,请尝试更改密码或重新登录!"), + + /** + * 无法手动踢出自己,请尝试退出登录操作! + */ + KICKOUT_SELF(5004, "无法手动踢出自己,请尝试退出登录操作!"); /** 状态码 */ private final Integer code; diff --git a/src/main/java/asia/yulinling/workflow/controller/AuthController.java b/src/main/java/asia/yulinling/workflow/controller/AuthController.java index 189b28c..5f60ae0 100644 --- a/src/main/java/asia/yulinling/workflow/controller/AuthController.java +++ b/src/main/java/asia/yulinling/workflow/controller/AuthController.java @@ -1,7 +1,10 @@ package asia.yulinling.workflow.controller; import asia.yulinling.workflow.dto.request.LoginRequest; +import asia.yulinling.workflow.dto.response.JWTAuthResponse; +import asia.yulinling.workflow.model.ApiResponse; import asia.yulinling.workflow.service.AuthService; +import asia.yulinling.workflow.utils.ResponseUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -25,9 +28,10 @@ public class AuthController { private final AuthService authenticate; @PostMapping("/login") - public ResponseEntity> login(@RequestBody LoginRequest loginRequest) { + public ApiResponse* 登录请求类 @@ -12,6 +12,20 @@ import lombok.Data; */ @Data public class LoginRequest { + /** + * 用户名或者邮箱名 + */ + @NotBlank(message = "用户名或者邮箱名不能为空") private String username; + + /** + * 密码 + */ + @NotBlank(message = "密码不能为空") private String password; + + /** + * 记住我 + */ + private Boolean rememberMe = false; } diff --git a/src/main/java/asia/yulinling/workflow/dto/request/PageParam.java b/src/main/java/asia/yulinling/workflow/dto/request/PageParam.java index 1645cf9..bbdc081 100644 --- a/src/main/java/asia/yulinling/workflow/dto/request/PageParam.java +++ b/src/main/java/asia/yulinling/workflow/dto/request/PageParam.java @@ -4,7 +4,7 @@ import lombok.Data; /** *
- * 分页请求参数 + * 分页请求参数 *
* * @author YLL @@ -12,8 +12,15 @@ import lombok.Data; */ @Data public class PageParam { - private int pageNum; - private int pageSize; + /** + * 当前页码 + */ + private Integer pageNum; + + /** + * 每条页数 + */ + private Integer pageSize; public PageParam() { this.pageNum = 1; diff --git a/src/main/java/asia/yulinling/workflow/dto/response/JWTAuthResponse.java b/src/main/java/asia/yulinling/workflow/dto/response/JWTAuthResponse.java index 9fc606f..f497a5d 100644 --- a/src/main/java/asia/yulinling/workflow/dto/response/JWTAuthResponse.java +++ b/src/main/java/asia/yulinling/workflow/dto/response/JWTAuthResponse.java @@ -18,4 +18,8 @@ import lombok.NoArgsConstructor; public class JWTAuthResponse { private String accessToken; private String tokenType = "Bearer"; + + public JWTAuthResponse(String accessToken) { + this.accessToken = accessToken; + } } diff --git a/src/main/java/asia/yulinling/workflow/dto/response/PageResult.java b/src/main/java/asia/yulinling/workflow/dto/response/PageResult.java index 559c1a5..6d4859e 100644 --- a/src/main/java/asia/yulinling/workflow/dto/response/PageResult.java +++ b/src/main/java/asia/yulinling/workflow/dto/response/PageResult.java @@ -2,16 +2,17 @@ package asia.yulinling.workflow.dto.response; import lombok.Data; +import java.io.Serializable; import java.util.List; /** - * 分页返回结果 + * 通用分页返回结果 * * @author YLL * @since 2025/6/8 */ @Data -public class PageResult+ * Security异常 + *
+ * + * @author YLL + * @since 2025/6/18 + */ +@EqualsAndHashCode(callSuper = true) +public class SecurityException extends BaseException { + public SecurityException(Status status) { + super(status); + } + + public SecurityException(Status status, Object data) { + super(status, data); + } + + public SecurityException(Integer code, String message) { + super(code, message); + } + + public SecurityException(Integer code, String message, Object data) { + super(code, message, data); + } +} diff --git a/src/main/java/asia/yulinling/workflow/handler/GlobalExceptionHandler.java b/src/main/java/asia/yulinling/workflow/exception/handler/GlobalExceptionHandler.java similarity index 96% rename from src/main/java/asia/yulinling/workflow/handler/GlobalExceptionHandler.java rename to src/main/java/asia/yulinling/workflow/exception/handler/GlobalExceptionHandler.java index d376811..f43769d 100644 --- a/src/main/java/asia/yulinling/workflow/handler/GlobalExceptionHandler.java +++ b/src/main/java/asia/yulinling/workflow/exception/handler/GlobalExceptionHandler.java @@ -1,4 +1,4 @@ -package asia.yulinling.workflow.handler; +package asia.yulinling.workflow.exception.handler; import asia.yulinling.workflow.exception.JsonException; import asia.yulinling.workflow.exception.PageException; diff --git a/src/main/java/asia/yulinling/workflow/model/ApiResponse.java b/src/main/java/asia/yulinling/workflow/model/ApiResponse.java index 519a958..074dd24 100644 --- a/src/main/java/asia/yulinling/workflow/model/ApiResponse.java +++ b/src/main/java/asia/yulinling/workflow/model/ApiResponse.java @@ -67,7 +67,7 @@ public class ApiResponse@@ -26,6 +32,7 @@ import java.util.Date; @Component @Slf4j public class JwtUtil { + private final StringRedisTemplate stringRedisTemplate; /** * jwt 加密 key,默认值:kw. */ @@ -52,26 +59,27 @@ public class JwtUtil { * @return JWT */ public String generateToken(Authentication authentication, boolean isRememberMe) { - // 1. 构建签名密钥 - Key key = key(); - - // 2. 当前时间 + // 1. 当前时间 Date now = new Date(); - // 3. 计算过期时间 + // 2. 计算过期时间 long ttl = isRememberMe ? this.remember : this.ttl; Date expiration = new Date(now.getTime() + ttl); - // 4. 构建 JWT + // 3. 构建 JWT String username = authentication.getName(); JwtBuilder builder = Jwts.builder() .setSubject(username) .setIssuedAt(now) .setExpiration(expiration) - .signWith(key); + .signWith(this.key()); - // 5. 返回生成的 token 字符串 - return builder.compact(); + // 4. 生成token + String token = builder.compact(); + // 5. 将token存入redis + stringRedisTemplate.opsForValue().set(Const.REDIS_JWT_KEY_PREFIX + username, token, ttl, TimeUnit.SECONDS); + // 6. 返回生成的token + return token; } /** @@ -82,15 +90,26 @@ public class JwtUtil { */ public Claims parseToken(String token) { try { - // 1. 获取签名密钥 - Key key = key(); + // 1. 解析Token + Claims claims = Jwts.parserBuilder() + .setSigningKey(this.key()) + .build() + .parseClaimsJws(token) + .getBody(); + // 2. 获取RedisKey + String redisKey = Const.REDIS_JWT_KEY_PREFIX + claims.getSubject(); - // 2. 解析Token - return Jwts.parserBuilder() - .setSigningKey(key) - .build() - .parseClaimsJws(token) - .getBody(); + // 3. 校验Token是否存在 + Long expire = stringRedisTemplate.getExpire(redisKey, TimeUnit.SECONDS); + if (expire <= 0) { + throw new SecurityException(Status.TOKEN_EXPIRED); + } + // 4. 校验Token是否一致 + String redisToken = stringRedisTemplate.opsForValue().get(redisKey); + if (!StrUtil.equals(redisToken, token)) { + throw new SecurityException(Status.TOKEN_OUT_OF_CTRL); + } + return claims; } catch (ExpiredJwtException e) { log.error("Token 已过期: {}", token, e); throw new JwtException("Token 已过期", e); @@ -136,6 +155,7 @@ public class JwtUtil { public void invalidateToken(HttpServletRequest request) { String token = getTokenFromRequest(request); String username = parseToken(token).getSubject(); + stringRedisTemplate.delete(Const.REDIS_JWT_KEY_PREFIX + username); } /** diff --git a/src/main/java/asia/yulinling/workflow/utils/RedisUtil.java b/src/main/java/asia/yulinling/workflow/utils/RedisUtil.java new file mode 100644 index 0000000..dec2da3 --- /dev/null +++ b/src/main/java/asia/yulinling/workflow/utils/RedisUtil.java @@ -0,0 +1,83 @@ +package asia.yulinling.workflow.utils; + +import asia.yulinling.workflow.dto.response.PageResult; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisConnectionUtils; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + *
+ * Redis工具类 + *
+ * + * @author YLL + * @since 2025/6/18 + */ +@RequiredArgsConstructor +@Component +@Slf4j +public class RedisUtil { + private final StringRedisTemplate stringRedisTemplate; + + /** + * 分页获取指定格式key,使用 scan 命令代替 keys 命令,在大数据量的情况下可以提高查询效率 + * + * @param patternKey key格式 + * @param currentPage 当前页码 + * @param pageSize 每页条数 + * @return 分页获取指定格式key + */ + public PageResult+ * 通用工具类 + *
+ * + * @author YLL + * @since 2025/6/18 + */ +@Slf4j +public class ResponseUtil { + /** + * 往 response 写出 json + * + * @param response 响应 + * @param status 状态 + * @param data 返回数据 + */ + public static void renderJson(HttpServletResponse response, Status status, Object data) { + try { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "*"); + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(200); + + response.getWriter().write(JSONUtil.toJsonStr( + new JSONObject(ApiResponse.ofStatus(status, data), false) + )); + } catch (IOException e) { + log.error("Response写正常Json异常:{}", e.getMessage()); + } + } + + public static void renderJson(HttpServletResponse response, BaseException baseException) { + try { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "*"); + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(200); + response.getWriter().write(JSONUtil.toJsonStr( + new JSONObject(ApiResponse.ofException(baseException), false) + )); + } catch (IOException e) { + log.error("Response写异常Json异常:{}", e.getMessage()); + } + } +} diff --git a/src/test/java/asia/yulinling/workflow/RedisTest.java b/src/test/java/asia/yulinling/workflow/RedisTest.java deleted file mode 100644 index 81747b8..0000000 --- a/src/test/java/asia/yulinling/workflow/RedisTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package asia.yulinling.workflow; - -import asia.yulinling.workflow.model.entity.User; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.StringRedisTemplate; - -import java.io.Serializable; -import java.util.Objects; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.IntStream; - -/** - *- * Redis测试 - *
- * - * @author YLL - * @since 2025/6/17 - */ -@Slf4j -public class RedisTest extends WorkFlowMainTests { - @Autowired - private RedisTemplate
@@ -23,10 +27,18 @@ import static org.junit.jupiter.api.Assertions.*;
*/
class JwtUtilTest {
private JwtUtil jwtUtil;
+ private StringRedisTemplate stringRedisTemplate;
+ private ValueOperations
+ * Redis测试类
+ *