需求说明
为了保证系统的安全性,需要对所有的 查询列表
接口,添加分页参数,并对分页参数进行校验,
,保证参数的合法性。
比如, 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 就不会被触发了。