1
This commit is contained in:
29
src/main/java/com/aida/seller/common/config/RedisConfig.java
Normal file
29
src/main/java/com/aida/seller/common/config/RedisConfig.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.aida.seller.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
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.StringRedisSerializer;
|
||||
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
StringRedisSerializer stringSerializer = new StringRedisSerializer();
|
||||
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
|
||||
|
||||
template.setKeySerializer(stringSerializer);
|
||||
template.setValueSerializer(jsonSerializer);
|
||||
template.setHashKeySerializer(stringSerializer);
|
||||
template.setHashValueSerializer(jsonSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.aida.seller.common.constants;
|
||||
|
||||
public class CommonConstants {
|
||||
|
||||
public static final int MINIO_PATH_TIMEOUT = 7 * 24 * 60 * 60; // minio图片临时访问地址 7 天过期(second)
|
||||
|
||||
public static final int TOKEN_EXPIRE_TIME = 7 * 24; // token 7 天过期(Hour)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.aida.seller.common.constants;
|
||||
|
||||
public class MinioBucketNameConstants {
|
||||
//默认桶名
|
||||
public static final String USER = "aida-user";
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.aida.seller.common.constants;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* MinIO文件命名常量类
|
||||
* 统一管理不同类型图片的命名规范
|
||||
*
|
||||
* @author Fida Team
|
||||
* @date 2026-03-09
|
||||
*/
|
||||
public class MinioFileConstants {
|
||||
|
||||
/**
|
||||
* 文件路径分隔符
|
||||
*/
|
||||
public static final String PATH_SEPARATOR = "/";
|
||||
|
||||
/**
|
||||
* 图片文件扩展名
|
||||
*/
|
||||
public static final String PNG_EXTENSION = ".png";
|
||||
public static final String JPG_EXTENSION = ".jpg";
|
||||
public static final String JPEG_EXTENSION = ".jpeg";
|
||||
|
||||
/**
|
||||
* 生成to_real_style图片文件名(仅路径部分)
|
||||
* 格式: to_real_style/UUID.png
|
||||
*
|
||||
* @return 文件路径(不含桶名)
|
||||
*/
|
||||
public static String generateToRealStyleObjectName() {
|
||||
return FileType.TO_REAL_STYLE.getDir() + PATH_SEPARATOR + UUID.randomUUID() + PNG_EXTENSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件类型生成对应的对象名称
|
||||
*
|
||||
* @param fileType 文件类型
|
||||
* @return 对象名称
|
||||
*/
|
||||
public static String generateObjectNameByType(FileType fileType) {
|
||||
return switch (fileType) {
|
||||
case TO_REAL_STYLE -> generateToRealStyleObjectName();
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 文件类型枚举
|
||||
*/
|
||||
public enum FileType {
|
||||
|
||||
TO_REAL_STYLE("to_real_style");
|
||||
|
||||
private final String dir;
|
||||
|
||||
FileType(String dir) {
|
||||
this.dir = dir;
|
||||
}
|
||||
|
||||
public String getDir() {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.aida.seller.common.constants;
|
||||
|
||||
public class OrderConstants {
|
||||
|
||||
private OrderConstants() {}
|
||||
|
||||
public static final Integer ORDER_PENDING = 0;
|
||||
public static final Integer ORDER_PAID = 1;
|
||||
public static final Integer ORDER_SHIPPED = 2;
|
||||
public static final Integer ORDER_COMPLETED = 3;
|
||||
public static final Integer ORDER_CANCELLED = 4;
|
||||
public static final Integer ORDER_REFUND = 5;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.aida.seller.common.constants;
|
||||
|
||||
public class ProductConstants {
|
||||
|
||||
private ProductConstants() {}
|
||||
|
||||
public static final Integer PRODUCT_DRAFT = 0;
|
||||
public static final Integer PRODUCT_ON_SALE = 1;
|
||||
public static final Integer PRODUCT_OFF_SALE = 2;
|
||||
public static final Integer PRODUCT_AUDITING = 3;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.aida.seller.common.constants;
|
||||
|
||||
public class StatusConstants {
|
||||
|
||||
private StatusConstants() {}
|
||||
|
||||
public static final Integer ENABLE = 1;
|
||||
public static final Integer DISABLE = 0;
|
||||
|
||||
public static final Integer DELETE = 1;
|
||||
public static final Integer NOT_DELETE = 0;
|
||||
|
||||
public static final Integer AUDIT_PENDING = 0;
|
||||
public static final Integer AUDIT_PASS = 1;
|
||||
public static final Integer AUDIT_REJECT = 2;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.aida.seller.common.context;
|
||||
|
||||
import com.aida.seller.model.vo.AuthPrincipalVo;
|
||||
|
||||
public class UserContext {
|
||||
private static final ThreadLocal<AuthPrincipalVo> userHolder = new ThreadLocal<>();
|
||||
|
||||
public static AuthPrincipalVo getUserHolder() {
|
||||
return userHolder.get();
|
||||
}
|
||||
|
||||
public static void delete() {
|
||||
userHolder.remove();
|
||||
}
|
||||
|
||||
public static void setUserHolder(AuthPrincipalVo authPrincipalVo) {
|
||||
userHolder.set(authPrincipalVo);
|
||||
}
|
||||
|
||||
public static Long getUserId() {
|
||||
AuthPrincipalVo holder = userHolder.get();
|
||||
return holder != null ? holder.getId() : null;
|
||||
}
|
||||
}
|
||||
24
src/main/java/com/aida/seller/common/dto/LoginDTO.java
Normal file
24
src/main/java/com/aida/seller/common/dto/LoginDTO.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.aida.seller.common.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "登录请求")
|
||||
public class LoginDTO {
|
||||
|
||||
@Schema(description = "用户名")
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "密码")
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "验证码")
|
||||
private String captcha;
|
||||
|
||||
@Schema(description = "验证码Key")
|
||||
private String captchaKey;
|
||||
}
|
||||
31
src/main/java/com/aida/seller/common/dto/LoginVO.java
Normal file
31
src/main/java/com/aida/seller/common/dto/LoginVO.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.aida.seller.common.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "登录响应")
|
||||
public class LoginVO {
|
||||
|
||||
@Schema(description = "访问令牌")
|
||||
private String accessToken;
|
||||
|
||||
@Schema(description = "令牌类型")
|
||||
private String tokenType = "Bearer";
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "商家ID")
|
||||
private Long sellerId;
|
||||
|
||||
@Schema(description = "过期时间(毫秒)")
|
||||
private Long expiresIn;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.aida.seller.common.exception;
|
||||
|
||||
import com.aida.seller.common.result.ResultEnum;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* @author: dwjian
|
||||
* @description: 业务异常
|
||||
*/
|
||||
@Data
|
||||
@Slf4j
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private Integer code;
|
||||
private String msg;
|
||||
|
||||
public BusinessException(ResultEnum resultEnum) {
|
||||
this.code = resultEnum.getCode();
|
||||
this.msg = resultEnum.getMsg();
|
||||
}
|
||||
|
||||
public BusinessException(String msg) {
|
||||
this.code = ResultEnum.FAIL.getCode();
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public BusinessException(String msg, Integer code) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public BusinessException(Throwable cause) {
|
||||
this.code = ResultEnum.FAIL.getCode();
|
||||
this.msg = cause.getMessage();
|
||||
}
|
||||
|
||||
public BusinessException(ResultEnum resultEnum, String customMsg) {
|
||||
this.code = resultEnum.getCode();
|
||||
this.msg = customMsg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.aida.seller.common.exception;
|
||||
|
||||
import com.aida.seller.common.result.Response;
|
||||
import com.aida.seller.common.result.ResultEnum;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public Response<?> handleBusinessException(BusinessException e) {
|
||||
log.error("业务异常: code={}, msg={}", e.getCode(), e.getMsg());
|
||||
return Response.fail(e.getCode(), e.getMsg());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Response<?> handleValidationException(MethodArgumentNotValidException e) {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining(", "));
|
||||
log.error("参数校验异常: {}", message);
|
||||
return Response.fail(ResultEnum.PARAMETER_ERROR.getCode(), message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(BindException.class)
|
||||
public Response<?> handleBindException(BindException e) {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining(", "));
|
||||
log.error("参数绑定异常: {}", message);
|
||||
return Response.fail(ResultEnum.PARAMETER_ERROR.getCode(), message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public Response<?> handleException(Exception e) {
|
||||
log.error("系统异常: ", e);
|
||||
return Response.error("系统繁忙,请稍后再试");
|
||||
}
|
||||
/**
|
||||
* 处理MinIO异常
|
||||
*/
|
||||
@ExceptionHandler(MinioException.class)
|
||||
public ResponseEntity<Object> handleMinioException(MinioException e) {
|
||||
log.error("[MinioException] {}", e.getMessage(), e);
|
||||
String message = e.getMessage();
|
||||
if (message != null && (message.contains("文件不能为空") || message.contains("不能为空"))) {
|
||||
Response<?> response = Response.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "File cannot be empty");
|
||||
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
Response<?> response = Response.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "File storage service error");
|
||||
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.aida.seller.common.exception;
|
||||
|
||||
/**
|
||||
* MinIO 操作异常类
|
||||
* 用于处理 MinIO 相关的业务异常
|
||||
*
|
||||
* @author Aida
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
public class MinioException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private String errorCode;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param message 异常信息
|
||||
*/
|
||||
public MinioException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param message 异常信息
|
||||
* @param cause 原因
|
||||
*/
|
||||
public MinioException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param errorCode 错误码
|
||||
* @param message 异常信息
|
||||
*/
|
||||
public MinioException(String errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param errorCode 错误码
|
||||
* @param message 异常信息
|
||||
* @param cause 原因
|
||||
*/
|
||||
public MinioException(String errorCode, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误码
|
||||
*
|
||||
* @return 错误码
|
||||
*/
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置错误码
|
||||
*
|
||||
* @param errorCode 错误码
|
||||
*/
|
||||
public void setErrorCode(String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.aida.seller.common.result;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @ClassName PageResponse
|
||||
* @Description 分页响应
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "分页响应结果")
|
||||
public class PageResponse<T> extends Response<List<T>> {
|
||||
@Schema(description = "页码")
|
||||
private long page;
|
||||
@Schema(description = "每页数量")
|
||||
private long size;
|
||||
@Schema(description = "总页数")
|
||||
private long pages;
|
||||
@Schema(description = "总条数")
|
||||
private long total;
|
||||
@Schema(description = "结果集")
|
||||
private List<T> content;
|
||||
|
||||
public PageResponse(Response<List<T>> response, long page, long size, long total, long pages) {
|
||||
if (response != null) {
|
||||
this.setData(response.getData());
|
||||
this.setErrCode(response.getErrCode());
|
||||
this.setErrMsg(response.getErrMsg());
|
||||
}
|
||||
this.page = page;
|
||||
this.size = size;
|
||||
this.total = total;
|
||||
this.pages = pages;
|
||||
this.content = response.getData();
|
||||
}
|
||||
|
||||
public static <T> PageResponse<T> success(IPage<T> page) {
|
||||
Response<List<T>> response = success(page.getRecords());
|
||||
return new PageResponse<>(response, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
}
|
||||
98
src/main/java/com/aida/seller/common/result/Response.java
Normal file
98
src/main/java/com/aida/seller/common/result/Response.java
Normal file
@@ -0,0 +1,98 @@
|
||||
package com.aida.seller.common.result;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @ClassName Response
|
||||
* @Description success代表响应成功 fail代表主动响应失败 error代表系统异常
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "响应结果")
|
||||
public class Response<T> implements Serializable {
|
||||
|
||||
@Schema(description = "响应状态码 0:成功 -1:失败")
|
||||
private int errCode;
|
||||
@Schema(description = "提示消息")
|
||||
private String errMsg;
|
||||
@Schema(description = "数据结果")
|
||||
private T data;
|
||||
|
||||
public static <T> Response<T> success() {
|
||||
return success(ResultEnum.SUCCESS, null);
|
||||
}
|
||||
|
||||
public static <T> Response<T> success(T data) {
|
||||
return success(ResultEnum.SUCCESS, data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> success(ResultEnum resultEnum, T data) {
|
||||
return getResponse(resultEnum.getCode(), resultEnum.getMsg(), data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> success(int code, T data) {
|
||||
return success(code, ResultEnum.SUCCESS.getMsg(), data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> success(int code, String msg, T data) {
|
||||
return getResponse(code, msg, data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(String msg) {
|
||||
return fail(ResultEnum.FAIL.getCode(), msg);
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(T data) {
|
||||
return fail(ResultEnum.FAIL, data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(ResultEnum resultEnum) {
|
||||
return fail(resultEnum.getCode(), resultEnum.getMsg(), null);
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(ResultEnum resultEnum, T data) {
|
||||
return getResponse(resultEnum.getCode(), resultEnum.getMsg(), data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(int code, String msg) {
|
||||
return fail(code, msg, null);
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(int code, String msg, T data) {
|
||||
return getResponse(code, msg, data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> error(String msg) {
|
||||
return error(ResultEnum.ERROR.getCode(), msg);
|
||||
}
|
||||
|
||||
public static <T> Response<T> error(T data) {
|
||||
return error(ResultEnum.ERROR, data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> error(ResultEnum resultEnum) {
|
||||
return error(resultEnum.getCode(), resultEnum.getMsg(), null);
|
||||
}
|
||||
|
||||
public static <T> Response<T> error(ResultEnum resultEnum, T data) {
|
||||
return getResponse(resultEnum.getCode(), resultEnum.getMsg(), data);
|
||||
}
|
||||
|
||||
public static <T> Response<T> error(int code, String msg) {
|
||||
return error(code, msg, null);
|
||||
}
|
||||
|
||||
public static <T> Response<T> error(int code, String msg, T data) {
|
||||
return getResponse(code, msg, data);
|
||||
}
|
||||
|
||||
private static <T> Response<T> getResponse(int code, String msg, T data) {
|
||||
return new Response<>(code, msg, data);
|
||||
}
|
||||
}
|
||||
55
src/main/java/com/aida/seller/common/result/ResultEnum.java
Normal file
55
src/main/java/com/aida/seller/common/result/ResultEnum.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package com.aida.seller.common.result;
|
||||
|
||||
/**
|
||||
* @ClassName ResultEnum
|
||||
* @Description 响应结果枚举
|
||||
*/
|
||||
public enum ResultEnum {
|
||||
|
||||
SUCCESS(true, 0, "SUCCESS"),
|
||||
FAIL(false, -1, "FAIL"),
|
||||
ERROR(false, -1, "system error!"),
|
||||
PARAMETER_ERROR(false, -2, "parameter error!"),
|
||||
|
||||
NO_LOGIN(false, -100, "User not logged in"),
|
||||
NO_PERMISSION(false, -200, "No access"),
|
||||
ACCOUNT_LOCK(false, -300, "Account frozen"),
|
||||
|
||||
PROMPT(false, 1, "Prompt"),
|
||||
WARNING(false, 2, "Warning"),
|
||||
;
|
||||
|
||||
private int code;
|
||||
private String msg;
|
||||
private boolean isOK;
|
||||
|
||||
ResultEnum(boolean isOK, int code, String msg) {
|
||||
this.isOK = isOK;
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getMsg() {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public void setMsg(String msg) {
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public boolean isOK() {
|
||||
return isOK;
|
||||
}
|
||||
|
||||
public void setOK(boolean OK) {
|
||||
isOK = OK;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.aida.seller.common.security.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "spring.security")
|
||||
public class SecurityProperties {
|
||||
|
||||
private String jwtSecret;
|
||||
|
||||
private String jwtTokenHeader = "Authorization";
|
||||
|
||||
private String jwtTokenPrefix = "Bearer-";
|
||||
|
||||
private long jwtExpiration;
|
||||
|
||||
private String[] ignorePaths;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.aida.seller.common.security.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.aida.seller.common.context.UserContext;
|
||||
import com.aida.seller.common.security.config.SecurityProperties;
|
||||
import com.aida.seller.common.security.jwt.SellerJwtHelper;
|
||||
import com.aida.seller.common.security.utils.SellerRedisUtil;
|
||||
import com.aida.seller.model.vo.AuthPrincipalVo;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.aida.seller.common.security.utils.SellerLocalCacheUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SellerAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final SellerJwtHelper jwtHelper;
|
||||
private final SecurityProperties securityProperties;
|
||||
private final SellerRedisUtil redisUtil;
|
||||
|
||||
private static final List<String> FILTER_URL = Arrays.asList(
|
||||
"/favicon.ico", "/doc.html", "/swagger-ui.html",
|
||||
"/swagger-resources", "/swagger-resources/", "/swagger-resources/configuration/ui",
|
||||
"/swagger-resources/configuration/security", "/webjars/", "/v2/api-docs",
|
||||
"/v3/api-docs", "/v3/api-docs/swagger-config",
|
||||
"/api/account/login", "/api/account/preLogin",
|
||||
"/api/designer/check"
|
||||
);
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
String requestURI = request.getRequestURI();
|
||||
|
||||
if (isIgnoredPath(requestURI)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
String jwtToken = request.getHeader(securityProperties.getJwtTokenHeader());
|
||||
if (StrUtil.isBlank(jwtToken)) {
|
||||
writeUnauthorized(response, "请传入token!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. JWT 签名验证
|
||||
if (!jwtHelper.validateToken(jwtToken)) {
|
||||
writeUnauthorized(response, "Token无效!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 解析用户信息
|
||||
AuthPrincipalVo principal = jwtHelper.parserToUser(jwtToken);
|
||||
if (principal == null || principal.getId() == null) {
|
||||
writeUnauthorized(response, "Token解析失败!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 本地缓存比对
|
||||
String cacheToken = SellerLocalCacheUtils.getTokenCache(principal.getId());
|
||||
if (StrUtil.isNotBlank(cacheToken)) {
|
||||
// 本地缓存有,直接比对
|
||||
if (!cacheToken.equals(jwtToken)) {
|
||||
writeUnauthorized(response, "Token已被顶替,请重新登录!");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 本地缓存没有,查 Redis
|
||||
String redisToken = redisUtil.getLoginToken(principal.getId());
|
||||
if (!StrUtil.isNotBlank(redisToken)) {
|
||||
// Redis 也没有,说明真的失效了
|
||||
writeUnauthorized(response, "Token已过期,请重新登录!");
|
||||
return;
|
||||
}
|
||||
if (!redisToken.equals(jwtToken)) {
|
||||
writeUnauthorized(response, "Token已被顶替,请重新登录!");
|
||||
return;
|
||||
}
|
||||
// Redis 有, 回填到本地缓存,减少后续 Redis 访问
|
||||
SellerLocalCacheUtils.setTokenCache(principal.getId(), jwtToken);
|
||||
}
|
||||
|
||||
// 4. 设置用户上下文
|
||||
UserContext.delete();
|
||||
UserContext.setUserHolder(principal);
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("JWT验证失败: {}", e.getMessage());
|
||||
writeUnauthorized(response, "Token已过期,请重新登录!");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isIgnoredPath(String requestURI) {
|
||||
// 检查配置文件中的白名单
|
||||
if (securityProperties.getIgnorePaths() != null) {
|
||||
for (String path : securityProperties.getIgnorePaths()) {
|
||||
String pattern = path.replace("/**", "").replace("*", "");
|
||||
if (requestURI.contains(pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 检查硬编码的白名单
|
||||
for (String url : FILTER_URL) {
|
||||
if (requestURI.contains(url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void writeUnauthorized(HttpServletResponse response, String message) throws IOException {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType("application/json");
|
||||
response.getWriter().write("{\"code\":401,\"message\":\"" + message + "\"}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.aida.seller.common.security.jwt;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.aida.seller.common.security.config.SecurityProperties;
|
||||
import com.aida.seller.model.vo.AuthPrincipalVo;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SellerJwtHelper {
|
||||
|
||||
private final SecurityProperties securityProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private static final String ISSUER = "DWJ";
|
||||
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
Claims claims = parser(token);
|
||||
return claims != null && StrUtil.isNotEmpty(claims.getSubject());
|
||||
} catch (Exception e) {
|
||||
log.error("JWT签名验证失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public AuthPrincipalVo parserToUser(String token) {
|
||||
try {
|
||||
String subject = parser(token).getSubject();
|
||||
if (StrUtil.isNotEmpty(subject)) {
|
||||
return objectMapper.readValue(subject, AuthPrincipalVo.class);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("JWT解析用户信息失败: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Claims parser(String token) {
|
||||
token = token.replaceAll(securityProperties.getJwtTokenPrefix(), "");
|
||||
SecretKey key = buildSigningKey();
|
||||
return Jwts.parser()
|
||||
.verifyWith(key)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
|
||||
private SecretKey buildSigningKey() {
|
||||
byte[] raw = securityProperties.getJwtSecret().getBytes(StandardCharsets.UTF_8);
|
||||
if (raw.length < 32) {
|
||||
raw = DigestUtil.sha256(raw);
|
||||
}
|
||||
return Keys.hmacShaKeyFor(raw);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.aida.seller.common.security.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
public class SellerLocalCacheUtils {
|
||||
|
||||
private static final ConcurrentHashMap<Long, CacheEntry> TOKEN_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private static final long EXPIRE_HOURS = 24 * 7 - 1;
|
||||
|
||||
private static class CacheEntry {
|
||||
private final String token;
|
||||
private final long expireTime;
|
||||
|
||||
CacheEntry(String token, long expireTime) {
|
||||
this.token = token;
|
||||
this.expireTime = expireTime;
|
||||
}
|
||||
|
||||
String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
boolean isExpired() {
|
||||
return System.currentTimeMillis() > expireTime;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setTokenCache(Long userId, String token) {
|
||||
long expireTime = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(EXPIRE_HOURS);
|
||||
TOKEN_CACHE.put(userId, new CacheEntry(token, expireTime));
|
||||
}
|
||||
|
||||
public static String getTokenCache(Long userId) {
|
||||
CacheEntry entry = TOKEN_CACHE.get(userId);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
if (entry.isExpired()) {
|
||||
TOKEN_CACHE.remove(userId);
|
||||
return null;
|
||||
}
|
||||
return entry.getToken();
|
||||
}
|
||||
|
||||
public static void delTokenCache(Long userId) {
|
||||
TOKEN_CACHE.remove(userId);
|
||||
}
|
||||
|
||||
public static void clearAll() {
|
||||
TOKEN_CACHE.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.aida.seller.common.security.utils;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SellerRedisUtil {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
public static final String LOGIN_TOKEN_KEY = "login:token:";
|
||||
|
||||
public String getLoginToken(Long userId) {
|
||||
try {
|
||||
Object value = redisTemplate.opsForValue().get(LOGIN_TOKEN_KEY + userId);
|
||||
return value != null ? value.toString() : null;
|
||||
} catch (Exception e) {
|
||||
log.error("Redis getLoginToken error, userId: {}, error: {}", userId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setLoginToken(Long userId, String token, long expireMillis) {
|
||||
try {
|
||||
long expireSeconds = expireMillis / 1000;
|
||||
if (expireSeconds <= 0) {
|
||||
expireSeconds = 1;
|
||||
}
|
||||
redisTemplate.opsForValue().set(LOGIN_TOKEN_KEY + userId, token, expireSeconds, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.error("Redis setLoginToken error, userId: {}, error: {}", userId, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user