diff --git a/src/main/java/io/github/xxyopen/novel/core/annotation/ValidateSortOrder.java b/src/main/java/io/github/xxyopen/novel/core/annotation/ValidateSortOrder.java new file mode 100644 index 0000000..c3b23cd --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/annotation/ValidateSortOrder.java @@ -0,0 +1,18 @@ +package io.github.xxyopen.novel.core.annotation; + +import java.lang.annotation.*; + +/** + * 自定义注解,用于标记需要进行排序字段(sort)和排序方式(order)校验的方法参数。 + *

+ * 在接收到请求参数时,可通过 AOP 对标注了该注解的参数进行统一处理, 校验并防止 SQL 注入等安全问题。 + * + * @author xiongxiaoyang + * @date 2025/7/17 + */ +@Target(ElementType.PARAMETER) // 表示该注解只能用于方法参数上 +@Retention(RetentionPolicy.RUNTIME) // 注解在运行时依然可用,便于 AOP 或其他框架读取 +@Documented // 该注解将被包含在生成的 Javadoc 中 +public @interface ValidateSortOrder { + +} diff --git a/src/main/java/io/github/xxyopen/novel/core/aspect/SortOrderValidationAspect.java b/src/main/java/io/github/xxyopen/novel/core/aspect/SortOrderValidationAspect.java new file mode 100644 index 0000000..9383df3 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/aspect/SortOrderValidationAspect.java @@ -0,0 +1,130 @@ +package io.github.xxyopen.novel.core.aspect; + +import io.github.xxyopen.novel.core.annotation.ValidateSortOrder; +import io.github.xxyopen.novel.core.common.req.PageReqDto; +import io.github.xxyopen.novel.core.common.util.SortWhitelistUtil; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Map; + +/** + * 排序字段和排序方式的安全校验切面类 + *

+ * 该切面用于拦截所有 Mapper 方法的调用,对带有 @ValidateSortOrder 注解的参数进行统一处理, + * 校验并清理其中的排序字段(sort)和排序方式(order)参数,防止 SQL 注入攻击。 + *

