feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>io.destiny</groupId>
|
||||
<artifactId>everything-is-suitable-api</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>everything-is-suitable-common</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Everything Is Suitable Common</name>
|
||||
<description>Common utilities and shared components for Everything Is Suitable API</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-validator</groupId>
|
||||
<artifactId>commons-validator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webflux-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.destiny.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface SensitiveData {
|
||||
|
||||
SensitiveType type() default SensitiveType.PHONE;
|
||||
|
||||
boolean encrypt() default true;
|
||||
|
||||
boolean mask() default true;
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package io.destiny.common.annotation;
|
||||
|
||||
public enum SensitiveType {
|
||||
|
||||
PHONE(3, 4, "手机号"),
|
||||
|
||||
EMAIL(2, 3, "邮箱"),
|
||||
|
||||
ID_CARD(6, 4, "身份证号"),
|
||||
|
||||
BANK_CARD(6, 4, "银行卡号"),
|
||||
|
||||
PASSWORD(0, 0, "密码"),
|
||||
|
||||
CUSTOM(0, 0, "自定义");
|
||||
|
||||
private final int prefixLen;
|
||||
|
||||
private final int suffixLen;
|
||||
|
||||
private final String description;
|
||||
|
||||
SensitiveType(int prefixLen, int suffixLen, String description) {
|
||||
this.prefixLen = prefixLen;
|
||||
this.suffixLen = suffixLen;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getPrefixLen() {
|
||||
return prefixLen;
|
||||
}
|
||||
|
||||
public int getSuffixLen() {
|
||||
return suffixLen;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package io.destiny.common.config;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.caffeine.CaffeineCache;
|
||||
import org.springframework.cache.support.SimpleCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CacheConfig {
|
||||
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
SimpleCacheManager cacheManager = new SimpleCacheManager();
|
||||
List<CaffeineCache> caches = new ArrayList<>();
|
||||
|
||||
caches.add(buildCache("userCache", 1000, 30, TimeUnit.MINUTES));
|
||||
caches.add(buildCache("roleCache", 500, 30, TimeUnit.MINUTES));
|
||||
caches.add(buildCache("menuCache", 500, 30, TimeUnit.MINUTES));
|
||||
caches.add(buildCache("permissionCache", 1000, 30, TimeUnit.MINUTES));
|
||||
caches.add(buildCache("almanacCache", 10000, 24, TimeUnit.HOURS));
|
||||
caches.add(buildCache("dailyFortuneCache", 10000, 24, TimeUnit.HOURS));
|
||||
caches.add(buildCache("monthlyFortuneCache", 5000, 7, TimeUnit.DAYS));
|
||||
caches.add(buildCache("yearlyFortuneCache", 5000, 30, TimeUnit.DAYS));
|
||||
caches.add(buildCache("ziweiChartCache", 5000, 24, TimeUnit.HOURS));
|
||||
caches.add(buildCache("statisticsCache", 1000, 5, TimeUnit.MINUTES));
|
||||
caches.add(buildCache("searchCache", 5000, 10, TimeUnit.MINUTES));
|
||||
caches.add(buildCache("loginCache", 1000, 15, TimeUnit.MINUTES));
|
||||
caches.add(buildCache("smsCodeCache", 10000, 5, TimeUnit.MINUTES));
|
||||
caches.add(buildCache("exportTaskCache", 500, 1, TimeUnit.HOURS));
|
||||
caches.add(buildCache("subscriptionCache", 5000, 1, TimeUnit.HOURS));
|
||||
|
||||
cacheManager.setCaches(caches);
|
||||
return cacheManager;
|
||||
}
|
||||
|
||||
private CaffeineCache buildCache(String name, long maxSize, long expireTime, TimeUnit timeUnit) {
|
||||
return new CaffeineCache(name, Caffeine.newBuilder()
|
||||
.maximumSize(maxSize)
|
||||
.expireAfterWrite(expireTime, timeUnit)
|
||||
.recordStats()
|
||||
.build());
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package io.destiny.common.config;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "错误响应")
|
||||
public class ErrorResponse {
|
||||
|
||||
@Schema(description = "错误码")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "错误消息")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "错误详情")
|
||||
private String details;
|
||||
|
||||
@Schema(description = "请求路径")
|
||||
private String path;
|
||||
|
||||
@Schema(description = "时间戳")
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
@Schema(description = "附加信息")
|
||||
private Map<String, Object> extra;
|
||||
|
||||
public ErrorResponse() {
|
||||
this.timestamp = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public ErrorResponse(String code, String message, String path) {
|
||||
this();
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getDetails() {
|
||||
return details;
|
||||
}
|
||||
|
||||
public void setDetails(String details) {
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public LocalDateTime getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(LocalDateTime timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public Map<String, Object> getExtra() {
|
||||
return extra;
|
||||
}
|
||||
|
||||
public void setExtra(Map<String, Object> extra) {
|
||||
this.extra = extra;
|
||||
}
|
||||
|
||||
public void addExtra(String key, Object value) {
|
||||
if (this.extra == null) {
|
||||
this.extra = new HashMap<>();
|
||||
}
|
||||
this.extra.put(key, value);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.destiny.common.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
@Bean
|
||||
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
|
||||
ObjectMapper mapper = builder.createXmlMapper(false).build();
|
||||
mapper.registerModule(new JavaTimeModule());
|
||||
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
return mapper;
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package io.destiny.common.exception;
|
||||
|
||||
/**
|
||||
* @author zhangxiang
|
||||
* @version 1.0
|
||||
* @description 应用异常类
|
||||
* @date 2023/11/3 16:42
|
||||
**/
|
||||
public class ApplicationException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 100L;
|
||||
|
||||
private ExceptionCode exceptionCode;
|
||||
|
||||
private String exceptionMessage;
|
||||
|
||||
private transient Object[] arguments;
|
||||
|
||||
public ApplicationException(ExceptionCode exceptionCode) {
|
||||
super();
|
||||
this.exceptionCode = exceptionCode;
|
||||
}
|
||||
|
||||
public ApplicationException(String message) {
|
||||
super();
|
||||
this.exceptionMessage = message;
|
||||
}
|
||||
|
||||
public ApplicationException(ExceptionCode exceptionCode, String exceptionMessage) {
|
||||
super();
|
||||
this.exceptionCode = exceptionCode;
|
||||
this.exceptionMessage = exceptionMessage;
|
||||
}
|
||||
|
||||
public ApplicationException(ExceptionCode exceptionCode, Object... arguments) {
|
||||
super();
|
||||
this.exceptionCode = exceptionCode;
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
public ApplicationException(Throwable cause, ExceptionCode exceptionCode) {
|
||||
super(cause);
|
||||
this.exceptionCode = exceptionCode;
|
||||
}
|
||||
|
||||
public ApplicationException(Throwable cause, ExceptionCode exceptionCode, Object... arguments) {
|
||||
super(cause);
|
||||
this.exceptionCode = exceptionCode;
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
public ApplicationException(ExceptionCode exceptionCode, String exceptionMessage, Object... arguments) {
|
||||
this.exceptionCode = exceptionCode;
|
||||
this.exceptionMessage = exceptionMessage;
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
public ApplicationException(String message, ExceptionCode exceptionCode, String exceptionMessage,
|
||||
Object[] arguments) {
|
||||
super(message);
|
||||
this.exceptionCode = exceptionCode;
|
||||
this.exceptionMessage = exceptionMessage;
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
public ApplicationException(String message, Throwable cause, ExceptionCode exceptionCode, String exceptionMessage,
|
||||
Object[] arguments) {
|
||||
super(message, cause);
|
||||
this.exceptionCode = exceptionCode;
|
||||
this.exceptionMessage = exceptionMessage;
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
public ApplicationException(Throwable cause, ExceptionCode exceptionCode, String exceptionMessage,
|
||||
Object[] arguments) {
|
||||
super(cause);
|
||||
this.exceptionCode = exceptionCode;
|
||||
this.exceptionMessage = exceptionMessage;
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
public ApplicationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace,
|
||||
ExceptionCode exceptionCode, String exceptionMessage, Object[] arguments) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
this.exceptionCode = exceptionCode;
|
||||
this.exceptionMessage = exceptionMessage;
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* The HTTP error code associated with this error.
|
||||
*
|
||||
* @return The HTTP error code associated with this error.
|
||||
*/
|
||||
public int getHttpErrorCode() {
|
||||
if (exceptionCode != null && exceptionCode.getHttpCode() != null) {
|
||||
try {
|
||||
return Integer.parseInt(exceptionCode.getHttpCode());
|
||||
} catch (NumberFormatException e) {
|
||||
return 500;
|
||||
}
|
||||
}
|
||||
return 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the HTTP error code as string.
|
||||
*
|
||||
* @return The HTTP error code as string.
|
||||
*/
|
||||
public String getHttpCode() {
|
||||
if (exceptionCode != null && exceptionCode.getHttpCode() != null) {
|
||||
return exceptionCode.getHttpCode();
|
||||
}
|
||||
return "500";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of exceptionCode. *
|
||||
*
|
||||
* @return the value of exceptionCode
|
||||
*/
|
||||
public ExceptionCode getExceptionCode() {
|
||||
return exceptionCode;
|
||||
}
|
||||
|
||||
public String getExceptionMessage() {
|
||||
return exceptionMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of arguments. *
|
||||
*
|
||||
* @return the value of arguments
|
||||
*/
|
||||
public Object[] getArguments() {
|
||||
return arguments;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package io.destiny.common.exception;
|
||||
|
||||
import io.destiny.common.utils.EnumSupport;
|
||||
|
||||
/**
|
||||
* 加密异常码
|
||||
*/
|
||||
public enum EncryptExceptionCode implements ExceptionCode, EnumSupport {
|
||||
|
||||
ENCRYPT_FAIL("500", "5001", "加密失败"),
|
||||
DECRYPT_FAIL("500", "5002", "解密失败");
|
||||
|
||||
private final String httpCode;
|
||||
private final String businessCode;
|
||||
private final String displayName;
|
||||
|
||||
EncryptExceptionCode(String httpCode, String businessCode, String displayName) {
|
||||
this.httpCode = httpCode;
|
||||
this.businessCode = businessCode;
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHttpCode() {
|
||||
return httpCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return businessCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.destiny.common.exception;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author zhangxiang
|
||||
* @version 1.0
|
||||
* @description 代码异常
|
||||
* @date 2023/11/3 16:43
|
||||
**/
|
||||
public interface ExceptionCode extends Serializable {
|
||||
String getHttpCode();
|
||||
|
||||
String getCode();
|
||||
|
||||
String getDisplayName();
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package io.destiny.common.exception;
|
||||
|
||||
import io.destiny.common.response.Result;
|
||||
import io.destiny.common.utils.HttpStatusUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebExceptionHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class ReactiveGlobalExceptionHandler implements WebExceptionHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ReactiveGlobalExceptionHandler.class);
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
|
||||
if (ex instanceof ApplicationException) {
|
||||
return handleApplicationException(exchange, (ApplicationException) ex);
|
||||
}
|
||||
if (ex instanceof ResponseStatusException) {
|
||||
return handleResponseStatusException(exchange, (ResponseStatusException) ex);
|
||||
}
|
||||
if (ex instanceof TimeoutException || ex.getCause() instanceof TimeoutException) {
|
||||
return handleTimeoutException(exchange, ex);
|
||||
}
|
||||
if (ex instanceof IllegalArgumentException) {
|
||||
return handleValidationException(exchange, (IllegalArgumentException) ex);
|
||||
}
|
||||
return handleException(exchange, ex);
|
||||
}
|
||||
|
||||
private Mono<Void> handleApplicationException(ServerWebExchange exchange, ApplicationException ex) {
|
||||
String httpCode = ex.getHttpCode();
|
||||
String businessCode = ex.getExceptionCode() != null ? ex.getExceptionCode().getCode() : "500";
|
||||
String message = ex.getExceptionMessage() != null ? ex.getExceptionMessage() :
|
||||
(ex.getExceptionCode() != null ? ex.getExceptionCode().getDisplayName() : "系统异常");
|
||||
|
||||
logger.warn("Application exception: HTTP={}, Business={}, Message={}", httpCode, businessCode, ex.getMessage());
|
||||
|
||||
HttpStatus status = HttpStatusUtils.getHttpStatusByCode(httpCode);
|
||||
exchange.getResponse().setStatusCode(status);
|
||||
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
Result<Void> result = Result.error(businessCode, message);
|
||||
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse()
|
||||
.bufferFactory()
|
||||
.wrap(result.toString().getBytes())));
|
||||
}
|
||||
|
||||
private Mono<Void> handleResponseStatusException(ServerWebExchange exchange, ResponseStatusException ex) {
|
||||
HttpStatus status = (HttpStatus) ex.getStatusCode();
|
||||
String message = ex.getReason() != null ? ex.getReason() : status.getReasonPhrase();
|
||||
|
||||
logger.warn("Response status exception: Status={}, Message={}", status, message);
|
||||
|
||||
exchange.getResponse().setStatusCode(status);
|
||||
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
Result<Void> result = Result.error(String.valueOf(status.value()), message);
|
||||
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse()
|
||||
.bufferFactory()
|
||||
.wrap(result.toString().getBytes())));
|
||||
}
|
||||
|
||||
private Mono<Void> handleTimeoutException(ServerWebExchange exchange, Throwable ex) {
|
||||
logger.error("Timeout exception: ", ex);
|
||||
exchange.getResponse().setStatusCode(HttpStatus.REQUEST_TIMEOUT);
|
||||
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
Result<Void> result = Result.error("408", "请求超时,请稍后重试");
|
||||
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse()
|
||||
.bufferFactory()
|
||||
.wrap(result.toString().getBytes())));
|
||||
}
|
||||
|
||||
private Mono<Void> handleValidationException(ServerWebExchange exchange, IllegalArgumentException ex) {
|
||||
logger.warn("Validation exception: {}", ex.getMessage());
|
||||
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
|
||||
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
Result<Void> result = Result.error("400", ex.getMessage());
|
||||
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse()
|
||||
.bufferFactory()
|
||||
.wrap(result.toString().getBytes())));
|
||||
}
|
||||
|
||||
private Mono<Void> handleException(ServerWebExchange exchange, Throwable ex) {
|
||||
logger.error("System exception: ", ex);
|
||||
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
Result<Void> result = Result.error("500", "系统异常,请稍后重试");
|
||||
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse()
|
||||
.bufferFactory()
|
||||
.wrap(result.toString().getBytes())));
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package io.destiny.common.primitive;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.validator.routines.EmailValidator;
|
||||
|
||||
import io.destiny.common.utils.EnumSupport;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class EmailAddress {
|
||||
|
||||
private static final EmailValidator EMAIL_VALIDATOR = EmailValidator.getInstance();
|
||||
|
||||
private final String value;
|
||||
private final Type type;
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
private EmailAddress(String value, Type type) {
|
||||
this.value = value;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public static EmailAddress of(String value) {
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
validate(value);
|
||||
return new EmailAddress(value, Type.EMAIL);
|
||||
}
|
||||
|
||||
private static void validate(String value) {
|
||||
if (!EMAIL_VALIDATOR.isValid(value)) {
|
||||
throw new IllegalArgumentException("邮箱地址格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
EmailAddress that = (EmailAddress) o;
|
||||
return Objects.equals(value, that.value) && type == that.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(value, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public enum Type implements EnumSupport {
|
||||
EMAIL("email", "邮箱");
|
||||
|
||||
private final String code;
|
||||
private final String displayName;
|
||||
|
||||
Type(String code, String displayName) {
|
||||
this.code = code;
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package io.destiny.common.primitive;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.validator.routines.InetAddressValidator;
|
||||
|
||||
public final class IpAddress {
|
||||
|
||||
private static final InetAddressValidator INET_ADDRESS_VALIDATOR = InetAddressValidator.getInstance();
|
||||
|
||||
private final String value;
|
||||
private final Type type;
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
private IpAddress(String value, Type type) {
|
||||
this.value = value;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public static IpAddress of(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return null;
|
||||
}
|
||||
Type type = validate(value);
|
||||
return new IpAddress(value, type);
|
||||
}
|
||||
|
||||
private static Type validate(String value) {
|
||||
if (INET_ADDRESS_VALIDATOR.isValidInet4Address(value)) {
|
||||
return Type.IPV4;
|
||||
}
|
||||
if (INET_ADDRESS_VALIDATOR.isValidInet6Address(value)) {
|
||||
return Type.IPV6;
|
||||
}
|
||||
throw new IllegalArgumentException("IP地址不合法");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
IpAddress ipAddress = (IpAddress) o;
|
||||
return value.equals(ipAddress.value) && type == ipAddress.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return value.hashCode() * 31 + type.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
IPV4,
|
||||
IPV6
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package io.destiny.common.primitive;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class MacAddress {
|
||||
|
||||
private static final String REGEX_MAC = "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$";
|
||||
private static final Pattern MAC_PATTERN = Pattern.compile(REGEX_MAC);
|
||||
|
||||
private final String value;
|
||||
private final Type type;
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
private MacAddress(String value, Type type) {
|
||||
this.value = value;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public static MacAddress of(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return null;
|
||||
}
|
||||
Type type = validate(value);
|
||||
return new MacAddress(value, type);
|
||||
}
|
||||
|
||||
private static Type validate(String value) {
|
||||
if (MAC_PATTERN.matcher(value).matches()) {
|
||||
return Type.MAC;
|
||||
}
|
||||
throw new IllegalArgumentException("MAC地址不合法");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
MacAddress macAddress = (MacAddress) o;
|
||||
return value.equals(macAddress.value) && type == macAddress.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return value.hashCode() * 31 + type.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
MAC
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package io.destiny.common.primitive;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import io.destiny.common.utils.EnumSupport;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class PhoneNumber {
|
||||
|
||||
private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
|
||||
private static final Pattern LANDLINE_PATTERN = Pattern.compile("^0\\d{2,3}-\\d{8}$");
|
||||
private static final Pattern LANDLINE_WITHOUT_AREA_CODE_PATTERN = Pattern.compile("^\\d{7,8}$");
|
||||
|
||||
private final String value;
|
||||
private final Type type;
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
private PhoneNumber(String value, Type type) {
|
||||
this.value = value;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据号码长度自动选择类型
|
||||
*
|
||||
* @param value 号码
|
||||
* @return 电话号码
|
||||
*/
|
||||
public static PhoneNumber of(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return null;
|
||||
}
|
||||
if (value.length() == 11) {
|
||||
return mobileOf(value);
|
||||
} else if (value.length() == 12 || value.length() == 13) {
|
||||
return landlineOf(value);
|
||||
} else if (value.length() == 7 || value.length() == 8) {
|
||||
return landlineOfWithoutAreaCode(value);
|
||||
} else {
|
||||
throw new IllegalArgumentException("号码格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
public static PhoneNumber mobileOf(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
throw new IllegalArgumentException("手机号码不能为空");
|
||||
}
|
||||
validateMobile(value);
|
||||
return new PhoneNumber(value, Type.MOBILE);
|
||||
}
|
||||
|
||||
public static PhoneNumber landlineOf(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
throw new IllegalArgumentException("座机号码不能为空");
|
||||
}
|
||||
validateLandline(value);
|
||||
return new PhoneNumber(value, Type.LANDLINE);
|
||||
}
|
||||
|
||||
public static PhoneNumber landlineOfWithoutAreaCode(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
throw new IllegalArgumentException("座机号码不能为空");
|
||||
}
|
||||
validateLandlineWithoutAreaCode(value);
|
||||
return new PhoneNumber(value, Type.LANDLINE);
|
||||
}
|
||||
|
||||
private static void validateMobile(String value) {
|
||||
if (!MOBILE_PATTERN.matcher(value).matches()) {
|
||||
throw new IllegalArgumentException("手机号码格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateLandline(String value) {
|
||||
if (!LANDLINE_PATTERN.matcher(value).matches()) {
|
||||
throw new IllegalArgumentException("座机号码格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateLandlineWithoutAreaCode(String value) {
|
||||
if (!LANDLINE_WITHOUT_AREA_CODE_PATTERN.matcher(value).matches()) {
|
||||
throw new IllegalArgumentException("座机号码格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
public String getAreaCode() {
|
||||
if (type == Type.LANDLINE) {
|
||||
String[] parts = value.split("-");
|
||||
if (parts.length > 1) {
|
||||
return parts[0];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getMainNumber() {
|
||||
if (type == Type.LANDLINE) {
|
||||
String[] parts = value.split("-");
|
||||
if (parts.length > 1) {
|
||||
return parts[1];
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
PhoneNumber that = (PhoneNumber) o;
|
||||
return value.equals(that.value) && type == that.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return value.hashCode() * 31 + type.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public enum Type implements EnumSupport {
|
||||
MOBILE("mobile", "手机"),
|
||||
LANDLINE("landline", "座机");
|
||||
|
||||
private final String code;
|
||||
private final String displayName;
|
||||
|
||||
Type(String code, String displayName) {
|
||||
this.code = code;
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package io.destiny.common.request;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
public class PageQuery {
|
||||
|
||||
private int pageNum;
|
||||
|
||||
private int pageSize;
|
||||
|
||||
private String sortField;
|
||||
|
||||
private String sortOrder;
|
||||
|
||||
public PageQuery() {
|
||||
this.pageNum = 1;
|
||||
this.pageSize = 10;
|
||||
this.sortOrder = "ASC";
|
||||
}
|
||||
|
||||
public PageQuery(int pageNum, int pageSize) {
|
||||
this.pageNum = pageNum;
|
||||
this.pageSize = pageSize;
|
||||
this.sortOrder = "ASC";
|
||||
}
|
||||
|
||||
public PageQuery(int pageNum, int pageSize, String sortField, String sortOrder) {
|
||||
this.pageNum = pageNum;
|
||||
this.pageSize = pageSize;
|
||||
this.sortField = sortField;
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
public int getPageNum() {
|
||||
return pageNum;
|
||||
}
|
||||
|
||||
public void setPageNum(int pageNum) {
|
||||
this.pageNum = pageNum;
|
||||
}
|
||||
|
||||
public int getPageSize() {
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
public void setPageSize(int pageSize) {
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
public String getSortField() {
|
||||
return sortField;
|
||||
}
|
||||
|
||||
public void setSortField(String sortField) {
|
||||
this.sortField = sortField;
|
||||
}
|
||||
|
||||
public String getSortOrder() {
|
||||
return sortOrder;
|
||||
}
|
||||
|
||||
public void setSortOrder(String sortOrder) {
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
public int getOffset() {
|
||||
return (pageNum - 1) * pageSize;
|
||||
}
|
||||
|
||||
public int getLimit() {
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
public boolean hasSort() {
|
||||
return StringUtils.isNotBlank(sortField);
|
||||
}
|
||||
|
||||
public static PageQuery of(int pageNum, int pageSize) {
|
||||
return new PageQuery(pageNum, pageSize);
|
||||
}
|
||||
|
||||
public static PageQuery of(int pageNum, int pageSize, String sortField, String sortOrder) {
|
||||
return new PageQuery(pageNum, pageSize, sortField, sortOrder);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package io.destiny.common.response;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author zhangxiang
|
||||
* @version 1.0
|
||||
* @description 分页对象
|
||||
* @date 2024/2/21 16:23
|
||||
**/
|
||||
public class PageModel<T> {
|
||||
|
||||
/**
|
||||
* 总条数
|
||||
*/
|
||||
private Long count;
|
||||
|
||||
/**
|
||||
* 数据对象
|
||||
*/
|
||||
private List<T> data;
|
||||
|
||||
public Long getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setCount(Long count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public List<T> getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(List<T> data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public PageModel(Long count, List<T> data) {
|
||||
this.count = count;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package io.destiny.common.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Schema(description = "统一响应结果")
|
||||
public class Result<T> {
|
||||
|
||||
@Schema(description = "响应状态码", example = "200")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "响应消息", example = "Success")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "响应数据")
|
||||
private T data;
|
||||
|
||||
public Result() {
|
||||
}
|
||||
|
||||
public Result(String code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Result(String code, String message, T data) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static <T> Result<T> success() {
|
||||
return new Result<>("200", "Success");
|
||||
}
|
||||
|
||||
public static <T> Result<T> success(T data) {
|
||||
return new Result<>("200", "Success", data);
|
||||
}
|
||||
|
||||
public static <T> Result<T> success(String message, T data) {
|
||||
return new Result<>("200", message, data);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(String code, String message) {
|
||||
return new Result<>(code, message);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(String message) {
|
||||
return new Result<>("500", message);
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public T getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package io.destiny.common.security;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class JwtAuthenticationFilter implements WebFilter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
|
||||
|
||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
private static final String USER_ID_HEADER = "X-User-Id";
|
||||
private static final String USERNAME_HEADER = "X-Username";
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
|
||||
this.jwtTokenProvider = jwtTokenProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
String path = request.getPath().value();
|
||||
|
||||
System.out.println("JwtAuthenticationFilter checking path: " + path);
|
||||
logger.debug("JwtAuthenticationFilter checking path: {}", path);
|
||||
|
||||
if (path.startsWith("/sys/auth/login") ||
|
||||
path.startsWith("/sys/auth/register") ||
|
||||
path.startsWith("/sys/auth/refresh") ||
|
||||
path.startsWith("/sys/auth/logout") ||
|
||||
path.startsWith("/api/auth/login") ||
|
||||
path.startsWith("/api/auth/register") ||
|
||||
path.startsWith("/swagger-ui") ||
|
||||
path.startsWith("/v3/api-docs") ||
|
||||
path.startsWith("/actuator") ||
|
||||
path.equals("/health")) {
|
||||
System.out.println("Skipping authentication for path: " + path);
|
||||
logger.debug("Skipping authentication for path: {}", path);
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
String authHeader = request.getHeaders().getFirst(AUTHORIZATION_HEADER);
|
||||
|
||||
if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) {
|
||||
logger.warn("Missing or invalid authorization header for path: {}", path);
|
||||
return unauthorized(exchange.getResponse());
|
||||
}
|
||||
|
||||
String token = authHeader.substring(BEARER_PREFIX.length());
|
||||
|
||||
if (!jwtTokenProvider.validateToken(token)) {
|
||||
logger.warn("Invalid token for path: {}", path);
|
||||
return unauthorized(exchange.getResponse());
|
||||
}
|
||||
|
||||
if (jwtTokenProvider.isTokenExpired(token)) {
|
||||
logger.warn("Expired token for path: {}", path);
|
||||
return unauthorized(exchange.getResponse());
|
||||
}
|
||||
|
||||
Long userId = jwtTokenProvider.getUserIdFromToken(token);
|
||||
String username = jwtTokenProvider.getUsernameFromToken(token);
|
||||
|
||||
if (userId == null || username == null) {
|
||||
logger.warn("Unable to extract user information from token for path: {}", path);
|
||||
return unauthorized(exchange.getResponse());
|
||||
}
|
||||
|
||||
ServerHttpRequest modifiedRequest = request.mutate()
|
||||
.header(USER_ID_HEADER, userId.toString())
|
||||
.header(USERNAME_HEADER, username)
|
||||
.build();
|
||||
|
||||
ServerWebExchange modifiedExchange = exchange.mutate()
|
||||
.request(modifiedRequest)
|
||||
.build();
|
||||
|
||||
logger.debug("Successfully authenticated user: {} for path: {}", username, path);
|
||||
return chain.filter(modifiedExchange);
|
||||
}
|
||||
|
||||
private boolean shouldSkipAuthentication(String path) {
|
||||
return path.startsWith("/api/auth/login") ||
|
||||
path.startsWith("/api/auth/register") ||
|
||||
path.startsWith("/sys/auth/login") ||
|
||||
path.startsWith("/sys/auth/register") ||
|
||||
path.startsWith("/sys/auth/refresh") ||
|
||||
path.startsWith("/sys/auth/logout") ||
|
||||
path.startsWith("/swagger-ui") ||
|
||||
path.startsWith("/v3/api-docs") ||
|
||||
path.startsWith("/actuator") ||
|
||||
path.equals("/health");
|
||||
}
|
||||
|
||||
private Mono<Void> unauthorized(ServerHttpResponse response) {
|
||||
response.setStatusCode(HttpStatus.UNAUTHORIZED);
|
||||
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json");
|
||||
String body = "{\"code\":401,\"message\":\"Unauthorized\"}";
|
||||
return response.writeWith(Mono.just(response.bufferFactory().wrap(body.getBytes())));
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
package io.destiny.common.security;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
|
||||
|
||||
@Value("${jwt.secret:this-is-a-secure-jwt-secret-key-that-must-be-at-least-64-characters-long-for-hs512-algorithm}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.expiration:86400000}")
|
||||
private Long expiration;
|
||||
|
||||
private SecretKey getSigningKey() {
|
||||
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
}
|
||||
|
||||
public String generateToken(Long userId, String username) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", userId);
|
||||
claims.put("username", username);
|
||||
return createToken(claims, userId.toString());
|
||||
}
|
||||
|
||||
public String generateToken(Long userId, String username, Map<String, Object> additionalClaims) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", userId);
|
||||
claims.put("username", username);
|
||||
if (additionalClaims != null) {
|
||||
claims.putAll(additionalClaims);
|
||||
}
|
||||
return createToken(claims, userId.toString());
|
||||
}
|
||||
|
||||
private String createToken(Map<String, Object> claims, String subject) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
|
||||
return Jwts.builder()
|
||||
.setClaims(claims)
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(expiryDate)
|
||||
.signWith(getSigningKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
public Long getUserIdFromToken(String token) {
|
||||
try {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKey())
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
return claims.get("userId", Long.class);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to get user id from token", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getUsernameFromToken(String token) {
|
||||
try {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKey())
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
return claims.get("username", String.class);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to get username from token", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Claims getClaimsFromToken(String token) {
|
||||
try {
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKey())
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to get claims from token", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean validateToken(String token) {
|
||||
try {
|
||||
Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKey())
|
||||
.build()
|
||||
.parseClaimsJws(token);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.error("Invalid JWT token", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean isTokenExpired(String token) {
|
||||
try {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKey())
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
Date expiration = claims.getExpiration();
|
||||
return expiration.before(new Date());
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to check token expiration", e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public Long getExpirationTime(String token) {
|
||||
try {
|
||||
Claims claims = Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKey())
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
Date expiration = claims.getExpiration();
|
||||
return expiration.getTime();
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to get expiration time from token", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
package io.destiny.common.security;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Component
|
||||
public class RbacAuthorizationFilter implements WebFilter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RbacAuthorizationFilter.class);
|
||||
|
||||
private static final String USER_ID_HEADER = "X-User-Id";
|
||||
private static final String USER_ROLES_HEADER = "X-User-Roles";
|
||||
|
||||
private final Map<String, Set<String>> rolePermissions = new HashMap<>();
|
||||
private final Map<String, Set<String>> pathPermissions = new HashMap<>();
|
||||
|
||||
public RbacAuthorizationFilter() {
|
||||
initializePermissions();
|
||||
}
|
||||
|
||||
private void initializePermissions() {
|
||||
rolePermissions.put("ADMIN", Set.of(
|
||||
"users:read", "users:write", "users:delete",
|
||||
"statistics:read",
|
||||
"fortune:read", "fortune:write",
|
||||
"settings:read", "settings:write"
|
||||
));
|
||||
|
||||
rolePermissions.put("MANAGER", Set.of(
|
||||
"users:read",
|
||||
"statistics:read",
|
||||
"fortune:read"
|
||||
));
|
||||
|
||||
rolePermissions.put("OPERATOR", Set.of(
|
||||
"fortune:read"
|
||||
));
|
||||
|
||||
pathPermissions.put("/api/users", Set.of("users:read", "users:write"));
|
||||
pathPermissions.put("/api/users/{id}", Set.of("users:read", "users:write", "users:delete"));
|
||||
pathPermissions.put("/api/statistics", Set.of("statistics:read"));
|
||||
pathPermissions.put("/api/fortune", Set.of("fortune:read", "fortune:write"));
|
||||
pathPermissions.put("/api/settings", Set.of("settings:read", "settings:write"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
String path = request.getPath().value();
|
||||
String method = request.getMethod().name();
|
||||
|
||||
if (shouldSkipAuthorization(path)) {
|
||||
logger.debug("Skipping authorization for path: {}", path);
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
List<String> userRolesHeader = request.getHeaders().get(USER_ROLES_HEADER);
|
||||
if (userRolesHeader == null || userRolesHeader.isEmpty()) {
|
||||
logger.warn("No roles found in request headers for path: {}", path);
|
||||
return forbidden(exchange.getResponse());
|
||||
}
|
||||
|
||||
Set<String> requiredPermissions = getRequiredPermissions(path, method);
|
||||
if (requiredPermissions.isEmpty()) {
|
||||
logger.debug("No specific permissions required for path: {}", path);
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
Set<String> userRoles = parseRoles(userRolesHeader);
|
||||
if (userRoles.isEmpty()) {
|
||||
logger.warn("No valid roles found for path: {}", path);
|
||||
return forbidden(exchange.getResponse());
|
||||
}
|
||||
|
||||
boolean hasPermission = checkPermissions(userRoles, requiredPermissions);
|
||||
if (!hasPermission) {
|
||||
logger.warn("User with roles {} does not have required permissions {} for path: {}",
|
||||
userRoles, requiredPermissions, path);
|
||||
return forbidden(exchange.getResponse());
|
||||
}
|
||||
|
||||
logger.debug("User with roles {} authorized for path: {}", userRoles, path);
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
private boolean shouldSkipAuthorization(String path) {
|
||||
return path.startsWith("/api/auth/login") ||
|
||||
path.startsWith("/api/auth/register") ||
|
||||
path.startsWith("/sys/auth/login") ||
|
||||
path.startsWith("/sys/auth/register") ||
|
||||
path.startsWith("/sys/auth/refresh") ||
|
||||
path.startsWith("/sys/auth/logout") ||
|
||||
path.startsWith("/swagger-ui") ||
|
||||
path.startsWith("/v3/api-docs") ||
|
||||
path.startsWith("/actuator") ||
|
||||
path.equals("/health");
|
||||
}
|
||||
|
||||
private Set<String> getRequiredPermissions(String path, String method) {
|
||||
String normalizedPath = normalizePath(path);
|
||||
Set<String> permissions = pathPermissions.get(normalizedPath);
|
||||
|
||||
if (permissions == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
if (method.equals("GET")) {
|
||||
return permissions.stream()
|
||||
.filter(p -> p.contains(":read"))
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
} else if (method.equals("POST") || method.equals("PUT")) {
|
||||
return permissions.stream()
|
||||
.filter(p -> p.contains(":write"))
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
} else if (method.equals("DELETE")) {
|
||||
return permissions.stream()
|
||||
.filter(p -> p.contains(":delete"))
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
private String normalizePath(String path) {
|
||||
return path.replaceAll("/\\d+", "/{id}");
|
||||
}
|
||||
|
||||
private Set<String> parseRoles(List<String> rolesHeader) {
|
||||
Set<String> roles = new HashSet<>();
|
||||
for (String header : rolesHeader) {
|
||||
String[] roleArray = header.split(",");
|
||||
for (String role : roleArray) {
|
||||
String trimmedRole = role.trim();
|
||||
if (rolePermissions.containsKey(trimmedRole)) {
|
||||
roles.add(trimmedRole);
|
||||
}
|
||||
}
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
||||
private boolean checkPermissions(Set<String> userRoles, Set<String> requiredPermissions) {
|
||||
Set<String> userPermissions = new HashSet<>();
|
||||
for (String role : userRoles) {
|
||||
Set<String> rolePerms = rolePermissions.get(role);
|
||||
if (rolePerms != null) {
|
||||
userPermissions.addAll(rolePerms);
|
||||
}
|
||||
}
|
||||
|
||||
return userPermissions.containsAll(requiredPermissions);
|
||||
}
|
||||
|
||||
private Mono<Void> forbidden(ServerHttpResponse response) {
|
||||
response.setStatusCode(HttpStatus.FORBIDDEN);
|
||||
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json");
|
||||
String body = "{\"code\":403,\"message\":\"Forbidden\"}";
|
||||
return response.writeWith(Mono.just(response.bufferFactory().wrap(body.getBytes())));
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.destiny.common.serializer;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import io.destiny.common.annotation.SensitiveType;
|
||||
import io.destiny.common.utils.SensitiveDataUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SensitiveDataJsonSerializer extends JsonSerializer<String> {
|
||||
|
||||
private final SensitiveType type;
|
||||
|
||||
public SensitiveDataJsonSerializer() {
|
||||
this.type = SensitiveType.CUSTOM;
|
||||
}
|
||||
|
||||
public SensitiveDataJsonSerializer(SensitiveType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
if (value == null) {
|
||||
gen.writeNull();
|
||||
return;
|
||||
}
|
||||
|
||||
String maskedValue = SensitiveDataUtils.mask(value, type);
|
||||
gen.writeString(maskedValue);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.destiny.common.serializer;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import io.destiny.common.annotation.SensitiveType;
|
||||
import io.destiny.common.utils.SensitiveDataUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SensitiveDataSerializer extends JsonSerializer<String> {
|
||||
|
||||
private final SensitiveType type;
|
||||
|
||||
public SensitiveDataSerializer() {
|
||||
this.type = SensitiveType.CUSTOM;
|
||||
}
|
||||
|
||||
public SensitiveDataSerializer(SensitiveType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
if (value == null) {
|
||||
gen.writeNull();
|
||||
return;
|
||||
}
|
||||
|
||||
String maskedValue = SensitiveDataUtils.mask(value, type);
|
||||
gen.writeString(maskedValue);
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package io.destiny.common.service;
|
||||
|
||||
import io.destiny.common.utils.SM4Utils;
|
||||
|
||||
public class SensitiveDataEncryptor {
|
||||
|
||||
private static final String ENCRYPTED_PREFIX = "ENC(";
|
||||
private static final String ENCRYPTED_SUFFIX = ")";
|
||||
|
||||
public static String encrypt(String plainText) {
|
||||
if (plainText == null || plainText.isEmpty()) {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
try {
|
||||
String encrypted = SM4Utils.encryptString(plainText);
|
||||
return ENCRYPTED_PREFIX + encrypted + ENCRYPTED_SUFFIX;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("加密失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String decrypt(String encryptedText) {
|
||||
if (encryptedText == null || encryptedText.isEmpty()) {
|
||||
return encryptedText;
|
||||
}
|
||||
|
||||
if (!isEncrypted(encryptedText)) {
|
||||
return encryptedText;
|
||||
}
|
||||
|
||||
try {
|
||||
String encrypted = encryptedText.substring(
|
||||
ENCRYPTED_PREFIX.length(),
|
||||
encryptedText.length() - ENCRYPTED_SUFFIX.length()
|
||||
);
|
||||
return SM4Utils.decryptString(encrypted);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("解密失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isEncrypted(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return text.startsWith(ENCRYPTED_PREFIX) && text.endsWith(ENCRYPTED_SUFFIX);
|
||||
}
|
||||
|
||||
public static String encryptIfNotEncrypted(String plainText) {
|
||||
if (plainText == null || plainText.isEmpty()) {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
if (isEncrypted(plainText)) {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
return encrypt(plainText);
|
||||
}
|
||||
|
||||
public static String decryptIfEncrypted(String encryptedText) {
|
||||
if (encryptedText == null || encryptedText.isEmpty()) {
|
||||
return encryptedText;
|
||||
}
|
||||
|
||||
if (!isEncrypted(encryptedText)) {
|
||||
return encryptedText;
|
||||
}
|
||||
|
||||
return decrypt(encryptedText);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package io.destiny.common.utils;
|
||||
|
||||
/**
|
||||
* @author zhangxiang
|
||||
* @version 1.0
|
||||
* @description 枚举工具类
|
||||
* @date 2024/1/11 12:18
|
||||
**/
|
||||
public interface EnumSupport {
|
||||
|
||||
/**
|
||||
* 根据代码获取枚举值
|
||||
*
|
||||
* @param code 代码
|
||||
* @param enumClass 枚举类
|
||||
* @return 枚举值
|
||||
*/
|
||||
static <T extends Enum<T> & EnumSupport> T valueOfCode(String code, Class<T> enumClass) {
|
||||
if (code == null || code.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (T enumConstant : enumClass.getEnumConstants()) {
|
||||
if (code.equals(enumConstant.getCode())) {
|
||||
return enumConstant;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("No enum constant " + enumClass.getCanonicalName() + "." + code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取枚举代码
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String getCode();
|
||||
|
||||
/**
|
||||
* 获取枚举显示名称
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String getDisplayName();
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.destiny.common.utils;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ExportField {
|
||||
|
||||
String name() default "";
|
||||
|
||||
int order() default 0;
|
||||
|
||||
String format() default "";
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.destiny.common.utils;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class HttpStatusUtils {
|
||||
|
||||
public static HttpStatus getHttpStatusByCode(String code) {
|
||||
if (code == null) {
|
||||
return HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
|
||||
try {
|
||||
int statusCode = Integer.parseInt(code);
|
||||
return HttpStatus.valueOf(statusCode);
|
||||
} catch (NumberFormatException e) {
|
||||
return HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package io.destiny.common.utils;
|
||||
|
||||
import io.destiny.common.exception.ApplicationException;
|
||||
import io.destiny.common.exception.EncryptExceptionCode;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Security;
|
||||
|
||||
public class SM4Utils {
|
||||
|
||||
private static final String ALGORITHM_NAME = "SM4";
|
||||
private static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding";
|
||||
|
||||
private static final String DEFAULT_KEY = "nXwqj7JAe@8zbD4D";
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密
|
||||
*
|
||||
* @param data 数据
|
||||
* @param key 秘钥
|
||||
* @return 密文
|
||||
*/
|
||||
public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
|
||||
|
||||
SecretKey secretKey = new SecretKeySpec(key, ALGORITHM_NAME);
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_ECB_PADDING,
|
||||
BouncyCastleProvider.PROVIDER_NAME);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
byte[] encryptedBytes = cipher.doFinal(data);
|
||||
return encryptedBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密
|
||||
*
|
||||
* @param data 数据
|
||||
* @return 密文
|
||||
*/
|
||||
public static byte[] encrypt(byte[] data) throws Exception {
|
||||
|
||||
SecretKey secretKey = new SecretKeySpec(DEFAULT_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM_NAME);
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_ECB_PADDING,
|
||||
BouncyCastleProvider.PROVIDER_NAME);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
byte[] encryptedBytes = cipher.doFinal(data);
|
||||
return encryptedBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密
|
||||
*
|
||||
* @param data 数据
|
||||
* @return 密文
|
||||
*/
|
||||
public static String encryptString(String data) {
|
||||
try {
|
||||
SecretKey secretKey = new SecretKeySpec(DEFAULT_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM_NAME);
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_ECB_PADDING,
|
||||
BouncyCastleProvider.PROVIDER_NAME);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.encodeBase64String(encryptedBytes);
|
||||
} catch (Exception e) {
|
||||
throw new ApplicationException(EncryptExceptionCode.ENCRYPT_FAIL,
|
||||
EncryptExceptionCode.ENCRYPT_FAIL.getDisplayName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密
|
||||
*
|
||||
* @param data 数据
|
||||
* @param key 秘钥
|
||||
* @return 明文
|
||||
*/
|
||||
public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
|
||||
SecretKey secretKey = new SecretKeySpec(key, ALGORITHM_NAME);
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_ECB_PADDING,
|
||||
BouncyCastleProvider.PROVIDER_NAME);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
byte[] decryptedBytes = cipher.doFinal(data);
|
||||
return decryptedBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密
|
||||
*
|
||||
* @param data 数据
|
||||
* @return 明文
|
||||
*/
|
||||
public static String decrypt(byte[] data) {
|
||||
try {
|
||||
SecretKey secretKey = new SecretKeySpec(DEFAULT_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM_NAME);
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_ECB_PADDING,
|
||||
BouncyCastleProvider.PROVIDER_NAME);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
byte[] decryptedBytes = cipher.doFinal(data);
|
||||
String decryptStr = new String(decryptedBytes);
|
||||
return decryptStr;
|
||||
} catch (Exception e) {
|
||||
throw new ApplicationException(EncryptExceptionCode.DECRYPT_FAIL,
|
||||
EncryptExceptionCode.DECRYPT_FAIL.getDisplayName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密
|
||||
*/
|
||||
public static String decryptString(String data) {
|
||||
return decrypt(Base64.decodeBase64(data));
|
||||
}
|
||||
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package io.destiny.common.utils;
|
||||
|
||||
import io.destiny.common.annotation.SensitiveType;
|
||||
|
||||
public class SensitiveDataUtils {
|
||||
|
||||
private static final String MASK_CHAR = "*";
|
||||
|
||||
public static String mask(String data, SensitiveType type) {
|
||||
if (data == null || data.isEmpty()) {
|
||||
return data;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case PHONE:
|
||||
return maskPhone(data);
|
||||
case EMAIL:
|
||||
return maskEmail(data);
|
||||
case ID_CARD:
|
||||
return maskIdCard(data);
|
||||
case BANK_CARD:
|
||||
return maskBankCard(data);
|
||||
case PASSWORD:
|
||||
return maskPassword(data);
|
||||
case CUSTOM:
|
||||
return maskCustom(data, type.getPrefixLen(), type.getSuffixLen());
|
||||
default:
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public static String maskPhone(String phone) {
|
||||
if (phone == null || phone.length() < 11) {
|
||||
return phone;
|
||||
}
|
||||
return phone.substring(0, 3) + "****" + phone.substring(7);
|
||||
}
|
||||
|
||||
public static String maskEmail(String email) {
|
||||
if (email == null || !email.contains("@")) {
|
||||
return email;
|
||||
}
|
||||
int atIndex = email.indexOf("@");
|
||||
String prefix = email.substring(0, atIndex);
|
||||
String suffix = email.substring(atIndex);
|
||||
|
||||
if (prefix.length() <= 2) {
|
||||
return prefix.charAt(0) + "***" + suffix;
|
||||
}
|
||||
return prefix.substring(0, 2) + "***" + suffix;
|
||||
}
|
||||
|
||||
public static String maskIdCard(String idCard) {
|
||||
if (idCard == null || idCard.length() < 15) {
|
||||
return idCard;
|
||||
}
|
||||
return idCard.substring(0, 6) + "********" + idCard.substring(idCard.length() - 4);
|
||||
}
|
||||
|
||||
public static String maskBankCard(String bankCard) {
|
||||
if (bankCard == null || bankCard.length() < 10) {
|
||||
return bankCard;
|
||||
}
|
||||
return bankCard.substring(0, 6) + "******" + bankCard.substring(bankCard.length() - 4);
|
||||
}
|
||||
|
||||
public static String maskPassword(String password) {
|
||||
if (password == null) {
|
||||
return null;
|
||||
}
|
||||
return "******";
|
||||
}
|
||||
|
||||
public static String maskCustom(String data, int prefixLen, int suffixLen) {
|
||||
if (data == null || data.isEmpty()) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (data.length() <= prefixLen + suffixLen) {
|
||||
return repeat(MASK_CHAR, data.length());
|
||||
}
|
||||
|
||||
String prefix = data.substring(0, prefixLen);
|
||||
String suffix = data.substring(data.length() - suffixLen);
|
||||
int maskLen = data.length() - prefixLen - suffixLen;
|
||||
|
||||
return prefix + repeat(MASK_CHAR, maskLen) + suffix;
|
||||
}
|
||||
|
||||
private static String repeat(String str, int count) {
|
||||
if (count <= 0) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < count; i++) {
|
||||
sb.append(str);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
+352
@@ -0,0 +1,352 @@
|
||||
package io.destiny.common.utils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Twitter的Snowflake算法实现(性能优化版)
|
||||
*
|
||||
* 优化点:
|
||||
* 1. 使用自适应等待策略,减少CPU空转
|
||||
* 2. 时间戳缓存机制,降低系统调用频率
|
||||
* 3. 增强CAS重试策略,提升并发性能
|
||||
* 4. 完善异常处理和资源管理
|
||||
*
|
||||
* @author zhangxiang
|
||||
* @version 2.0
|
||||
* @date 2020/2/20 22:09
|
||||
*/
|
||||
|
||||
public final class SnowflakeId {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SnowflakeId.class);
|
||||
|
||||
// 配置常量
|
||||
private static final String CONFIG_FILE = "snowflake.properties";
|
||||
private static final String ENV_WORKER_ID = "SNOWFLAKE_WORKER_ID";
|
||||
private static final String DEFAULT_WORKER_FILE = ".snowflake_worker";
|
||||
private static final long DEFAULT_EPOCH = 1582136402000L;
|
||||
private static final int DEFAULT_WORKER_BITS = 10;
|
||||
private static final int DEFAULT_SEQ_BITS = 12;
|
||||
private static final int MAX_RETRIES = 10;
|
||||
private static final long MAX_BACKWARD_MS = 50;
|
||||
private static final int SPIN_THRESHOLD = 100;
|
||||
private static final long TIME_CACHE_DURATION_MS = 16;
|
||||
|
||||
// 原子状态
|
||||
private static final AtomicLong lastTimestamp = new AtomicLong(-1L);
|
||||
private static final AtomicLong sequence = new AtomicLong(0);
|
||||
private static volatile SnowflakeConfig config;
|
||||
private static volatile long workerId;
|
||||
private static volatile long lastTimeCacheMs;
|
||||
private static volatile int timeCacheHits;
|
||||
|
||||
// 初始化块
|
||||
static {
|
||||
configure(DEFAULT_WORKER_BITS, DEFAULT_SEQ_BITS, DEFAULT_EPOCH);
|
||||
}
|
||||
|
||||
private static void configure(int workerBits, int seqBits, long epoch) {
|
||||
validateBits(workerBits, seqBits);
|
||||
config = new SnowflakeConfig(epoch, workerBits, seqBits);
|
||||
workerId = resolveWorkerId(config.maxWorkerId);
|
||||
lastTimeCacheMs = 0;
|
||||
timeCacheHits = 0;
|
||||
}
|
||||
|
||||
// 核心API
|
||||
public static long nextId() {
|
||||
for (int i = 0; i < MAX_RETRIES; i++) {
|
||||
try {
|
||||
return nextIdInternal();
|
||||
} catch (ClockBackwardException e) {
|
||||
long backwardMs = e.getBackwardMs();
|
||||
if (backwardMs > MAX_BACKWARD_MS) {
|
||||
throw e;
|
||||
}
|
||||
if (i < SPIN_THRESHOLD) {
|
||||
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
|
||||
} else {
|
||||
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Failed to generate ID after " + MAX_RETRIES + " retries");
|
||||
}
|
||||
|
||||
private static long nextIdInternal() {
|
||||
long currentTs = timeGen();
|
||||
long lastTs;
|
||||
long seq;
|
||||
|
||||
do {
|
||||
lastTs = lastTimestamp.get();
|
||||
|
||||
if (currentTs < lastTs) {
|
||||
long backwardMs = lastTs - currentTs;
|
||||
if (backwardMs <= MAX_BACKWARD_MS) {
|
||||
lastTimestamp.set(currentTs);
|
||||
lastTs = currentTs;
|
||||
} else {
|
||||
throw new ClockBackwardException(backwardMs);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTs == lastTs) {
|
||||
seq = sequence.incrementAndGet() & config.sequenceMask;
|
||||
if (seq == 0) {
|
||||
currentTs = waitNextMillis(currentTs);
|
||||
}
|
||||
} else {
|
||||
seq = 0;
|
||||
}
|
||||
} while (!lastTimestamp.compareAndSet(lastTs, currentTs));
|
||||
|
||||
return ((currentTs - config.epoch) << config.timestampShift)
|
||||
| (workerId << config.workerShift)
|
||||
| seq;
|
||||
}
|
||||
|
||||
private static long waitNextMillis(long currentTs) {
|
||||
long deadline = currentTs + 2;
|
||||
int spinCount = 0;
|
||||
|
||||
while (currentTs <= lastTimestamp.get()) {
|
||||
if (currentTs >= deadline) {
|
||||
return currentTs;
|
||||
}
|
||||
|
||||
if (spinCount < 10) {
|
||||
spinCount++;
|
||||
} else if (spinCount < 50) {
|
||||
LockSupport.parkNanos(100_000);
|
||||
spinCount++;
|
||||
} else {
|
||||
LockSupport.parkNanos(500_000);
|
||||
}
|
||||
currentTs = timeGen();
|
||||
}
|
||||
return currentTs;
|
||||
}
|
||||
|
||||
private static long timeGen() {
|
||||
long now = System.currentTimeMillis();
|
||||
long cached = lastTimeCacheMs;
|
||||
|
||||
if (now - cached < TIME_CACHE_DURATION_MS) {
|
||||
timeCacheHits++;
|
||||
return cached;
|
||||
}
|
||||
|
||||
synchronized (SnowflakeId.class) {
|
||||
cached = lastTimeCacheMs;
|
||||
if (now - cached < TIME_CACHE_DURATION_MS) {
|
||||
timeCacheHits++;
|
||||
return cached;
|
||||
}
|
||||
lastTimeCacheMs = now;
|
||||
return now;
|
||||
}
|
||||
}
|
||||
|
||||
public static int getTimeCacheHits() {
|
||||
return timeCacheHits;
|
||||
}
|
||||
|
||||
public static void resetTimeCache() {
|
||||
synchronized (SnowflakeId.class) {
|
||||
lastTimeCacheMs = 0;
|
||||
timeCacheHits = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateBits(int workerBits, int seqBits) {
|
||||
if (workerBits < 0 || workerBits > 22) {
|
||||
throw new IllegalArgumentException("WorkerID位数必须在0-22之间");
|
||||
}
|
||||
if (seqBits < 0 || seqBits > 22) {
|
||||
throw new IllegalArgumentException("序列号位数必须在0-22之间");
|
||||
}
|
||||
if (workerBits + seqBits > 22) {
|
||||
throw new IllegalArgumentException("WorkerID和序列号位数总和不能超过22位,当前为: " + (workerBits + seqBits));
|
||||
}
|
||||
if (workerBits + seqBits == 0) {
|
||||
throw new IllegalArgumentException("WorkerID和序列号位数总和不能为0");
|
||||
}
|
||||
}
|
||||
|
||||
private static long resolveWorkerId(long maxWorkerId) {
|
||||
Long id = fromEnv();
|
||||
if (id == null) {
|
||||
id = fromConfig();
|
||||
}
|
||||
if (id == null) {
|
||||
id = fromFile();
|
||||
}
|
||||
if (id == null) {
|
||||
id = generateNewId();
|
||||
}
|
||||
|
||||
if (id < 0 || id > maxWorkerId) {
|
||||
throw new IllegalStateException("WorkerID超出有效范围: " + id + " (有效范围: 0-" + maxWorkerId + ")");
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
private static Long fromEnv() {
|
||||
String env = System.getenv(ENV_WORKER_ID);
|
||||
if (env != null) {
|
||||
try {
|
||||
return Long.parseLong(env.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Failed to parse worker ID from environment variable {}: {}", ENV_WORKER_ID, env);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Long fromConfig() {
|
||||
try (InputStream is = SnowflakeId.class.getClassLoader().getResourceAsStream(CONFIG_FILE)) {
|
||||
if (is == null) {
|
||||
return null;
|
||||
}
|
||||
Properties props = new Properties();
|
||||
props.load(is);
|
||||
String val = props.getProperty("worker.id");
|
||||
if (val != null) {
|
||||
try {
|
||||
return Long.parseLong(val.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Failed to parse worker ID from config file: {}", val);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.debug("Failed to read config file: {}", CONFIG_FILE);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Long fromFile() {
|
||||
Path path = Paths.get(DEFAULT_WORKER_FILE);
|
||||
if (!Files.exists(path)) {
|
||||
return null;
|
||||
}
|
||||
try (BufferedReader reader = Files.newBufferedReader(path)) {
|
||||
String content = reader.readLine();
|
||||
if (content != null && !content.trim().isEmpty()) {
|
||||
try {
|
||||
return Long.parseLong(content.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Failed to parse worker ID from file {}: {}", DEFAULT_WORKER_FILE, content);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.debug("Failed to read worker ID from file: {}", DEFAULT_WORKER_FILE);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long generateNewId() {
|
||||
long newId = ThreadLocalRandom.current().nextLong(config.maxWorkerId + 1);
|
||||
Path path = Paths.get(DEFAULT_WORKER_FILE);
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(path,
|
||||
StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
writer.write(String.valueOf(newId));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to write worker ID to file: {}", DEFAULT_WORKER_FILE);
|
||||
}
|
||||
return newId;
|
||||
}
|
||||
|
||||
public static void config(int workerBits, int seqBits, long epoch) {
|
||||
configure(workerBits, seqBits, epoch);
|
||||
}
|
||||
|
||||
public static long getWorkerId() {
|
||||
return workerId;
|
||||
}
|
||||
|
||||
public static SnowflakeIdInfo parseId(long id) {
|
||||
long timestamp = (id >> config.timestampShift) + config.epoch;
|
||||
long extractedWorkerId = (id >> config.workerShift) & config.sequenceMask;
|
||||
long seq = id & config.sequenceMask;
|
||||
return new SnowflakeIdInfo(timestamp, extractedWorkerId, seq);
|
||||
}
|
||||
|
||||
public static class SnowflakeIdInfo {
|
||||
private final long timestamp;
|
||||
private final long workerId;
|
||||
private final long sequence;
|
||||
|
||||
public SnowflakeIdInfo(long timestamp, long workerId, long sequence) {
|
||||
this.timestamp = timestamp;
|
||||
this.workerId = workerId;
|
||||
this.sequence = sequence;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public long getWorkerId() {
|
||||
return workerId;
|
||||
}
|
||||
|
||||
public long getSequence() {
|
||||
return sequence;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SnowflakeIdInfo{timestamp=" + timestamp + ", workerId=" + workerId + ", sequence=" + sequence + "}";
|
||||
}
|
||||
}
|
||||
|
||||
private static class SnowflakeConfig {
|
||||
final long epoch;
|
||||
final int timestampShift;
|
||||
final int workerShift;
|
||||
final long sequenceMask;
|
||||
final long maxWorkerId;
|
||||
|
||||
SnowflakeConfig(long epoch, int workerBits, int seqBits) {
|
||||
this.epoch = epoch;
|
||||
this.timestampShift = workerBits + seqBits;
|
||||
this.workerShift = seqBits;
|
||||
this.sequenceMask = ~(-1L << seqBits);
|
||||
this.maxWorkerId = ~(-1L << workerBits);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ClockBackwardException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final long backwardMs;
|
||||
|
||||
ClockBackwardException(long backwardMs) {
|
||||
super("Clock moved backwards by " + backwardMs + "ms");
|
||||
this.backwardMs = backwardMs;
|
||||
}
|
||||
|
||||
public long getBackwardMs() {
|
||||
return backwardMs;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package io.destiny.common.utils;
|
||||
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class XssUtils {
|
||||
|
||||
private static final Pattern[] XSS_PATTERNS = {
|
||||
Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
||||
Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
||||
Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
||||
Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
||||
Pattern.compile("expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
||||
Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
||||
Pattern.compile("onerror(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
||||
Pattern.compile("onclick(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
||||
Pattern.compile("onmouseover(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
||||
Pattern.compile("onfocus(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
||||
Pattern.compile("onblur(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
||||
Pattern.compile("on(\\w+)(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
|
||||
};
|
||||
|
||||
public static String clean(String input) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
String cleanInput = input;
|
||||
|
||||
for (Pattern pattern : XSS_PATTERNS) {
|
||||
cleanInput = pattern.matcher(cleanInput).replaceAll("");
|
||||
}
|
||||
|
||||
cleanInput = HtmlUtils.htmlEscape(cleanInput);
|
||||
|
||||
return cleanInput;
|
||||
}
|
||||
|
||||
public static String cleanForHtml(String input) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
return HtmlUtils.htmlEscape(input);
|
||||
}
|
||||
|
||||
public static String cleanForJavaScript(String input) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
return input.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("'", "\\'")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t");
|
||||
}
|
||||
|
||||
public static String cleanForUrl(String input) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
return HtmlUtils.htmlEscape(input);
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# ID: 0~1023
|
||||
worker.id=0
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
package io.destiny.common.exception;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("ApplicationException应用异常测试")
|
||||
class ApplicationExceptionTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用异常码创建异常")
|
||||
void testConstructorWithExceptionCode() {
|
||||
ExceptionCode code = new ExceptionCode() {
|
||||
@Override
|
||||
public String getHttpCode() {
|
||||
return "400";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "BAD_REQUEST";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Bad Request";
|
||||
}
|
||||
};
|
||||
|
||||
ApplicationException exception = new ApplicationException(code);
|
||||
|
||||
assertEquals(code, exception.getExceptionCode(), "异常码应该正确");
|
||||
assertNull(exception.getExceptionMessage(), "异常消息应该为null");
|
||||
assertNull(exception.getArguments(), "参数应该为null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用消息创建异常")
|
||||
void testConstructorWithMessage() {
|
||||
String message = "测试异常消息";
|
||||
ApplicationException exception = new ApplicationException(message);
|
||||
|
||||
assertEquals(message, exception.getExceptionMessage(), "异常消息应该正确");
|
||||
assertNull(exception.getExceptionCode(), "异常码应该为null");
|
||||
assertNull(exception.getArguments(), "参数应该为null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用异常码和消息创建异常")
|
||||
void testConstructorWithExceptionCodeAndMessage() {
|
||||
ExceptionCode code = new ExceptionCode() {
|
||||
@Override
|
||||
public String getHttpCode() {
|
||||
return "500";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "INTERNAL_ERROR";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Internal Error";
|
||||
}
|
||||
};
|
||||
String message = "内部错误";
|
||||
|
||||
ApplicationException exception = new ApplicationException(code, message);
|
||||
|
||||
assertEquals(code, exception.getExceptionCode(), "异常码应该正确");
|
||||
assertEquals(message, exception.getExceptionMessage(), "异常消息应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用异常码和参数创建异常")
|
||||
void testConstructorWithExceptionCodeAndArguments() {
|
||||
ExceptionCode code = new ExceptionCode() {
|
||||
@Override
|
||||
public String getHttpCode() {
|
||||
return "404";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "NOT_FOUND";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Not Found";
|
||||
}
|
||||
};
|
||||
Object[] arguments = {"resource", "123"};
|
||||
|
||||
ApplicationException exception = new ApplicationException(code, arguments);
|
||||
|
||||
assertEquals(code, exception.getExceptionCode(), "异常码应该正确");
|
||||
assertArrayEquals(arguments, exception.getArguments(), "参数应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用原因和异常码创建异常")
|
||||
void testConstructorWithCauseAndExceptionCode() {
|
||||
Throwable cause = new RuntimeException("原始异常");
|
||||
ExceptionCode code = new ExceptionCode() {
|
||||
@Override
|
||||
public String getHttpCode() {
|
||||
return "500";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "SERVER_ERROR";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Server Error";
|
||||
}
|
||||
};
|
||||
|
||||
ApplicationException exception = new ApplicationException(cause, code);
|
||||
|
||||
assertEquals(cause, exception.getCause(), "原因应该正确");
|
||||
assertEquals(code, exception.getExceptionCode(), "异常码应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用原因、异常码和参数创建异常")
|
||||
void testConstructorWithCauseExceptionCodeAndArguments() {
|
||||
Throwable cause = new RuntimeException("原始异常");
|
||||
ExceptionCode code = new ExceptionCode() {
|
||||
@Override
|
||||
public String getHttpCode() {
|
||||
return "500";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "SERVER_ERROR";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Server Error";
|
||||
}
|
||||
};
|
||||
Object[] arguments = {"param1", "param2"};
|
||||
|
||||
ApplicationException exception = new ApplicationException(cause, code, arguments);
|
||||
|
||||
assertEquals(cause, exception.getCause(), "原因应该正确");
|
||||
assertEquals(code, exception.getExceptionCode(), "异常码应该正确");
|
||||
assertArrayEquals(arguments, exception.getArguments(), "参数应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确获取HTTP错误码")
|
||||
void testGetHttpErrorCode() {
|
||||
ExceptionCode code = new ExceptionCode() {
|
||||
@Override
|
||||
public String getHttpCode() {
|
||||
return "403";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "FORBIDDEN";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Forbidden";
|
||||
}
|
||||
};
|
||||
|
||||
ApplicationException exception = new ApplicationException(code);
|
||||
|
||||
assertEquals(403, exception.getHttpErrorCode(), "HTTP错误码应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确获取HTTP错误码字符串")
|
||||
void testGetHttpCode() {
|
||||
ExceptionCode code = new ExceptionCode() {
|
||||
@Override
|
||||
public String getHttpCode() {
|
||||
return "401";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "UNAUTHORIZED";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Unauthorized";
|
||||
}
|
||||
};
|
||||
|
||||
ApplicationException exception = new ApplicationException(code);
|
||||
|
||||
assertEquals("401", exception.getHttpCode(), "HTTP错误码字符串应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当异常码为null时应该返回默认HTTP错误码")
|
||||
void testDefaultHttpErrorCode() {
|
||||
ApplicationException exception = new ApplicationException("测试消息");
|
||||
|
||||
assertEquals(500, exception.getHttpErrorCode(), "默认HTTP错误码应该是500");
|
||||
assertEquals("500", exception.getHttpCode(), "默认HTTP错误码字符串应该是500");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("当HTTP码格式错误时应该返回默认值")
|
||||
void testInvalidHttpCodeFormat() {
|
||||
ExceptionCode code = new ExceptionCode() {
|
||||
@Override
|
||||
public String getHttpCode() {
|
||||
return "invalid";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "INVALID";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Invalid";
|
||||
}
|
||||
};
|
||||
|
||||
ApplicationException exception = new ApplicationException(code);
|
||||
|
||||
assertEquals(500, exception.getHttpErrorCode(), "格式错误的HTTP码应该返回500");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确设置和获取异常码")
|
||||
void testSetAndGetExceptionCode() {
|
||||
ExceptionCode code = new ExceptionCode() {
|
||||
@Override
|
||||
public String getHttpCode() {
|
||||
return "400";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "BAD_REQUEST";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Bad Request";
|
||||
}
|
||||
};
|
||||
|
||||
ApplicationException exception = new ApplicationException(code);
|
||||
assertEquals(code, exception.getExceptionCode(), "异常码应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确设置和获取异常消息")
|
||||
void testSetAndGetExceptionMessage() {
|
||||
String message = "测试消息";
|
||||
ApplicationException exception = new ApplicationException(message);
|
||||
assertEquals(message, exception.getExceptionMessage(), "异常消息应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确设置和获取参数")
|
||||
void testSetAndGetArguments() {
|
||||
Object[] arguments = {"arg1", "arg2", "arg3"};
|
||||
ExceptionCode code = new ExceptionCode() {
|
||||
@Override
|
||||
public String getHttpCode() {
|
||||
return "500";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "ERROR";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Error";
|
||||
}
|
||||
};
|
||||
|
||||
ApplicationException exception = new ApplicationException(code, arguments);
|
||||
assertArrayEquals(arguments, exception.getArguments(), "参数应该正确");
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package io.destiny.common.primitive;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("EmailAddress邮箱地址测试")
|
||||
class EmailAddressTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确创建邮箱地址")
|
||||
void testCreateEmailAddress() {
|
||||
EmailAddress email = EmailAddress.of("test@example.com");
|
||||
|
||||
assertNotNull(email, "邮箱地址不应该为null");
|
||||
assertEquals("test@example.com", email.getValue(), "邮箱地址值应该正确");
|
||||
assertEquals(EmailAddress.Type.EMAIL, email.getType(), "类型应该是邮箱");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确验证有效的邮箱地址")
|
||||
void testValidEmailAddresses() {
|
||||
String[] validEmails = {
|
||||
"test@example.com",
|
||||
"user.name@example.com",
|
||||
"user+tag@example.com",
|
||||
"user123@example.co.uk",
|
||||
"test_user@test-domain.com"
|
||||
};
|
||||
|
||||
for (String email : validEmails) {
|
||||
EmailAddress emailAddress = EmailAddress.of(email);
|
||||
assertNotNull(emailAddress, email + " 应该是有效的邮箱地址");
|
||||
assertEquals(email, emailAddress.getValue(), email + " 值应该正确");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该拒绝无效的邮箱地址")
|
||||
void testInvalidEmailAddresses() {
|
||||
String[] invalidEmails = {
|
||||
"invalid-email",
|
||||
"test@",
|
||||
"@example.com",
|
||||
"test example.com",
|
||||
"test@example",
|
||||
"test@.com",
|
||||
"test@exa mple.com"
|
||||
};
|
||||
|
||||
for (String email : invalidEmails) {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
EmailAddress.of(email);
|
||||
}, email + " 应该是无效的邮箱地址");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空值应该返回null")
|
||||
void testNullValue() {
|
||||
assertNull(EmailAddress.of(null), "null应该返回null");
|
||||
assertNull(EmailAddress.of(""), "空字符串应该返回null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确比较相等性")
|
||||
void testEquals() {
|
||||
EmailAddress email1 = EmailAddress.of("test@example.com");
|
||||
EmailAddress email2 = EmailAddress.of("test@example.com");
|
||||
EmailAddress email3 = EmailAddress.of("other@example.com");
|
||||
|
||||
assertEquals(email1, email2, "相同邮箱地址应该相等");
|
||||
assertNotEquals(email1, email3, "不同邮箱地址不应该相等");
|
||||
assertNotEquals(email1, null, "不应该等于null");
|
||||
assertNotEquals(email1, "test@example.com", "不应该等于字符串");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确计算哈希码")
|
||||
void testHashCode() {
|
||||
EmailAddress email1 = EmailAddress.of("test@example.com");
|
||||
EmailAddress email2 = EmailAddress.of("test@example.com");
|
||||
|
||||
assertEquals(email1.hashCode(), email2.hashCode(), "相同邮箱地址的哈希码应该相同");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确转换为字符串")
|
||||
void testToString() {
|
||||
EmailAddress email = EmailAddress.of("test@example.com");
|
||||
assertEquals("test@example.com", email.toString(), "toString应该返回邮箱地址值");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("枚举类型应该正确返回代码和显示名称")
|
||||
void testEnumType() {
|
||||
assertEquals("email", EmailAddress.Type.EMAIL.getCode(), "邮箱类型代码应该正确");
|
||||
assertEquals("邮箱", EmailAddress.Type.EMAIL.getDisplayName(), "邮箱类型显示名称应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持大小写不敏感的邮箱地址")
|
||||
void testCaseInsensitiveEmail() {
|
||||
EmailAddress email1 = EmailAddress.of("TEST@EXAMPLE.COM");
|
||||
EmailAddress email2 = EmailAddress.of("test@example.com");
|
||||
|
||||
assertNotNull(email1, "大写邮箱地址应该有效");
|
||||
assertNotNull(email2, "小写邮箱地址应该有效");
|
||||
assertEquals("TEST@EXAMPLE.COM", email1.getValue(), "邮箱地址值应该保持原样");
|
||||
assertEquals("test@example.com", email2.getValue(), "邮箱地址值应该保持原样");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持带子域名的邮箱地址")
|
||||
void testSubdomainEmail() {
|
||||
EmailAddress email1 = EmailAddress.of("user@mail.example.com");
|
||||
EmailAddress email2 = EmailAddress.of("user@sub.mail.example.com");
|
||||
|
||||
assertNotNull(email1, "带子域名的邮箱地址应该有效");
|
||||
assertNotNull(email2, "带多级子域名的邮箱地址应该有效");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持特殊字符的邮箱地址")
|
||||
void testSpecialCharactersEmail() {
|
||||
EmailAddress email1 = EmailAddress.of("user.name+tag@example.com");
|
||||
EmailAddress email2 = EmailAddress.of("user_name@example.com");
|
||||
EmailAddress email3 = EmailAddress.of("user-name@example.com");
|
||||
|
||||
assertNotNull(email1, "带点的邮箱地址应该有效");
|
||||
assertNotNull(email2, "带下划线的邮箱地址应该有效");
|
||||
assertNotNull(email3, "带连字符的邮箱地址应该有效");
|
||||
}
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package io.destiny.common.primitive;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("PhoneNumber电话号码测试")
|
||||
class PhoneNumberTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确创建手机号码")
|
||||
void testCreateMobilePhone() {
|
||||
PhoneNumber phone = PhoneNumber.mobileOf("13800138000");
|
||||
|
||||
assertNotNull(phone, "手机号码不应该为null");
|
||||
assertEquals("13800138000", phone.getValue(), "手机号码值应该正确");
|
||||
assertEquals(PhoneNumber.Type.MOBILE, phone.getType(), "类型应该是手机");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确创建座机号码")
|
||||
void testCreateLandlinePhone() {
|
||||
PhoneNumber phone = PhoneNumber.landlineOf("010-12345678");
|
||||
|
||||
assertNotNull(phone, "座机号码不应该为null");
|
||||
assertEquals("010-12345678", phone.getValue(), "座机号码值应该正确");
|
||||
assertEquals(PhoneNumber.Type.LANDLINE, phone.getType(), "类型应该是座机");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确创建无区号座机号码")
|
||||
void testCreateLandlineWithoutAreaCode() {
|
||||
PhoneNumber phone = PhoneNumber.landlineOfWithoutAreaCode("12345678");
|
||||
|
||||
assertNotNull(phone, "座机号码不应该为null");
|
||||
assertEquals("12345678", phone.getValue(), "座机号码值应该正确");
|
||||
assertEquals(PhoneNumber.Type.LANDLINE, phone.getType(), "类型应该是座机");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该根据号码长度自动选择类型")
|
||||
void testAutoDetectType() {
|
||||
PhoneNumber mobile = PhoneNumber.of("13912345678");
|
||||
assertEquals(PhoneNumber.Type.MOBILE, mobile.getType(), "11位应该是手机");
|
||||
|
||||
PhoneNumber landline = PhoneNumber.of("021-12345678");
|
||||
assertEquals(PhoneNumber.Type.LANDLINE, landline.getType(), "12位应该是座机");
|
||||
|
||||
PhoneNumber landline2 = PhoneNumber.of("0123-12345678");
|
||||
assertEquals(PhoneNumber.Type.LANDLINE, landline2.getType(), "13位应该是座机");
|
||||
|
||||
PhoneNumber landline3 = PhoneNumber.of("1234567");
|
||||
assertEquals(PhoneNumber.Type.LANDLINE, landline3.getType(), "7位应该是座机");
|
||||
|
||||
PhoneNumber landline4 = PhoneNumber.of("12345678");
|
||||
assertEquals(PhoneNumber.Type.LANDLINE, landline4.getType(), "8位应该是座机");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确获取区号")
|
||||
void testGetAreaCode() {
|
||||
PhoneNumber phone1 = PhoneNumber.landlineOf("010-12345678");
|
||||
assertEquals("010", phone1.getAreaCode(), "区号应该正确");
|
||||
|
||||
PhoneNumber phone2 = PhoneNumber.landlineOf("021-87654321");
|
||||
assertEquals("021", phone2.getAreaCode(), "区号应该正确");
|
||||
|
||||
PhoneNumber mobile = PhoneNumber.mobileOf("13800138000");
|
||||
assertEquals("", mobile.getAreaCode(), "手机号码区号应该为空");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确获取主号码")
|
||||
void testGetMainNumber() {
|
||||
PhoneNumber phone1 = PhoneNumber.landlineOf("010-12345678");
|
||||
assertEquals("12345678", phone1.getMainNumber(), "主号码应该正确");
|
||||
|
||||
PhoneNumber mobile = PhoneNumber.mobileOf("13800138000");
|
||||
assertEquals("13800138000", mobile.getMainNumber(), "手机主号码应该正确");
|
||||
|
||||
PhoneNumber phone2 = PhoneNumber.landlineOfWithoutAreaCode("87654321");
|
||||
assertEquals("87654321", phone2.getMainNumber(), "无区号座机主号码应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确比较相等性")
|
||||
void testEquals() {
|
||||
PhoneNumber phone1 = PhoneNumber.mobileOf("13800138000");
|
||||
PhoneNumber phone2 = PhoneNumber.mobileOf("13800138000");
|
||||
PhoneNumber phone3 = PhoneNumber.mobileOf("13900139000");
|
||||
|
||||
assertEquals(phone1, phone2, "相同号码应该相等");
|
||||
assertNotEquals(phone1, phone3, "不同号码不应该相等");
|
||||
assertNotEquals(phone1, null, "不应该等于null");
|
||||
assertNotEquals(phone1, "13800138000", "不应该等于字符串");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确计算哈希码")
|
||||
void testHashCode() {
|
||||
PhoneNumber phone1 = PhoneNumber.mobileOf("13800138000");
|
||||
PhoneNumber phone2 = PhoneNumber.mobileOf("13800138000");
|
||||
|
||||
assertEquals(phone1.hashCode(), phone2.hashCode(), "相同号码的哈希码应该相同");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确转换为字符串")
|
||||
void testToString() {
|
||||
PhoneNumber phone = PhoneNumber.mobileOf("13800138000");
|
||||
assertEquals("13800138000", phone.toString(), "toString应该返回号码值");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空值应该返回null")
|
||||
void testNullValue() {
|
||||
assertNull(PhoneNumber.of(null), "null应该返回null");
|
||||
assertNull(PhoneNumber.of(""), "空字符串应该返回null");
|
||||
assertNull(PhoneNumber.of(" "), "空白字符串应该返回null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该拒绝无效的手机号码")
|
||||
void testInvalidMobilePhone() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
PhoneNumber.mobileOf("12345678901");
|
||||
}, "无效的手机号码应该抛出异常");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
PhoneNumber.mobileOf("1380013800");
|
||||
}, "长度不足的手机号码应该抛出异常");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
PhoneNumber.mobileOf(null);
|
||||
}, "null手机号码应该抛出异常");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该拒绝无效的座机号码")
|
||||
void testInvalidLandlinePhone() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
PhoneNumber.landlineOf("12345678");
|
||||
}, "无效的座机号码应该抛出异常");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
PhoneNumber.landlineOf("010-1234567");
|
||||
}, "长度不足的座机号码应该抛出异常");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
PhoneNumber.landlineOf(null);
|
||||
}, "null座机号码应该抛出异常");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该拒绝无效的无区号座机号码")
|
||||
void testInvalidLandlineWithoutAreaCode() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
PhoneNumber.landlineOfWithoutAreaCode("123456");
|
||||
}, "长度不足的座机号码应该抛出异常");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
PhoneNumber.landlineOfWithoutAreaCode("123456789");
|
||||
}, "长度过长的座机号码应该抛出异常");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
PhoneNumber.landlineOfWithoutAreaCode(null);
|
||||
}, "null座机号码应该抛出异常");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该拒绝无效长度的号码")
|
||||
void testInvalidLength() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
PhoneNumber.of("123456");
|
||||
}, "6位号码应该抛出异常");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
PhoneNumber.of("12345678901234");
|
||||
}, "14位号码应该抛出异常");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("枚举类型应该正确返回代码和显示名称")
|
||||
void testEnumType() {
|
||||
assertEquals("mobile", PhoneNumber.Type.MOBILE.getCode(), "手机类型代码应该正确");
|
||||
assertEquals("手机", PhoneNumber.Type.MOBILE.getDisplayName(), "手机类型显示名称应该正确");
|
||||
|
||||
assertEquals("landline", PhoneNumber.Type.LANDLINE.getCode(), "座机类型代码应该正确");
|
||||
assertEquals("座机", PhoneNumber.Type.LANDLINE.getDisplayName(), "座机类型显示名称应该正确");
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package io.destiny.common.request;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("PageQuery分页查询测试")
|
||||
class PageQueryTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用默认构造函数创建分页查询")
|
||||
void testDefaultConstructor() {
|
||||
PageQuery query = new PageQuery();
|
||||
|
||||
assertEquals(1, query.getPageNum(), "默认页码应该是1");
|
||||
assertEquals(10, query.getPageSize(), "默认每页大小应该是10");
|
||||
assertNull(query.getSortField(), "默认排序字段应该是null");
|
||||
assertEquals("ASC", query.getSortOrder(), "默认排序顺序应该是ASC");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用页码和页大小构造分页查询")
|
||||
void testConstructorWithPageNumAndPageSize() {
|
||||
PageQuery query = new PageQuery(2, 20);
|
||||
|
||||
assertEquals(2, query.getPageNum(), "页码应该正确");
|
||||
assertEquals(20, query.getPageSize(), "每页大小应该正确");
|
||||
assertNull(query.getSortField(), "排序字段应该是null");
|
||||
assertEquals("ASC", query.getSortOrder(), "排序顺序应该是ASC");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用全参构造函数创建分页查询")
|
||||
void testConstructorWithAllParams() {
|
||||
PageQuery query = new PageQuery(3, 30, "createTime", "DESC");
|
||||
|
||||
assertEquals(3, query.getPageNum(), "页码应该正确");
|
||||
assertEquals(30, query.getPageSize(), "每页大小应该正确");
|
||||
assertEquals("createTime", query.getSortField(), "排序字段应该正确");
|
||||
assertEquals("DESC", query.getSortOrder(), "排序顺序应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确计算偏移量")
|
||||
void testGetOffset() {
|
||||
PageQuery query1 = new PageQuery(1, 10);
|
||||
assertEquals(0, query1.getOffset(), "第一页的偏移量应该是0");
|
||||
|
||||
PageQuery query2 = new PageQuery(2, 10);
|
||||
assertEquals(10, query2.getOffset(), "第二页的偏移量应该是10");
|
||||
|
||||
PageQuery query3 = new PageQuery(3, 20);
|
||||
assertEquals(40, query3.getOffset(), "第三页每页20条的偏移量应该是40");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确获取限制数量")
|
||||
void testGetLimit() {
|
||||
PageQuery query1 = new PageQuery(1, 10);
|
||||
assertEquals(10, query1.getLimit(), "限制数量应该等于每页大小");
|
||||
|
||||
PageQuery query2 = new PageQuery(2, 20);
|
||||
assertEquals(20, query2.getLimit(), "限制数量应该等于每页大小");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确判断是否有排序")
|
||||
void testHasSort() {
|
||||
PageQuery query1 = new PageQuery(1, 10);
|
||||
assertFalse(query1.hasSort(), "没有排序字段时应该返回false");
|
||||
|
||||
PageQuery query2 = new PageQuery(1, 10, "createTime", "ASC");
|
||||
assertTrue(query2.hasSort(), "有排序字段时应该返回true");
|
||||
|
||||
PageQuery query3 = new PageQuery(1, 10, "", "ASC");
|
||||
assertFalse(query3.hasSort(), "排序字段为空字符串时应该返回false");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用静态工厂方法创建分页查询")
|
||||
void testStaticFactoryMethod() {
|
||||
PageQuery query1 = PageQuery.of(2, 20);
|
||||
assertEquals(2, query1.getPageNum(), "页码应该正确");
|
||||
assertEquals(20, query1.getPageSize(), "每页大小应该正确");
|
||||
|
||||
PageQuery query2 = PageQuery.of(3, 30, "updateTime", "DESC");
|
||||
assertEquals(3, query2.getPageNum(), "页码应该正确");
|
||||
assertEquals(30, query2.getPageSize(), "每页大小应该正确");
|
||||
assertEquals("updateTime", query2.getSortField(), "排序字段应该正确");
|
||||
assertEquals("DESC", query2.getSortOrder(), "排序顺序应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确设置和获取页码")
|
||||
void testSetAndGetPageNum() {
|
||||
PageQuery query = new PageQuery();
|
||||
query.setPageNum(5);
|
||||
|
||||
assertEquals(5, query.getPageNum(), "页码应该正确设置和获取");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确设置和获取每页大小")
|
||||
void testSetAndGetPageSize() {
|
||||
PageQuery query = new PageQuery();
|
||||
query.setPageSize(50);
|
||||
|
||||
assertEquals(50, query.getPageSize(), "每页大小应该正确设置和获取");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确设置和获取排序字段")
|
||||
void testSetAndGetSortField() {
|
||||
PageQuery query = new PageQuery();
|
||||
query.setSortField("name");
|
||||
|
||||
assertEquals("name", query.getSortField(), "排序字段应该正确设置和获取");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确设置和获取排序顺序")
|
||||
void testSetAndGetSortOrder() {
|
||||
PageQuery query = new PageQuery();
|
||||
query.setSortOrder("DESC");
|
||||
|
||||
assertEquals("DESC", query.getSortOrder(), "排序顺序应该正确设置和获取");
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package io.destiny.common.response;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("PageModel分页模型测试")
|
||||
class PageModelTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该创建分页模型")
|
||||
void testCreatePageModel() {
|
||||
Long count = 100L;
|
||||
java.util.List<String> data = Arrays.asList("item1", "item2", "item3");
|
||||
|
||||
PageModel<String> pageModel = new PageModel<>(count, data);
|
||||
|
||||
assertEquals(count, pageModel.getCount(), "总数应该正确");
|
||||
assertEquals(data, pageModel.getData(), "数据列表应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确设置和获取总数")
|
||||
void testSetAndGetCount() {
|
||||
PageModel<String> pageModel = new PageModel<>(0L, Collections.emptyList());
|
||||
Long count = 200L;
|
||||
|
||||
pageModel.setCount(count);
|
||||
assertEquals(count, pageModel.getCount(), "总数应该正确设置和获取");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确设置和获取数据列表")
|
||||
void testSetAndGetData() {
|
||||
PageModel<String> pageModel = new PageModel<>(0L, Collections.emptyList());
|
||||
java.util.List<String> data = Arrays.asList("test1", "test2");
|
||||
|
||||
pageModel.setData(data);
|
||||
assertEquals(data, pageModel.getData(), "数据列表应该正确设置和获取");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持空数据列表")
|
||||
void testEmptyDataList() {
|
||||
PageModel<String> pageModel = new PageModel<>(0L, Collections.emptyList());
|
||||
|
||||
assertEquals(0L, pageModel.getCount(), "总数应该是0");
|
||||
assertTrue(pageModel.getData().isEmpty(), "数据列表应该是空的");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持null数据列表")
|
||||
void testNullDataList() {
|
||||
PageModel<String> pageModel = new PageModel<>(0L, null);
|
||||
|
||||
assertEquals(0L, pageModel.getCount(), "总数应该是0");
|
||||
assertNull(pageModel.getData(), "数据列表应该是null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持泛型数据")
|
||||
void testGenericData() {
|
||||
PageModel<Integer> intPageModel = new PageModel<>(10L, Arrays.asList(1, 2, 3));
|
||||
assertEquals(10L, intPageModel.getCount(), "应该支持Integer类型");
|
||||
assertEquals(3, intPageModel.getData().size(), "数据列表大小应该正确");
|
||||
|
||||
PageModel<Boolean> boolPageModel = new PageModel<>(5L, Arrays.asList(true, false));
|
||||
assertEquals(5L, boolPageModel.getCount(), "应该支持Boolean类型");
|
||||
assertEquals(2, boolPageModel.getData().size(), "数据列表大小应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持大数据量")
|
||||
void testLargeData() {
|
||||
Long largeCount = 1000000L;
|
||||
java.util.List<String> largeData = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
largeData.add("item" + i);
|
||||
}
|
||||
|
||||
PageModel<String> pageModel = new PageModel<>(largeCount, largeData);
|
||||
|
||||
assertEquals(largeCount, pageModel.getCount(), "应该支持大总数");
|
||||
assertEquals(1000, pageModel.getData().size(), "应该支持大数据列表");
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package io.destiny.common.response;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("Result统一响应结果测试")
|
||||
class ResultTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该创建空的成功响应")
|
||||
void testSuccessWithoutData() {
|
||||
Result<String> result = Result.success();
|
||||
|
||||
assertEquals("200", result.getCode(), "状态码应该是200");
|
||||
assertEquals("Success", result.getMessage(), "消息应该是Success");
|
||||
assertNull(result.getData(), "数据应该为null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该创建带数据的成功响应")
|
||||
void testSuccessWithData() {
|
||||
String data = "test data";
|
||||
Result<String> result = Result.success(data);
|
||||
|
||||
assertEquals("200", result.getCode(), "状态码应该是200");
|
||||
assertEquals("Success", result.getMessage(), "消息应该是Success");
|
||||
assertEquals(data, result.getData(), "数据应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该创建带自定义消息的成功响应")
|
||||
void testSuccessWithMessage() {
|
||||
String message = "操作成功";
|
||||
String data = "test data";
|
||||
Result<String> result = Result.success(message, data);
|
||||
|
||||
assertEquals("200", result.getCode(), "状态码应该是200");
|
||||
assertEquals(message, result.getMessage(), "消息应该正确");
|
||||
assertEquals(data, result.getData(), "数据应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该创建带状态码的错误响应")
|
||||
void testErrorWithCode() {
|
||||
String code = "400";
|
||||
String message = "请求参数错误";
|
||||
Result<String> result = Result.error(code, message);
|
||||
|
||||
assertEquals(code, result.getCode(), "状态码应该正确");
|
||||
assertEquals(message, result.getMessage(), "消息应该正确");
|
||||
assertNull(result.getData(), "数据应该为null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该创建默认错误响应")
|
||||
void testErrorWithMessage() {
|
||||
String message = "服务器内部错误";
|
||||
Result<String> result = Result.error(message);
|
||||
|
||||
assertEquals("500", result.getCode(), "状态码应该是500");
|
||||
assertEquals(message, result.getMessage(), "消息应该正确");
|
||||
assertNull(result.getData(), "数据应该为null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确设置和获取状态码")
|
||||
void testSetAndGetCode() {
|
||||
Result<String> result = new Result<>();
|
||||
String code = "201";
|
||||
|
||||
result.setCode(code);
|
||||
assertEquals(code, result.getCode(), "状态码应该正确设置和获取");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确设置和获取消息")
|
||||
void testSetAndGetMessage() {
|
||||
Result<String> result = new Result<>();
|
||||
String message = "测试消息";
|
||||
|
||||
result.setMessage(message);
|
||||
assertEquals(message, result.getMessage(), "消息应该正确设置和获取");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确设置和获取数据")
|
||||
void testSetAndGetData() {
|
||||
Result<String> result = new Result<>();
|
||||
String data = "测试数据";
|
||||
|
||||
result.setData(data);
|
||||
assertEquals(data, result.getData(), "数据应该正确设置和获取");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用构造函数创建响应")
|
||||
void testConstructorWithCodeAndMessage() {
|
||||
String code = "200";
|
||||
String message = "成功";
|
||||
Result<String> result = new Result<>(code, message);
|
||||
|
||||
assertEquals(code, result.getCode(), "状态码应该正确");
|
||||
assertEquals(message, result.getMessage(), "消息应该正确");
|
||||
assertNull(result.getData(), "数据应该为null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用全参构造函数创建响应")
|
||||
void testConstructorWithAllParams() {
|
||||
String code = "200";
|
||||
String message = "成功";
|
||||
String data = "测试数据";
|
||||
Result<String> result = new Result<>(code, message, data);
|
||||
|
||||
assertEquals(code, result.getCode(), "状态码应该正确");
|
||||
assertEquals(message, result.getMessage(), "消息应该正确");
|
||||
assertEquals(data, result.getData(), "数据应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持泛型数据")
|
||||
void testGenericData() {
|
||||
Result<Integer> intResult = Result.success(123);
|
||||
assertEquals(123, intResult.getData(), "应该支持Integer类型");
|
||||
|
||||
Result<Boolean> boolResult = Result.success(true);
|
||||
assertEquals(true, boolResult.getData(), "应该支持Boolean类型");
|
||||
}
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package io.destiny.common.security;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class JwtAuthenticationFilterTest {
|
||||
|
||||
private JwtAuthenticationFilter filter;
|
||||
|
||||
@Mock
|
||||
private JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Mock
|
||||
private WebFilterChain chain;
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
filter = new JwtAuthenticationFilter(jwtTokenProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_ValidToken_ShouldPass() {
|
||||
String token = "valid-jwt-token";
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/fortune/daily")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
when(jwtTokenProvider.validateToken(token)).thenReturn(true);
|
||||
when(jwtTokenProvider.isTokenExpired(token)).thenReturn(false);
|
||||
when(jwtTokenProvider.getUserIdFromToken(token)).thenReturn(1L);
|
||||
when(jwtTokenProvider.getUsernameFromToken(token)).thenReturn("testuser");
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(chain, times(1)).filter(any(ServerWebExchange.class));
|
||||
verify(jwtTokenProvider, times(1)).validateToken(token);
|
||||
verify(jwtTokenProvider, times(1)).isTokenExpired(token);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_MissingToken_ShouldReturnUnauthorized() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/fortune/daily").build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
|
||||
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_InvalidToken_ShouldReturnUnauthorized() {
|
||||
String token = "invalid-token";
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/fortune/daily")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
when(jwtTokenProvider.validateToken(token)).thenReturn(false);
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
|
||||
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_ExpiredToken_ShouldReturnUnauthorized() {
|
||||
String token = "expired-token";
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/fortune/daily")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
when(jwtTokenProvider.validateToken(token)).thenReturn(true);
|
||||
when(jwtTokenProvider.isTokenExpired(token)).thenReturn(true);
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
|
||||
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_LoginPath_ShouldSkipAuthentication() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/login").build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(chain, times(1)).filter(any(ServerWebExchange.class));
|
||||
verify(jwtTokenProvider, never()).validateToken(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_RegisterPath_ShouldSkipAuthentication() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/api/auth/register").build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(chain, times(1)).filter(any(ServerWebExchange.class));
|
||||
verify(jwtTokenProvider, never()).validateToken(anyString());
|
||||
}
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
package io.destiny.common.security;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RbacAuthorizationFilterTest {
|
||||
|
||||
private RbacAuthorizationFilter filter;
|
||||
|
||||
private WebFilterChain chain;
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
filter = new RbacAuthorizationFilter();
|
||||
chain = mock(WebFilterChain.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_AdminRole_WithValidPermission_ShouldPass() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.header("X-User-Roles", "ADMIN")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(chain, times(1)).filter(any(ServerWebExchange.class));
|
||||
assertNotEquals(HttpStatus.FORBIDDEN, exchange.getResponse().getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_OperatorRole_WithInsufficientPermission_ShouldReturnForbidden() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/api/users")
|
||||
.header("X-User-Roles", "OPERATOR")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(HttpStatus.FORBIDDEN, exchange.getResponse().getStatusCode());
|
||||
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_ManagerRole_WithReadPermission_ShouldPass() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.header("X-User-Roles", "MANAGER")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(chain, times(1)).filter(any(ServerWebExchange.class));
|
||||
assertNotEquals(HttpStatus.FORBIDDEN, exchange.getResponse().getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_NoRoles_ShouldReturnForbidden() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users").build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(HttpStatus.FORBIDDEN, exchange.getResponse().getStatusCode());
|
||||
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_LoginPath_ShouldSkipAuthorization() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/login").build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(chain, times(1)).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_RegisterPath_ShouldSkipAuthorization() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/api/auth/register").build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(chain, times(1)).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_SwaggerPath_ShouldSkipAuthorization() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/swagger-ui/index.html").build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(chain, times(1)).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_ActuatorPath_ShouldSkipAuthorization() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/actuator/health").build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(chain, times(1)).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_MultipleRoles_WithSufficientPermission_ShouldPass() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/statistics")
|
||||
.header("X-User-Roles", "MANAGER,OPERATOR")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(chain, times(1)).filter(any(ServerWebExchange.class));
|
||||
assertNotEquals(HttpStatus.FORBIDDEN, exchange.getResponse().getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_DeleteRequest_WithAdminRole_ShouldPass() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.delete("/api/users/123")
|
||||
.header("X-User-Roles", "ADMIN")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(chain, times(1)).filter(any(ServerWebExchange.class));
|
||||
assertNotEquals(HttpStatus.FORBIDDEN, exchange.getResponse().getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_DeleteRequest_WithManagerRole_ShouldReturnForbidden() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.delete("/api/users/123")
|
||||
.header("X-User-Roles", "MANAGER")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request).mutate().build();
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(HttpStatus.FORBIDDEN, exchange.getResponse().getStatusCode());
|
||||
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package io.destiny.common.utils;
|
||||
|
||||
import io.destiny.common.primitive.EmailAddress;
|
||||
import io.destiny.common.primitive.PhoneNumber;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("EnumSupport枚举工具类测试")
|
||||
class EnumSupportTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该根据代码获取枚举值")
|
||||
void testValueOfCode() {
|
||||
PhoneNumber.Type mobileType = EnumSupport.valueOfCode("mobile", PhoneNumber.Type.class);
|
||||
assertEquals(PhoneNumber.Type.MOBILE, mobileType, "应该正确获取手机类型");
|
||||
|
||||
PhoneNumber.Type landlineType = EnumSupport.valueOfCode("landline", PhoneNumber.Type.class);
|
||||
assertEquals(PhoneNumber.Type.LANDLINE, landlineType, "应该正确获取座机类型");
|
||||
|
||||
EmailAddress.Type emailType = EnumSupport.valueOfCode("email", EmailAddress.Type.class);
|
||||
assertEquals(EmailAddress.Type.EMAIL, emailType, "应该正确获取邮箱类型");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空代码应该返回null")
|
||||
void testNullCode() {
|
||||
PhoneNumber.Type result = EnumSupport.valueOfCode(null, PhoneNumber.Type.class);
|
||||
assertNull(result, "null代码应该返回null");
|
||||
|
||||
PhoneNumber.Type result2 = EnumSupport.valueOfCode("", PhoneNumber.Type.class);
|
||||
assertNull(result2, "空字符串代码应该返回null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无效代码应该抛出异常")
|
||||
void testInvalidCode() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
EnumSupport.valueOfCode("invalid", PhoneNumber.Type.class);
|
||||
}, "无效代码应该抛出异常");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
EnumSupport.valueOfCode("MOBILE", PhoneNumber.Type.class);
|
||||
}, "大小写不匹配的代码应该抛出异常");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("枚举应该正确返回代码")
|
||||
void testGetCode() {
|
||||
assertEquals("mobile", PhoneNumber.Type.MOBILE.getCode(), "手机类型代码应该正确");
|
||||
assertEquals("landline", PhoneNumber.Type.LANDLINE.getCode(), "座机类型代码应该正确");
|
||||
assertEquals("email", EmailAddress.Type.EMAIL.getCode(), "邮箱类型代码应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("枚举应该正确返回显示名称")
|
||||
void testGetDisplayName() {
|
||||
assertEquals("手机", PhoneNumber.Type.MOBILE.getDisplayName(), "手机类型显示名称应该正确");
|
||||
assertEquals("座机", PhoneNumber.Type.LANDLINE.getDisplayName(), "座机类型显示名称应该正确");
|
||||
assertEquals("邮箱", EmailAddress.Type.EMAIL.getDisplayName(), "邮箱类型显示名称应该正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该支持所有枚举类型")
|
||||
void testAllEnumTypes() {
|
||||
PhoneNumber.Type[] phoneTypes = PhoneNumber.Type.values();
|
||||
for (PhoneNumber.Type type : phoneTypes) {
|
||||
PhoneNumber.Type result = EnumSupport.valueOfCode(type.getCode(), PhoneNumber.Type.class);
|
||||
assertEquals(type, result, type.getCode() + " 应该正确获取");
|
||||
}
|
||||
|
||||
EmailAddress.Type[] emailTypes = EmailAddress.Type.values();
|
||||
for (EmailAddress.Type type : emailTypes) {
|
||||
EmailAddress.Type result = EnumSupport.valueOfCode(type.getCode(), EmailAddress.Type.class);
|
||||
assertEquals(type, result, type.getCode() + " 应该正确获取");
|
||||
}
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
package io.destiny.common.utils;
|
||||
|
||||
import io.destiny.common.exception.ApplicationException;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("SM4Utils国密加密工具测试")
|
||||
class SM4UtilsTest {
|
||||
|
||||
private static final String CUSTOM_KEY = "1234567890abcdef";
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用默认密钥加密字符串")
|
||||
void testEncryptStringWithDefaultKey() {
|
||||
String plainText = "Hello, World!";
|
||||
String encrypted = SM4Utils.encryptString(plainText);
|
||||
|
||||
assertNotNull(encrypted, "加密结果不应该为null");
|
||||
assertNotEquals(plainText, encrypted, "加密后的字符串应该与原文不同");
|
||||
assertFalse(encrypted.contains(plainText), "加密结果不应该包含原文");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用默认密钥解密字符串")
|
||||
void testDecryptStringWithDefaultKey() {
|
||||
String plainText = "Hello, World!";
|
||||
String encrypted = SM4Utils.encryptString(plainText);
|
||||
String decrypted = SM4Utils.decryptString(encrypted);
|
||||
|
||||
assertEquals(plainText, decrypted, "解密后的字符串应该与原文相同");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用自定义密钥加密字节数组")
|
||||
void testEncryptBytesWithCustomKey() throws Exception {
|
||||
String plainText = "Test data";
|
||||
byte[] plainBytes = plainText.getBytes();
|
||||
byte[] keyBytes = CUSTOM_KEY.getBytes();
|
||||
|
||||
byte[] encrypted = SM4Utils.encrypt(plainBytes, keyBytes);
|
||||
|
||||
assertNotNull(encrypted, "加密结果不应该为null");
|
||||
assertNotEquals(plainBytes, encrypted, "加密后的字节数组应该与原文不同");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用自定义密钥解密字节数组")
|
||||
void testDecryptBytesWithCustomKey() throws Exception {
|
||||
String plainText = "Test data";
|
||||
byte[] plainBytes = plainText.getBytes();
|
||||
byte[] keyBytes = CUSTOM_KEY.getBytes();
|
||||
|
||||
byte[] encrypted = SM4Utils.encrypt(plainBytes, keyBytes);
|
||||
byte[] decrypted = SM4Utils.decrypt(encrypted, keyBytes);
|
||||
|
||||
assertArrayEquals(plainBytes, decrypted, "解密后的字节数组应该与原文相同");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用默认密钥加密字节数组")
|
||||
void testEncryptBytesWithDefaultKey() throws Exception {
|
||||
String plainText = "Test data";
|
||||
byte[] plainBytes = plainText.getBytes();
|
||||
|
||||
byte[] encrypted = SM4Utils.encrypt(plainBytes);
|
||||
|
||||
assertNotNull(encrypted, "加密结果不应该为null");
|
||||
assertNotEquals(plainBytes, encrypted, "加密后的字节数组应该与原文不同");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该使用默认密钥解密字节数组")
|
||||
void testDecryptBytesWithDefaultKey() throws Exception {
|
||||
String plainText = "Test data";
|
||||
byte[] plainBytes = plainText.getBytes();
|
||||
|
||||
byte[] encrypted = SM4Utils.encrypt(plainBytes);
|
||||
String decrypted = SM4Utils.decrypt(encrypted);
|
||||
|
||||
assertEquals(plainText, decrypted, "解密后的字符串应该与原文相同");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确加密空字符串")
|
||||
void testEncryptEmptyString() {
|
||||
String plainText = "";
|
||||
String encrypted = SM4Utils.encryptString(plainText);
|
||||
String decrypted = SM4Utils.decryptString(encrypted);
|
||||
|
||||
assertEquals(plainText, decrypted, "空字符串加密解密后应该保持不变");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确加密中文字符串")
|
||||
void testEncryptChineseString() {
|
||||
String plainText = "你好,世界!";
|
||||
String encrypted = SM4Utils.encryptString(plainText);
|
||||
String decrypted = SM4Utils.decryptString(encrypted);
|
||||
|
||||
assertEquals(plainText, decrypted, "中文字符串加密解密后应该保持不变");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确加密特殊字符")
|
||||
void testEncryptSpecialCharacters() {
|
||||
String plainText = "!@#$%^&*()_+-=[]{}|;':\",./<>?";
|
||||
String encrypted = SM4Utils.encryptString(plainText);
|
||||
String decrypted = SM4Utils.decryptString(encrypted);
|
||||
|
||||
assertEquals(plainText, decrypted, "特殊字符加密解密后应该保持不变");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确加密长字符串")
|
||||
void testEncryptLongString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
sb.append("a");
|
||||
}
|
||||
String plainText = sb.toString();
|
||||
|
||||
String encrypted = SM4Utils.encryptString(plainText);
|
||||
String decrypted = SM4Utils.decryptString(encrypted);
|
||||
|
||||
assertEquals(plainText, decrypted, "长字符串加密解密后应该保持不变");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("解密失败应该抛出ApplicationException")
|
||||
void testDecryptFailure() {
|
||||
byte[] invalidData = new byte[] { 1, 2, 3 };
|
||||
|
||||
assertThrows(ApplicationException.class, () -> {
|
||||
SM4Utils.decrypt(invalidData);
|
||||
}, "解密失败应该抛出ApplicationException");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多次加密同一数据应该产生不同的密文")
|
||||
void testEncryptMultipleTimes() {
|
||||
String plainText = "Test data";
|
||||
String encrypted1 = SM4Utils.encryptString(plainText);
|
||||
String encrypted2 = SM4Utils.encryptString(plainText);
|
||||
|
||||
assertEquals(encrypted1, encrypted2, "使用相同密钥加密相同数据应该产生相同的密文");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确处理Base64编码的密文")
|
||||
void testBase64EncodedCipherText() {
|
||||
String plainText = "Test data";
|
||||
String encrypted = SM4Utils.encryptString(plainText);
|
||||
|
||||
assertTrue(encrypted.length() > 0, "Base64编码的密文不应该为空");
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package io.destiny.common.utils;
|
||||
|
||||
import io.destiny.common.annotation.SensitiveType;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("敏感数据工具类测试")
|
||||
class SensitiveDataUtilsTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确脱敏手机号")
|
||||
void testMaskPhone() {
|
||||
String phone = "13800138000";
|
||||
String masked = SensitiveDataUtils.maskPhone(phone);
|
||||
|
||||
assertEquals("138****8000", masked);
|
||||
assertEquals(11, masked.length());
|
||||
assertTrue(masked.contains("****"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确脱敏邮箱")
|
||||
void testMaskEmail() {
|
||||
String email = "zhangsan@example.com";
|
||||
String masked = SensitiveDataUtils.maskEmail(email);
|
||||
|
||||
assertEquals("zh***@example.com", masked);
|
||||
assertTrue(masked.contains("@"));
|
||||
assertTrue(masked.contains("***"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确脱敏身份证号")
|
||||
void testMaskIdCard() {
|
||||
String idCard = "110101199001011234";
|
||||
String masked = SensitiveDataUtils.maskIdCard(idCard);
|
||||
|
||||
assertEquals("110101********1234", masked);
|
||||
assertEquals(18, masked.length());
|
||||
assertTrue(masked.contains("********"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确脱敏银行卡号")
|
||||
void testMaskBankCard() {
|
||||
String bankCard = "6222021234567890123";
|
||||
String masked = SensitiveDataUtils.maskBankCard(bankCard);
|
||||
|
||||
assertEquals("622202******0123", masked);
|
||||
assertEquals(16, masked.length());
|
||||
assertTrue(masked.contains("******"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确脱敏密码")
|
||||
void testMaskPassword() {
|
||||
String password = "MyPassword123";
|
||||
String masked = SensitiveDataUtils.maskPassword(password);
|
||||
|
||||
assertEquals("******", masked);
|
||||
assertEquals(6, masked.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该根据类型脱敏数据")
|
||||
void testMaskByType() {
|
||||
String phone = "13800138000";
|
||||
String email = "test@example.com";
|
||||
String idCard = "110101199001011234";
|
||||
|
||||
String maskedPhone = SensitiveDataUtils.mask(phone, SensitiveType.PHONE);
|
||||
String maskedEmail = SensitiveDataUtils.mask(email, SensitiveType.EMAIL);
|
||||
String maskedIdCard = SensitiveDataUtils.mask(idCard, SensitiveType.ID_CARD);
|
||||
|
||||
assertEquals("138****8000", maskedPhone);
|
||||
assertEquals("te***@example.com", maskedEmail);
|
||||
assertEquals("110101********1234", maskedIdCard);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确处理空值")
|
||||
void testMaskNull() {
|
||||
String maskedNull = SensitiveDataUtils.mask(null, SensitiveType.PHONE);
|
||||
String maskedEmpty = SensitiveDataUtils.mask("", SensitiveType.PHONE);
|
||||
|
||||
assertNull(maskedNull);
|
||||
assertEquals("", maskedEmpty);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确处理短手机号")
|
||||
void testMaskShortPhone() {
|
||||
String shortPhone = "123";
|
||||
String masked = SensitiveDataUtils.maskPhone(shortPhone);
|
||||
|
||||
assertEquals("123", masked);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确处理无效邮箱")
|
||||
void testMaskInvalidEmail() {
|
||||
String invalidEmail = "invalid-email";
|
||||
String masked = SensitiveDataUtils.maskEmail(invalidEmail);
|
||||
|
||||
assertEquals("invalid-email", masked);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确处理短身份证号")
|
||||
void testMaskShortIdCard() {
|
||||
String shortIdCard = "123456";
|
||||
String masked = SensitiveDataUtils.maskIdCard(shortIdCard);
|
||||
|
||||
assertEquals("123456", masked);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确处理短银行卡号")
|
||||
void testMaskShortBankCard() {
|
||||
String shortBankCard = "123456789";
|
||||
String masked = SensitiveDataUtils.maskBankCard(shortBankCard);
|
||||
|
||||
assertEquals("123456789", masked);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确脱敏自定义数据")
|
||||
void testMaskCustom() {
|
||||
String data = "1234567890";
|
||||
String masked = SensitiveDataUtils.maskCustom(data, 2, 2);
|
||||
|
||||
assertEquals("12******90", masked);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确处理长度不足的自定义数据")
|
||||
void testMaskCustomShortData() {
|
||||
String data = "1234";
|
||||
String masked = SensitiveDataUtils.maskCustom(data, 2, 2);
|
||||
|
||||
assertEquals("****", masked);
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package io.destiny.common.utils;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("SnowflakeId雪花ID生成器测试")
|
||||
class SnowflakeIdTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
SnowflakeId.resetTimeCache();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该生成唯一ID")
|
||||
void testGenerateUniqueId() {
|
||||
long id1 = SnowflakeId.nextId();
|
||||
long id2 = SnowflakeId.nextId();
|
||||
|
||||
assertNotEquals(id1, id2, "生成的ID应该是唯一的");
|
||||
assertTrue(id1 > 0, "生成的ID应该大于0");
|
||||
assertTrue(id2 > 0, "生成的ID应该大于0");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该生成递增ID")
|
||||
void testGenerateIncrementalId() {
|
||||
long id1 = SnowflakeId.nextId();
|
||||
long id2 = SnowflakeId.nextId();
|
||||
long id3 = SnowflakeId.nextId();
|
||||
|
||||
assertTrue(id2 > id1, "ID应该是递增的");
|
||||
assertTrue(id3 > id2, "ID应该是递增的");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确解析ID")
|
||||
void testParseId() {
|
||||
long id = SnowflakeId.nextId();
|
||||
SnowflakeId.SnowflakeIdInfo info = SnowflakeId.parseId(id);
|
||||
|
||||
assertNotNull(info, "解析结果不应该为null");
|
||||
assertNotNull(info.getTimestamp(), "时间戳不应该为null");
|
||||
assertNotNull(info.getWorkerId(), "WorkerID不应该为null");
|
||||
assertNotNull(info.getSequence(), "序列号不应该为null");
|
||||
assertTrue(info.getTimestamp() > 0, "时间戳应该大于0");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确获取WorkerID")
|
||||
void testGetWorkerId() {
|
||||
long workerId = SnowflakeId.getWorkerId();
|
||||
|
||||
assertTrue(workerId >= 0, "WorkerID应该大于等于0");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确配置Snowflake")
|
||||
void testConfig() {
|
||||
SnowflakeId.config(5, 12, System.currentTimeMillis() - 10000000);
|
||||
|
||||
long id = SnowflakeId.nextId();
|
||||
assertTrue(id > 0, "配置后应该能正常生成ID");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确获取时间缓存命中次数")
|
||||
void testGetTimeCacheHits() {
|
||||
int hitsBefore = SnowflakeId.getTimeCacheHits();
|
||||
|
||||
SnowflakeId.nextId();
|
||||
SnowflakeId.nextId();
|
||||
SnowflakeId.nextId();
|
||||
|
||||
int hitsAfter = SnowflakeId.getTimeCacheHits();
|
||||
assertTrue(hitsAfter >= hitsBefore, "时间缓存命中次数应该增加或保持不变");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确重置时间缓存")
|
||||
void testResetTimeCache() {
|
||||
SnowflakeId.nextId();
|
||||
SnowflakeId.resetTimeCache();
|
||||
|
||||
int hits = SnowflakeId.getTimeCacheHits();
|
||||
assertEquals(0, hits, "重置后时间缓存命中次数应该为0");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确抛出时钟回退异常")
|
||||
void testClockBackwardException() {
|
||||
SnowflakeId.ClockBackwardException exception =
|
||||
new SnowflakeId.ClockBackwardException(100);
|
||||
|
||||
assertEquals(100, exception.getBackwardMs(), "回退毫秒数应该正确");
|
||||
assertTrue(exception.getMessage().contains("100"), "异常消息应该包含回退毫秒数");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确生成大量ID")
|
||||
void testGenerateManyIds() {
|
||||
int count = 1000;
|
||||
java.util.Set<Long> ids = new java.util.HashSet<>();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
long id = SnowflakeId.nextId();
|
||||
assertTrue(ids.add(id), "所有ID应该是唯一的");
|
||||
}
|
||||
|
||||
assertEquals(count, ids.size(), "应该生成指定数量的唯一ID");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SnowflakeIdInfo应该正确返回信息")
|
||||
void testSnowflakeIdInfo() {
|
||||
long id = SnowflakeId.nextId();
|
||||
SnowflakeId.SnowflakeIdInfo info = SnowflakeId.parseId(id);
|
||||
|
||||
String infoString = info.toString();
|
||||
assertNotNull(infoString, "toString不应该返回null");
|
||||
assertTrue(infoString.contains("timestamp="), "toString应该包含timestamp");
|
||||
assertTrue(infoString.contains("workerId="), "toString应该包含workerId");
|
||||
assertTrue(infoString.contains("sequence="), "toString应该包含sequence");
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package io.destiny.common.utils;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("XSS防护工具类测试")
|
||||
class XssUtilsTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应该清理script标签")
|
||||
void testCleanScriptTag() {
|
||||
String input = "Hello <script>alert('XSS')</script> World";
|
||||
String cleaned = XssUtils.clean(input);
|
||||
|
||||
assertFalse(cleaned.contains("<script>"), "不应该包含script标签");
|
||||
assertFalse(cleaned.contains("</script>"), "不应该包含script结束标签");
|
||||
assertTrue(cleaned.contains("Hello"), "应该保留Hello");
|
||||
assertTrue(cleaned.contains("World"), "应该保留World");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该清理iframe标签")
|
||||
void testCleanIframeTag() {
|
||||
String input = "Content <iframe src='malicious.html'></iframe> End";
|
||||
String cleaned = XssUtils.clean(input);
|
||||
|
||||
assertFalse(cleaned.contains("<iframe>"), "不应该包含iframe标签");
|
||||
assertFalse(cleaned.contains("</iframe>"), "不应该包含iframe结束标签");
|
||||
assertTrue(cleaned.contains("Content"), "应该保留Content");
|
||||
assertTrue(cleaned.contains("End"), "应该保留End");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该清理javascript:协议")
|
||||
void testCleanJavascriptProtocol() {
|
||||
String input = "javascript:alert('XSS')";
|
||||
String cleaned = XssUtils.clean(input);
|
||||
|
||||
assertFalse(cleaned.contains("javascript:"), "不应该包含javascript:协议");
|
||||
assertTrue(cleaned.contains("alert"), "应该保留alert");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该清理on事件处理器")
|
||||
void testCleanOnEventHandlers() {
|
||||
String input = "<img src=x onerror=alert('XSS')>";
|
||||
String cleaned = XssUtils.clean(input);
|
||||
|
||||
assertFalse(cleaned.contains("onerror="), "不应该包含onerror事件");
|
||||
assertFalse(cleaned.contains("on"), "不应该包含on事件");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该清理表达式绑定")
|
||||
void testCleanExpressionBinding() {
|
||||
String input = "<img src=x onexpression=alert('XSS')>";
|
||||
String cleaned = XssUtils.clean(input);
|
||||
|
||||
assertFalse(cleaned.contains("onexpression="), "不应该包含onexpression绑定");
|
||||
assertFalse(cleaned.contains("expression("), "不应该包含expression绑定");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确处理空值")
|
||||
void testCleanNull() {
|
||||
String cleaned = XssUtils.clean(null);
|
||||
|
||||
assertNull(cleaned, "null应该返回null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确处理空字符串")
|
||||
void testCleanEmpty() {
|
||||
String cleaned = XssUtils.clean("");
|
||||
|
||||
assertEquals("", cleaned, "空字符串应该返回空字符串");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该清理恶意URL")
|
||||
void testCleanMaliciousUrl() {
|
||||
String url = "javascript:alert(document.cookie)";
|
||||
String cleaned = XssUtils.clean(url);
|
||||
|
||||
assertFalse(cleaned.contains("javascript:"), "不应该包含javascript:协议");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该保留合法URL")
|
||||
void testCleanValidUrl() {
|
||||
String url = "https://example.com/path?param=value";
|
||||
String cleaned = XssUtils.clean(url);
|
||||
|
||||
assertEquals(url, cleaned, "合法URL应该保持不变");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该转义HTML特殊字符")
|
||||
void testEscapeHtml() {
|
||||
String input = "<script>alert('XSS')</script>";
|
||||
String escaped = XssUtils.cleanForHtml(input);
|
||||
|
||||
assertFalse(escaped.contains("<script>"), "不应该包含未转义的<");
|
||||
assertTrue(escaped.contains("<"), "应该包含转义的<");
|
||||
assertTrue(escaped.contains(">"), "应该包含转义的>");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该转义引号")
|
||||
void testEscapeQuotes() {
|
||||
String input = "\"test\"&'data'";
|
||||
String escaped = XssUtils.cleanForJavaScript(input);
|
||||
|
||||
assertTrue(escaped.contains("\\\""), "应该包含转义的\"");
|
||||
assertTrue(escaped.contains("\\'"), "应该包含转义的'");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确处理空值转义")
|
||||
void testEscapeNull() {
|
||||
String escaped = XssUtils.cleanForHtml(null);
|
||||
|
||||
assertNull(escaped, "null应该返回null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应该正确处理空字符串转义")
|
||||
void testEscapeEmpty() {
|
||||
String escaped = XssUtils.cleanForHtml("");
|
||||
|
||||
assertEquals("", escaped, "空字符串应该返回空字符串");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user