一、引言
接口参数校验是现代应用程序开发中不可或缺的一部分,尤其在构建API服务时显得尤为重要。数据交互的准确性与安全性直接关系到系统的整体稳定性和用户体验。无效或错误的输入参数可能导致数据库操作异常、业务逻辑混乱、资源泄露等问题,甚至可能被恶意利用进行攻击。例如,不正确的主键值可能导致无法找到对应的数据记录,或者非法的数据格式可能导致SQL注入等安全风险。
在众多需要严格校验的参数中,@Id
标识符具有特殊的意义。在ORM框架如Java Persistence API (JPA)和Hibernate中,@Id
注解通常用于标记实体类中的属性作为唯一标识符,即主键字段。对于RESTful API而言,@Id
常常出现在请求路径中,用来指定客户端希望访问或操作的具体资源实例。
二、为何需要校验@Id
为何需要对@Id
类型的参数进行特殊校验
资源定位:在API接口设计中,特别是RESTful风格的API,通常会使用资源的ID来定位和操作特定的资源实例。比如通过/users/{id}
这样的路径获取或更新用户信息时,{id}
就是@Id
类型的参数。对这类参数进行校验可以确保客户端提供的ID指向数据库中存在的有效资源。
安全性考量:未经验证的@Id
参数可能导致潜在的安全问题。例如,恶意用户可能会尝试SQL注入,将非法SQL注入到ID参数中。对@Id
进行严格的类型和格式校验,有助于抵御这种攻击行为。
性能优化:无效的@Id
会导致数据库查询失败或返回空结果,这不仅浪费了系统资源,还可能导致额外的错误处理开销。预先对@Id
进行有效的校验可以帮助减少不必要的数据库交互,提升整体性能。
综上所述,对API接口中的@Id
类型参数进行特殊校验,是从数据完整性和安全性等多个层面出发,以确保系统稳健运行的重要环节。
三、应用场景
当需要根据id
值查询、更新或删除数据库中的一条记录时,需要对传入的id参数进行长度和格式的有效性校验。比如,在执行如下的操作时:
- 通过id查询一条记录;
- 通过id修改一条记录;
- 通过id删除一条记录;
这些操作的前提都是能够准确地通过id定位到资源。如果接收到的id参数长度不合法(过长或过短),或者格式不符合预期(如应为整数但传入了非数字字符),那么这样的请求理论上不可能找到对应的数据库记录。因此,在服务端接口逻辑处理的最前端,即控制器层(Controller)中实施校验是非常关键的,可以有效拦截这类无效请求,避免不必要的数据库访问操作,同时也能及时返回错误信息,提高系统的稳定性和响应效率。
比如,当数据库系统中的主键是由雪花算法生成的,则此时ID格式应该为19位的数字。此时应该校验接口输入的ID参数,必须为19位的数字;否则直接返回错误信息给前端,不用再去查询数据库了。
四、实现方式与代码示例
通过Spring Validation模块对路径变量进行数据校验;在处理复杂或特定的校验需求时,通过创建自定义注解并关联一个自定义Validator进行校验逻辑实现。
本文将实现一个自定义的校验注解@Id
和对应的校验器Validator。
自定义校验注解:@Id
创建了一个名为@Id的自定义注解,并通过其description属性提供更具体的描述信息。
package com.example.core.validation.id;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 字符串必须是格式正确的ID。正确格式为:19位数字。
* <p>
* null 是无效的,不能够通过校验。
* <p>
* 支持的类型:字符串
*
* @author songguanxun
* @since 2024-1-20
*/
@Target({PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = IdValidator.class)
public @interface Id {
String message() default "ID,必须为19位数字";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* ID的详细描述,比如:用户ID。
* <p>
* 用来替换 {@link #message} 中的“ID”,使描述信息更具体。
*/
String description() default "";
}
自定义校验器:IdValidator
编写了对应的IdValidator
类作为校验器,实现了ConstraintValidator<Id, String>
接口以执行实际的校验逻辑。
package com.example.core.validation.id;
import com.example.core.validation.ResetMessageUtil;
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
/**
* ID,格式校验器。
* <p>
* 当前ID的格式:必须为19位数字。
*/
public class IdValidator implements ConstraintValidator<Id, String> {
private String description;
@Override
public void initialize(Id constraintAnnotation) {
description = constraintAnnotation.description();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (!StringUtils.hasText(value)) {
if (StringUtils.hasText(description)) {
// 根据description,优化提示信息。
String message = String.format("%s,不能为空", description);
ResetMessageUtil.reset(context, message);
} else {
ResetMessageUtil.reset(context, "ID,不能为空");
}
return false;
}
if (!isValid(value)) {
// 根据description,优化提示信息。
if (StringUtils.hasText(description)) {
String message = String.format("%s,必须为19位数字", description);
ResetMessageUtil.reset(context, message);
}
return false;
}
return true;
}
private final Pattern PATTERN = Pattern.compile("^\\d{19}$");
/**
* 是有效的ID
*/
private boolean isValid(String value) {
return PATTERN.matcher(value).matches();
}
}
五、校验实例
在Controller层,通过在 路径参数 上使用自定义的@Id
注解,并确保控制器类已被@Validated
注解启用验证功能,即可在接收到请求时自动调用自定义的验证逻辑。
单个ID校验
接口校验代码
package com.example.web.user.controller;
import com.example.core.validation.id.Id;
import com.example.web.model.vo.UserVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Validated
@RestController
@RequestMapping("users")
@Tag(name = "用户管理")
public class UserController {
@GetMapping("{id}")
@Operation(summary = "查询用户")
@Parameter(name = "id", description = "用户ID", example = "1234567890123456789")
public UserVO getUser(@PathVariable @Id(description = "用户ID") String id) {
UserVO vo = new UserVO();
vo.setId(id);
vo.setName("张三");
vo.setMobilePhone("18612345678");
vo.setEmail("zhangsan@example.com");
return vo;
}
}
校验效果
多个ID校验
接口校验代码
package com.example.web.user.controller;
import com.example.core.validation.id.Id;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@RestController
@RequestMapping
@Tag(name = "用户角色管理")
public class UserRoleController {
@DeleteMapping("users/{userId}/roles/{roleId}")
@Operation(summary = "删除用户的角色")
@Parameter(name = "userId", description = "用户ID", example = "1234567890123456789")
@Parameter(name = "roleId", description = "角色ID", example = "9876543210123456789")
public void deleteUserRole(@PathVariable @Id(description = "用户ID") String userId,
@PathVariable @Id(description = "角色ID") String roleId) {
log.info("测试,删除用户的角色");
}
}
校验效果
五、最佳实践与异常统一处理
在实际开发过程中,遇到路径变量校验失败时如何优雅地反馈给客户端的问题?
推荐的做法是:异常统一处理。
路径变量校验失败,会抛出
ConstraintViolationException
异常,通过异常统一处理来对此异常进行处理。具体的实现细节,请参考如下文章:《全局异常统一处理之约束违反异常:ConstraintViolationException》