需求说明
为了保证系统的安全性,需要对所有的 查询列表
接口,添加分页参数,并对分页参数进行校验,
,保证参数的合法性。
比如, pageSize(每页显示条数)
,如果不做校验,一旦传递过来一个很大的数值,比如,十万亿,数据库可能会直接卡住,或者应用服务器的内存可能也被挤爆。
分页参数与校验逻辑
分页参数 | 校验逻辑 |
---|---|
currentPage | 当前页,应大于等于1 |
pageSize | 每页显示条数,取值范围为[1, 100] |
currentPage 和 pageSize,都应该是整数,如果传入的是小数、超出范围的数字、或者非数字,也应该直接报错;SpringMVC
已经自动支持这部分校验,不需要我们再去额外处理。
解决方案
使用 AOP(面向切面编程),在所有接口前,检查分页参数;如果不合法,直接返回接口调用失败,并将错误原因返回。
返回接口调用失败
,采用的方法是抛出业务异常,然后,由异常统一处理模块,将错误原因封装到返回结果中。
代码
参数校验切面
package com.example.core.advice;
import com.example.core.model.BusinessException;
import com.example.core.model.ErrorEnum;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 分页参数校验
*/
@Aspect
@Order(10)
@Component
public class PageValidator {
private static final String CURRENT_PAGE = "currentPage";
private static final String PAGE_SIZE = "pageSize";
// 拦截 com.example.web 包及其子包下的所有类的@RequestMapping注解修饰的方法
@Pointcut("execution(* com.example.web..*.*(..)) and @annotation(org.springframework.web.bind.annotation.RequestMapping)")
private void pointcut() {
}
// Before表示 advice() 将在目标方法执行前执行
@Before("pointcut()")
public void advice(JoinPoint joinPoint) {
// 获取请求信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return;
}
HttpServletRequest request = attributes.getRequest();
// 校验: [当前页]
validateCurrentPage(request);
// 校验: [每页显示条数]
validatePageSize(request);
}
/**
* 校验: [当前页]
*/
private void validateCurrentPage(HttpServletRequest request) {
String currentPageString = request.getParameter(CURRENT_PAGE);
if (currentPageString == null) {
return;
}
int currentPage = Integer.parseInt(currentPageString);
if (currentPage >= 1) {
return;
}
String userMessage = "当前页,应大于等于1";
String errorMessage = String.format("%s:【分页参数校验异常】:【错误字段:[%s],错误值:[%s],错误信息:[%s]】。",
ErrorEnum.A0425.getMessage(), CURRENT_PAGE, currentPage, userMessage);
throw new BusinessException(userMessage, ErrorEnum.A0425.name(), errorMessage);
}
/**
* 校验: [每页显示条数]
*/
private void validatePageSize(HttpServletRequest request) {
String pageSizeString = request.getParameter(PAGE_SIZE);
if (pageSizeString == null) {
return;
}
int pageSize = Integer.parseInt(pageSizeString);
if (pageSize >= 1 && pageSize <= 100) {
return;
}
String userMessage = "每页显示条数,取值范围为[1, 100]";
String errorMessage = String.format("%s:【分页参数校验异常】:【错误字段:[%s],错误值:[%s],错误信息:[%s]】。",
ErrorEnum.A0425.getMessage(), PAGE_SIZE, pageSize, userMessage);
throw new BusinessException(userMessage, ErrorEnum.A0425.name(), errorMessage);
}
}
分页参数实体
分页参数,一般使用封装好的 分页参数实体
接收(推荐),但是也可以直接写在接口参数列表中(不推荐)。但是,不论怎样接收,只要是分页参数,就应该被校验。
下面是 分页参数实体
的代码:
package com.example.core.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springdoc.api.annotations.ParameterObject;
@Data
@ParameterObject
@Schema(name = "分页参数Query")
public class PageQuery {
@Schema(description = "当前页", type = "Integer", defaultValue = "1", example = "1", minimum = "1")
private Integer currentPage = 1;
@Schema(description = "每页显示条数", type = "Integer", defaultValue = "10", example = "10", minimum = "1", maximum = "100")
private Integer pageSize = 10;
}
测试接口
package com.example.web.page.controller;
import com.example.core.log.annotation.ApiLog;
import com.example.core.model.PageQuery;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("page")
@Tag(name = "分页参数校验")
public class PageController {
@ApiLog
@GetMapping(path = "users/PageQuery")
@Operation(summary = "查询用户列表 PageQuery", description = "分页参数校验,使用PageQuery接收分页参数。")
public String listUsers(PageQuery pageQuery) {
return "查询用户列表 PageQuery:成功";
}
@GetMapping(path = "users/NoPageQuery")
@Operation(summary = "查询用户列表 NoPageQuery", description = "分页参数校验,分页参数直接写在了方法的参数列表中,未使用PageQuery接收分页参数。")
public String listUsersWithoutPageQuery(Integer currentPage, Integer pageSize) {
return "查询用户列表 NoPageQuery:成功";
}
@RequestMapping(path = "users/RequestMapping", method = RequestMethod.GET)
@Operation(summary = "查询用户列表 RequestMapping", description = "分页参数校验,使用 @RequestMapping 注解。")
public String listUsersByRequestMapping(PageQuery pageQuery) {
return "查询用户列表 RequestMapping:成功";
}
}
测试结果
@Order(10)
对切面进行排序,设定切面的触发顺序。数值越小的,优先级越高。
能够触发的切面可能有好多个,需要排个序,告诉项目先触发哪一个。
如果没有设定触发顺序,会按照切面在项目中的位置顺序来触发。比如下图中,默认情况,就是PageValidator 先触发,ApiLogAspect 后出发。
如果 PageValidator 抛出了异常,后面的 ApiLogAspect 就不会被触发了。