feat:
- 使用Redis存储Token并进行校验 - 完善常量
This commit is contained in:
parent
28bba7ed79
commit
8937ec945a
4
pom.xml
4
pom.xml
@ -147,6 +147,10 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -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<String, String>,也就是只能存入字符串,因此支持序列化
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, Serializable> redisTemplate(RedisConnectionFactory factory) {
|
||||
RedisTemplate<String, Serializable> 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();
|
||||
// }
|
||||
}
|
||||
|
||||
@ -20,6 +20,14 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Spring Security配置类
|
||||
* </p>
|
||||
*
|
||||
* @author YLL
|
||||
* @since 2025/6/17
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
|
||||
60
src/main/java/asia/yulinling/workflow/constant/Const.java
Normal file
60
src/main/java/asia/yulinling/workflow/constant/Const.java
Normal file
@ -0,0 +1,60 @@
|
||||
package asia.yulinling.workflow.constant;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 常量池
|
||||
* </p>
|
||||
*
|
||||
* @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 = "匿名用户";
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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<JWTAuthResponse> login(@RequestBody LoginRequest loginRequest) {
|
||||
String token = authenticate.login(loginRequest);
|
||||
return ResponseEntity.ok(token);
|
||||
JWTAuthResponse jwtAuthResponse = new JWTAuthResponse(token);
|
||||
return ApiResponse.ofSuccess(jwtAuthResponse);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package asia.yulinling.workflow.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 登录请求类
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -18,4 +18,8 @@ import lombok.NoArgsConstructor;
|
||||
public class JWTAuthResponse {
|
||||
private String accessToken;
|
||||
private String tokenType = "Bearer";
|
||||
|
||||
public JWTAuthResponse(String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<T> {
|
||||
public class PageResult<T> implements Serializable {
|
||||
private long total; // 总记录数
|
||||
private int totalPage; // 总页数
|
||||
private int currentPage; // 当前页码
|
||||
@ -46,4 +47,19 @@ public class PageResult<T> {
|
||||
this.from = (currentPage - 1) * pageSize + 1;
|
||||
this.to = Math.min(currentPage * pageSize, (int) total);
|
||||
}
|
||||
|
||||
// 构造方法(可选传参)
|
||||
public PageResult(long total, List<T> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
package asia.yulinling.workflow.exception;
|
||||
|
||||
import asia.yulinling.workflow.constant.Status;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Security异常
|
||||
* </p>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -67,7 +67,7 @@ public class ApiResponse<T> {
|
||||
* @return ApiResponse
|
||||
*/
|
||||
public static <T> ApiResponse<T> ofSuccess(T data) {
|
||||
return ofStatus(Status.OK, data);
|
||||
return ofStatus(Status.SUCCESS, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,7 +77,7 @@ public class ApiResponse<T> {
|
||||
* @return ApiResponse
|
||||
*/
|
||||
public static ApiResponse<Void> ofMessage(String message) {
|
||||
return of(Status.OK.getCode(), message, null);
|
||||
return of(Status.SUCCESS.getCode(), message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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,6 +43,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
String token = jwtUtil.getTokenFromRequest(request);
|
||||
|
||||
if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) {
|
||||
try {
|
||||
// 2. 解析token获取username
|
||||
String username = jwtUtil.parseToken(token).getSubject();
|
||||
|
||||
@ -53,6 +57,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
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);
|
||||
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@ -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();
|
||||
|
||||
// 2. 解析Token
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(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();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
83
src/main/java/asia/yulinling/workflow/utils/RedisUtil.java
Normal file
83
src/main/java/asia/yulinling/workflow/utils/RedisUtil.java
Normal file
@ -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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Redis工具类
|
||||
* </p>
|
||||
*
|
||||
* @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<String> findKeyForPage(String patternKey, int currentPage, int pageSize) {
|
||||
ScanOptions options = ScanOptions.scanOptions().match(patternKey).build();
|
||||
RedisConnectionFactory redisConnectionFactory = stringRedisTemplate.getConnectionFactory();
|
||||
RedisConnection redisConnection = redisConnectionFactory.getConnection();
|
||||
Cursor<byte[]> cursor = redisConnection.scan(options);
|
||||
List<String> 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<String> keys) {
|
||||
stringRedisTemplate.delete(keys);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 通用工具类
|
||||
* </p>
|
||||
*
|
||||
* @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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Redis测试
|
||||
* </p>
|
||||
*
|
||||
* @author YLL
|
||||
* @since 2025/6/17
|
||||
*/
|
||||
@Slf4j
|
||||
public class RedisTest extends WorkFlowMainTests {
|
||||
@Autowired
|
||||
private RedisTemplate<String, Serializable> 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@ -23,10 +27,18 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
*/
|
||||
class JwtUtilTest {
|
||||
private JwtUtil jwtUtil;
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
private ValueOperations<String, String> 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
|
||||
|
||||
80
src/test/java/asia/yulinling/workflow/utils/RedisTest.java
Normal file
80
src/test/java/asia/yulinling/workflow/utils/RedisTest.java
Normal file
@ -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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Redis测试类
|
||||
* </p>
|
||||
*
|
||||
* @author YLL
|
||||
* @since 2025/6/18
|
||||
*/
|
||||
@Slf4j
|
||||
public class RedisTest extends WorkFlowMainTests {
|
||||
@Autowired
|
||||
private RedisTemplate<String, Serializable> 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());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user