+ * 支持处理以下类型的参数: + * - PageReqDto 类型对象 + * - Map 类型参数 + * - 任意带有 sort/order 字段的 POJO 对象 + * + * @author xiongxiaoyang + * @date 2025/7/17 + */ +@Aspect +@Component +@RequiredArgsConstructor +public class SortOrderValidationAspect { + + /** + * 拦截所有 Mapper 方法的调用,检查参数中是否包含 @ValidateSortOrder 注解。 + * 如果有,则对参数中的 sort 和 order 字段进行安全校验和清理。 + */ + @SneakyThrows + @Around("execution(* io.github.xxyopen.*.dao.mapper.*Mapper.*(..))") + public Object processSortOrderFields(ProceedingJoinPoint joinPoint) { + Object[] args = joinPoint.getArgs(); + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + + // 获取方法参数上的所有注解 + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + + // 遍历所有参数,检查是否有 @ValidateSortOrder 注解 + for (int i = 0; i < parameterAnnotations.length; i++) { + boolean hasAnnotation = Arrays.stream(parameterAnnotations[i]) + .anyMatch(a -> a.annotationType().equals(ValidateSortOrder.class)); + + if (hasAnnotation && args[i] != null) { + // 对带注解的参数进行处理 + handleAnnotatedParameter(args[i]); + } + } + + // 继续执行原方法 + return joinPoint.proceed(args); + } + + /** + * 根据参数类型,分别处理不同形式的 sort/order 字段。 + */ + @SneakyThrows + private void handleAnnotatedParameter(Object obj) { + if (obj instanceof PageReqDto dto) { + processPageReqDto(dto); + } else if (obj instanceof Map map) { + processMap(map); + } else { + processGenericObject(obj); + } + } + + /** + * 处理 PageReqDto 类型参数中的 sort 和 order 字段。 + */ + private void processPageReqDto(PageReqDto dto) { + if (dto.getSort() != null) { + dto.setSort(SortWhitelistUtil.sanitizeColumn(dto.getSort())); + } + if (dto.getOrder() != null) { + dto.setOrder(SortWhitelistUtil.sanitizeOrder(dto.getOrder())); + } + } + + /** + * 处理 Map 类型参数中的 sort 和 order 字段。 + */ + private void processMap(Map map) { + if (map.get("sort") instanceof String sortStr) { + map.put("sort", SortWhitelistUtil.sanitizeColumn(sortStr)); + } + if (map.get("order") instanceof String orderStr) { + map.put("order", SortWhitelistUtil.sanitizeOrder(orderStr)); + } + } + + /** + * 使用反射处理任意对象中的 sort 和 order 字段。 + * 支持任何带有这两个字段的 POJO。 + */ + @SneakyThrows + private void processGenericObject(Object obj) { + for (Field field : obj.getClass().getDeclaredFields()) { + switch (field.getName()) { + case "sort", "order" -> { + field.setAccessible(true); + Object value = field.get(obj); + if (value instanceof String strValue) { + String sanitized = "sort".equals(field.getName()) + ? SortWhitelistUtil.sanitizeColumn(strValue) + : SortWhitelistUtil.sanitizeOrder(strValue); + field.set(obj, sanitized); + } + } + default -> { + // 忽略其他字段 + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/xxyopen/novel/core/common/req/PageReqDto.java b/src/main/java/io/github/xxyopen/novel/core/common/req/PageReqDto.java index 31c92c9..bcf5340 100644 --- a/src/main/java/io/github/xxyopen/novel/core/common/req/PageReqDto.java +++ b/src/main/java/io/github/xxyopen/novel/core/common/req/PageReqDto.java @@ -30,4 +30,16 @@ public class PageReqDto { @Parameter(hidden = true) private boolean fetchAll = false; + /** + * 排序字段 + */ + @Parameter(description = "排序字段") + private String sort; + + /** + * 排序方式 + */ + @Parameter(description = "排序方式") + private String order; + } diff --git a/src/main/java/io/github/xxyopen/novel/core/common/util/SortWhitelistUtil.java b/src/main/java/io/github/xxyopen/novel/core/common/util/SortWhitelistUtil.java new file mode 100644 index 0000000..77a8ff5 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/common/util/SortWhitelistUtil.java @@ -0,0 +1,63 @@ +package io.github.xxyopen.novel.core.common.util; + +import lombok.experimental.UtilityClass; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * 排序字段和排序方式的安全校验工具类 + *

+ * 用于对请求参数中的排序字段(sort)和排序方式(order)进行白名单校验, + * 防止 SQL 注入攻击,确保传入的字段名和排序方式是系统允许的合法值。 + *

+ * 该工具类使用 Lombok 的 @UtilityClass 注解,确保: + * - 无法被实例化 + * - 所有方法为静态方法 + * + * @author xiongxiaoyang + * @date 2025/7/17 + */ +@UtilityClass +public class SortWhitelistUtil { + + /** + * 允许的排序字段白名单集合 + * 包含系统中允许作为排序依据的数据库字段名。 + */ + private final Set allowedColumns = new HashSet<>( + Arrays.asList( "last_chapter_update_time", "word_count", "visit_count")); + + /** + * 允许的排序方式白名单集合 + * 支持升序(asc)和降序(desc)两种排序方式。 + */ + private final Set allowedOrders = new HashSet<>(Arrays.asList("asc", "desc")); + + /** + * 校验并清理排序字段 + *

+ * 如果输入的字段在白名单中,则返回小写形式; + * 否则返回默认字段 "id"。 + * + * @param input 用户输入的排序字段 + * @return 安全的排序字段名 + */ + public String sanitizeColumn(String input) { + return allowedColumns.contains(input.toLowerCase()) ? input.toLowerCase() : "id"; + } + + /** + * 校验并清理排序方式 + *

+ * 如果输入的排序方式是 "asc" 或 "desc",则返回其小写形式; + * 否则返回默认排序方式 "asc"。 + * + * @param input 用户输入的排序方式 + * @return 安全的排序方式(asc 或 desc) + */ + public String sanitizeOrder(String input) { + return allowedOrders.contains(input.toLowerCase()) ? input.toLowerCase() : "asc"; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/xxyopen/novel/dao/mapper/BookInfoMapper.java b/src/main/java/io/github/xxyopen/novel/dao/mapper/BookInfoMapper.java index 2284441..b45d2ef 100644 --- a/src/main/java/io/github/xxyopen/novel/dao/mapper/BookInfoMapper.java +++ b/src/main/java/io/github/xxyopen/novel/dao/mapper/BookInfoMapper.java @@ -1,6 +1,7 @@ package io.github.xxyopen.novel.dao.mapper; import com.baomidou.mybatisplus.core.metadata.IPage; +import io.github.xxyopen.novel.core.annotation.ValidateSortOrder; import io.github.xxyopen.novel.dao.entity.BookInfo; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import io.github.xxyopen.novel.dto.req.BookSearchReqDto; @@ -32,6 +33,6 @@ public interface BookInfoMapper extends BaseMapper { * @param condition 搜索条件 * @return 返回结果 * */ - List searchBooks(IPage page, BookSearchReqDto condition); + List searchBooks(IPage page, @ValidateSortOrder BookSearchReqDto condition); } diff --git a/src/main/java/io/github/xxyopen/novel/dto/req/BookSearchReqDto.java b/src/main/java/io/github/xxyopen/novel/dto/req/BookSearchReqDto.java index 015427c..d927361 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/req/BookSearchReqDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/req/BookSearchReqDto.java @@ -70,9 +70,4 @@ public class BookSearchReqDto extends PageReqDto { @JsonFormat(pattern = "yyyy-MM-dd") private Date updateTimeMin; - /** - * 排序字段 - */ - @Parameter(description = "排序字段") - private String sort; } diff --git a/src/main/resources/mapper/BookInfoMapper.xml b/src/main/resources/mapper/BookInfoMapper.xml index b6dde7d..f629198 100644 --- a/src/main/resources/mapper/BookInfoMapper.xml +++ b/src/main/resources/mapper/BookInfoMapper.xml @@ -32,7 +32,7 @@ and last_chapter_update_time >= #{condition.updateTimeMin} - order by ${condition.sort} + order by ${condition.sort} desc