diff --git a/pom.xml b/pom.xml index 8177e96..6ed663a 100644 --- a/pom.xml +++ b/pom.xml @@ -147,6 +147,10 @@ org.apache.commons commons-pool2 + + org.springframework.boot + spring-boot-starter-validation + diff --git a/src/main/java/asia/yulinling/workflow/config/RedisConfig.java b/src/main/java/asia/yulinling/workflow/config/RedisConfig.java index e53b4a5..21b659c 100644 --- a/src/main/java/asia/yulinling/workflow/config/RedisConfig.java +++ b/src/main/java/asia/yulinling/workflow/config/RedisConfig.java @@ -2,16 +2,11 @@ package asia.yulinling.workflow.config; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; -import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.cache.RedisCacheConfiguration; -import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.io.Serializable; @@ -28,6 +23,9 @@ import java.io.Serializable; @AutoConfigureAfter(RedisAutoConfiguration.class) @EnableCaching public class RedisConfig { + /** + * 默认情况下的模板只能支持RedisTemplate,也就是只能存入字符串,因此支持序列化 + */ @Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) { RedisTemplate redisTemplate = new RedisTemplate<>(); @@ -36,14 +34,4 @@ public class RedisConfig { redisTemplate.setConnectionFactory(factory); return redisTemplate; } - -// @Bean -// public CacheManager cacheManager(RedisConnectionFactory factory) { -// RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig(); -// RedisCacheConfiguration redisCacheConfiguration = configuration.serializeKeysWith( -// RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) -// ).serializeValuesWith( -// RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); -// return RedisCacheManager.builder(factory).cacheDefaults(redisCacheConfiguration).build(); -// } } diff --git a/src/main/java/asia/yulinling/workflow/config/SecurityConfig.java b/src/main/java/asia/yulinling/workflow/config/SecurityConfig.java index 6c9c677..745b40b 100644 --- a/src/main/java/asia/yulinling/workflow/config/SecurityConfig.java +++ b/src/main/java/asia/yulinling/workflow/config/SecurityConfig.java @@ -20,6 +20,14 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +/** + *

+ * 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 login(@RequestBody LoginRequest loginRequest) { String token = authenticate.login(loginRequest); - return ResponseEntity.ok(token); + JWTAuthResponse jwtAuthResponse = new JWTAuthResponse(token); + return ApiResponse.ofSuccess(jwtAuthResponse); } } diff --git a/src/main/java/asia/yulinling/workflow/dto/request/LoginRequest.java b/src/main/java/asia/yulinling/workflow/dto/request/LoginRequest.java index 0799ce4..cf36ab3 100644 --- a/src/main/java/asia/yulinling/workflow/dto/request/LoginRequest.java +++ b/src/main/java/asia/yulinling/workflow/dto/request/LoginRequest.java @@ -1,7 +1,7 @@ package asia.yulinling.workflow.dto.request; +import jakarta.validation.constraints.NotBlank; import lombok.Data; - /** *

* 登录请求类 @@ -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 { +public class PageResult implements Serializable { private long total; // 总记录数 private int totalPage; // 总页数 private int currentPage; // 当前页码 @@ -46,4 +47,19 @@ public class PageResult { this.from = (currentPage - 1) * pageSize + 1; this.to = Math.min(currentPage * pageSize, (int) total); } + + // 构造方法(可选传参) + public PageResult(long total, List list) { + this.total = total; + this.list = list; + + // 自动计算 + this.totalPage = (int) Math.ceil((double) total / pageSize); + this.hasNext = currentPage < totalPage; + this.hasPrev = currentPage > 1; + this.firstPage = currentPage == 1; + this.lastPage = currentPage == totalPage; + this.from = (currentPage - 1) * pageSize + 1; + this.to = Math.min(currentPage * pageSize, (int) total); + } } diff --git a/src/main/java/asia/yulinling/workflow/exception/BaseException.java b/src/main/java/asia/yulinling/workflow/exception/BaseException.java index a20a574..4dcc7b1 100644 --- a/src/main/java/asia/yulinling/workflow/exception/BaseException.java +++ b/src/main/java/asia/yulinling/workflow/exception/BaseException.java @@ -14,9 +14,10 @@ import lombok.EqualsAndHashCode; */ @Data @EqualsAndHashCode(callSuper = true) -public class BaseException extends RuntimeException{ - private final Integer code; - private final String message; +public class BaseException extends RuntimeException { + private Integer code; + private String message; + private Object data; public BaseException(Status status) { super(status.getMessage()); @@ -24,9 +25,19 @@ public class BaseException extends RuntimeException{ this.message = status.getMessage(); } + public BaseException(Status status, Object data) { + this(status); + this.data = data; + } + public BaseException(Integer code, String message) { super(message); this.code = code; this.message = message; } + + public BaseException(Integer code, String message, Object data) { + this(code, message); + this.data = data; + } } diff --git a/src/main/java/asia/yulinling/workflow/exception/SecurityException.java b/src/main/java/asia/yulinling/workflow/exception/SecurityException.java new file mode 100644 index 0000000..ca674cc --- /dev/null +++ b/src/main/java/asia/yulinling/workflow/exception/SecurityException.java @@ -0,0 +1,31 @@ +package asia.yulinling.workflow.exception; + +import asia.yulinling.workflow.constant.Status; +import lombok.EqualsAndHashCode; + +/** + *

+ * 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 { * @return ApiResponse */ public static ApiResponse ofSuccess(T data) { - return ofStatus(Status.OK, data); + return ofStatus(Status.SUCCESS, data); } /** @@ -77,7 +77,7 @@ public class ApiResponse { * @return ApiResponse */ public static ApiResponse ofMessage(String message) { - return of(Status.OK.getCode(), message, null); + return of(Status.SUCCESS.getCode(), message, null); } /** diff --git a/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationFilter.java b/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationFilter.java index e396e60..6fbe284 100644 --- a/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationFilter.java +++ b/src/main/java/asia/yulinling/workflow/security/JwtAuthenticationFilter.java @@ -1,7 +1,10 @@ package asia.yulinling.workflow.security; +import asia.yulinling.workflow.exception.SecurityException; +import asia.yulinling.workflow.constant.Status; import asia.yulinling.workflow.model.vo.user.UserPrincipal; import asia.yulinling.workflow.utils.JwtUtil; +import asia.yulinling.workflow.utils.ResponseUtil; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -40,19 +43,25 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { String token = jwtUtil.getTokenFromRequest(request); if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) { - // 2. 解析token获取username - String username = jwtUtil.parseToken(token).getSubject(); + try { + // 2. 解析token获取username + String username = jwtUtil.parseToken(token).getSubject(); - // 3. 根据username验证password - UserPrincipal userPrincipal = jwtUserDetailsService.loadUserByUsername(username); - log.info("userPrincipal: {}", userPrincipal); + // 3. 根据username验证password + UserPrincipal userPrincipal = jwtUserDetailsService.loadUserByUsername(username); + log.info("userPrincipal: {}", userPrincipal); - UsernamePasswordAuthenticationToken authenticationToken - = new UsernamePasswordAuthenticationToken(userPrincipal, null, userPrincipal.getAuthorities()); - log.info("authenticationToken: {}", authenticationToken); + UsernamePasswordAuthenticationToken authenticationToken + = new UsernamePasswordAuthenticationToken(userPrincipal, null, userPrincipal.getAuthorities()); + log.info("authenticationToken: {}", authenticationToken); - authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authenticationToken); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + + } catch (SecurityException e) { + log.error("Security异常:{}", e.getMessage()); + ResponseUtil.renderJson(response, e); + } } filterChain.doFilter(request, response); diff --git a/src/main/java/asia/yulinling/workflow/utils/JwtUtil.java b/src/main/java/asia/yulinling/workflow/utils/JwtUtil.java index 42a7f35..bea6e7c 100644 --- a/src/main/java/asia/yulinling/workflow/utils/JwtUtil.java +++ b/src/main/java/asia/yulinling/workflow/utils/JwtUtil.java @@ -1,5 +1,9 @@ package asia.yulinling.workflow.utils; +import asia.yulinling.workflow.constant.Const; +import asia.yulinling.workflow.constant.Status; +import asia.yulinling.workflow.exception.SecurityException; +import cn.hutool.core.util.StrUtil; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import jakarta.servlet.http.HttpServletRequest; @@ -7,12 +11,14 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.core.Authentication; import io.jsonwebtoken.*; import org.springframework.stereotype.Component; import java.security.Key; import java.util.Date; +import java.util.concurrent.TimeUnit; /** *

@@ -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 findKeyForPage(String patternKey, int currentPage, int pageSize) { + ScanOptions options = ScanOptions.scanOptions().match(patternKey).build(); + RedisConnectionFactory redisConnectionFactory = stringRedisTemplate.getConnectionFactory(); + RedisConnection redisConnection = redisConnectionFactory.getConnection(); + Cursor cursor = redisConnection.scan(options); + List result = new ArrayList<>(); + long tmpIndex = 0; + int startIndex = (currentPage - 1) * pageSize; + int end = startIndex + pageSize; + while (cursor.hasNext()) { + String key = new String(cursor.next()); + if (tmpIndex >= startIndex && tmpIndex < end) { + result.add(key); + } + tmpIndex++; + } + + try { + cursor.close(); + RedisConnectionUtils.releaseConnection(redisConnection, redisConnectionFactory); + } catch (Exception e) { + log.error("Redis连接关闭异常:{}", e.getMessage()); + } + + return new PageResult<>(tmpIndex, result); + } + + /** + * 删除Redis中的key + */ + public void delete(String key) { + stringRedisTemplate.delete(key); + } + + /** + * 批量删除Redis中key + * + * @param keys 键列表 + */ + public void delete(Collection keys) { + stringRedisTemplate.delete(keys); + } +} diff --git a/src/main/java/asia/yulinling/workflow/utils/ResponseUtil.java b/src/main/java/asia/yulinling/workflow/utils/ResponseUtil.java new file mode 100644 index 0000000..eb0a411 --- /dev/null +++ b/src/main/java/asia/yulinling/workflow/utils/ResponseUtil.java @@ -0,0 +1,58 @@ +package asia.yulinling.workflow.utils; + +import asia.yulinling.workflow.constant.Status; +import asia.yulinling.workflow.exception.BaseException; +import asia.yulinling.workflow.model.ApiResponse; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; + +/** + *

+ * 通用工具类 + *

+ * + * @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 redisTemplate; - @Autowired - private StringRedisTemplate stringRedisTemplate; - - - @Test - public void testRedisTemplate() throws InterruptedException { -// CountDownLatch latch = new CountDownLatch(1000); -// ExecutorService executorService = Executors.newFixedThreadPool(100); -// -// IntStream.range(0, 1000).forEach(i -> { -// executorService.execute(() -> { -// stringRedisTemplate.opsForValue().increment("count", 1); -// latch.countDown(); -// }); -// }); -// -// latch.await(); // 等待所有线程执行完毕 -// executorService.shutdown(); // 关闭线程池 -// -// Long count = Long.valueOf(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("count"))); -// log.info("count: {}", count); -// -// stringRedisTemplate.opsForValue().set("k1", "v1"); -// String k1 = stringRedisTemplate.opsForValue().get("k1"); -// log.info("k1: {}", k1); - } -} diff --git a/src/test/java/asia/yulinling/workflow/utils/JwtUtilTest.java b/src/test/java/asia/yulinling/workflow/utils/JwtUtilTest.java index c42ce88..f4ac805 100644 --- a/src/test/java/asia/yulinling/workflow/utils/JwtUtilTest.java +++ b/src/test/java/asia/yulinling/workflow/utils/JwtUtilTest.java @@ -3,6 +3,9 @@ package asia.yulinling.workflow.utils; import io.jsonwebtoken.Claims; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mockito; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -12,6 +15,7 @@ import org.springframework.security.core.userdetails.UserDetails; import java.util.List; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; /** *

@@ -23,10 +27,18 @@ import static org.junit.jupiter.api.Assertions.*; */ class JwtUtilTest { private JwtUtil jwtUtil; + private StringRedisTemplate stringRedisTemplate; + private ValueOperations valueOps; @BeforeEach void setUp() { - jwtUtil = new JwtUtil(); + stringRedisTemplate = Mockito.mock(StringRedisTemplate.class); + valueOps = Mockito.mock(ValueOperations.class); + + // 当调用 stringRedisTemplate.opsForValue() 时返回 valueOps + when(stringRedisTemplate.opsForValue()).thenReturn(valueOps); + + jwtUtil = new JwtUtil(stringRedisTemplate); } @Test diff --git a/src/test/java/asia/yulinling/workflow/utils/RedisTest.java b/src/test/java/asia/yulinling/workflow/utils/RedisTest.java new file mode 100644 index 0000000..9a33865 --- /dev/null +++ b/src/test/java/asia/yulinling/workflow/utils/RedisTest.java @@ -0,0 +1,80 @@ +package asia.yulinling.workflow.utils; + +import asia.yulinling.workflow.WorkFlowMainTests; +import io.jsonwebtoken.Claims; +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 org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; + +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; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + *

+ * Redis测试类 + *

+ * + * @author YLL + * @since 2025/6/18 + */ +@Slf4j +public class RedisTest extends WorkFlowMainTests { + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Autowired + private JwtUtil jwtUtil; + @Autowired + private RedisUtil redisUtil; + + @Test + public void testRedisTemplate() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1000); + ExecutorService executorService = Executors.newFixedThreadPool(100); + + IntStream.range(0, 1000).forEach(i -> { + executorService.execute(() -> { + stringRedisTemplate.opsForValue().increment("count", 1); + latch.countDown(); + }); + }); + + latch.await(); // 等待所有线程执行完毕 + executorService.shutdown(); // 关闭线程池 + + Long count = Long.valueOf(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("count"))); + log.info("count: {}", count); + + stringRedisTemplate.opsForValue().set("k1", "v1"); + String k1 = stringRedisTemplate.opsForValue().get("k1"); + log.info("k1: {}", k1); + } + + @Test + public void testRedis() { + UserDetails user = User.withUsername("test") + .password("test") + .roles("USER") + .build(); + Authentication authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); + String token = jwtUtil.generateToken(authentication, false); + assertNotNull(token); + + Claims claims = jwtUtil.parseToken(token); + assertEquals("test", claims.getSubject()); + } +}