確保數(shù)據(jù)安全!使用Spring Boot 實現(xiàn)強大的API參數(shù)驗證
我們在項目開發(fā)中,出于對數(shù)據(jù)完整性的考慮,基本上每個接口都需要參數(shù)校驗,參數(shù)校驗可以自己手動校驗,也可以用工具校驗,今天松哥和大家分享如何利用 Spring Boot 自帶的工具實現(xiàn)參數(shù)校驗。
一 前端 or 后端?
參數(shù)校驗應(yīng)該在前端完成還是后端完成?
正常來說,前后端都是需要校驗的,但是前后端校驗的目的各不相同。
一般來說,前端校驗可以滿足兩個需求:
- 用戶體驗:前端校驗可以即時反饋給用戶,減少等待服務(wù)器響應(yīng)的時間,提高用戶體驗。
- 減輕服務(wù)器負擔:通過前端校驗可以過濾掉一些明顯無效的請求,減少不必要的服務(wù)器負載。
真正要確保數(shù)據(jù)完整性,還得要靠后端,后端校驗可以起到如下作用:
- 安全性:由于前端代碼可以被繞過或修改。后端校驗是安全的必要保障,確保即使前端校驗被繞過,數(shù)據(jù)的安全性和完整性也能得到保證。
- 數(shù)據(jù)一致性:后端校驗可以確保所有通過的請求都符合業(yè)務(wù)邏輯和數(shù)據(jù)模型的要求,保持數(shù)據(jù)的一致性。
- 容錯性:后端校驗可以處理那些前端未能覆蓋到的異常情況,作為最后一道防線。
- 跨平臺一致性:后端校驗確保了無論用戶通過何種客戶端(Web、移動應(yīng)用、第三方 API 等)訪問服務(wù),數(shù)據(jù)校驗的標準都是一致的。
- 維護和可擴展性:后端校驗邏輯通常更容易維護和更新,因為它們集中在服務(wù)器端,而不是分散在多個客戶端。
- 日志和監(jiān)控:后端可以記錄校驗失敗的請求,這對于監(jiān)控系統(tǒng)安全和進行問題診斷非常有用。
因此,后端校驗才能真正確保數(shù)據(jù)的完整性,今天松哥也是要和大家聊一聊后端數(shù)據(jù)校驗。
二 參數(shù)校驗注解
2.1 參數(shù)校驗依據(jù)
在 Spring Boot 中,數(shù)據(jù)校驗是通過 JSR303/JSR380 規(guī)范的 Bean Validation 實現(xiàn)的。
這里涉及到兩個概念,松哥和大家簡單說下。
JSR303 是 Bean Validation 的 1.0 版本,正式名稱為《Bean Validation》。它提供了一套注解和 API 來定義 Java 對象(Bean)的驗證規(guī)則。這些注解可以直接用于 Bean 的屬性上,以聲明式的方式定義驗證邏輯。JSR303 定義了一組標準的驗證注解,如 @NotNull、@Size、@Email 等,用于校驗對象的屬性是否滿足特定的條件。
而 JSR380 則是 Bean Validation 的 2.0 版本,也稱為《Jakarta Bean Validation 2.0》。隨著 JavaEE 向 JakartaEE 的遷移,JSR380 成為了新的規(guī)范。JSR380 在 JSR303 的基礎(chǔ)上進行了擴展和改進,增加了新的注解、改進了 API,并提供了更好的集成方式。JSR380 的注解與 JSR303 兼容,但增加了一些新的注解,如 @Email 的 message 屬性支持國際化,以及 @PositiveOrZero、@NegativeOrZero 等。
松哥下面案例主要和小伙伴們分享最新的 JSR380 規(guī)范中的參數(shù)校驗注解。
2.2 代碼實踐
現(xiàn)在我們創(chuàng)建一個 Spring Boot 項目,使用當前最新版,并且引入?yún)?shù)校驗依賴,最終創(chuàng)建好的工程依賴如下:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
假設(shè)我現(xiàn)在有一個 UserDto 類,需要進行參數(shù)校驗,那么我可以按照如下方式定義 UserDto:
/**
* @author:江南一點雨
* @site:http://www.javaboy.org
* @微信公眾號:江南一點雨
* @github:https://github.com/lenve
* @gitee:https://gitee.com/lenve
*/
public class UserDto {
@NotNull(message = "用戶名不能為空")
private String username;
@NotBlank(message = "密碼不能為空")
private String password;
@NotEmpty(message = "郵箱不能為空")
private String email;
//省略 getter/setter
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
接下來在 Controller 的方法參數(shù)前使用 @Validated 注解來開啟校驗。
/**
* @author:江南一點雨
* @site:http://www.javaboy.org
* @微信公眾號:江南一點雨
* @github:https://github.com/lenve
* @gitee:https://gitee.com/lenve
*/
@RestController
public class UserController {
@GetMapping("/hello")
public String hello(@Validated UserDto userDto, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
// 處理校驗失敗情況
}
return "200";
}
}
當參數(shù)校驗失敗時,會拋出 MethodArgumentNotValidException 異常??梢栽谌之惓L幚砥髦胁东@該異常并進行統(tǒng)一處理。
/**
* @author:江南一點雨
* @site:http://www.javaboy.org
* @微信公眾號:江南一點雨
* @github:https://github.com/lenve
* @gitee:https://gitee.com/lenve
*/
@RestControllerAdvice
public class GlobalException {
@ExceptionHandler(MethodArgumentNotValidException.class)
public String handleValidationExceptions(MethodArgumentNotValidException ex) {
// 獲取校驗結(jié)果的錯誤信息
String message = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
return message;
}
}
如此就大功告成了~是不是非常 Easy?
2.3 異常提示優(yōu)化
上面參數(shù)校驗注解中的異常提示都是在 Java 代碼里邊硬編碼的,我們也可以提前定義好異常提示文本,然后在代碼里引用即可,這樣更加方便,也好維護。
在 Spring Boot 項目中,可以通過在 messages.properties 文件中定義異常提示文本,并在代碼中通過 @Message 注解引用這些文本來實現(xiàn)國際化和自定義錯誤消息。
具體步驟是這樣的:
- 創(chuàng)建 messages.properties 文件:在 src/main/resources 目錄下創(chuàng)建一個 messages.properties 文件(對于不同語言版本,可以創(chuàng)建如 messages_en.properties、messages_fr.properties 等文件)。
- 定義異常提示文本:在 messages.properties 文件中定義鍵值對,鍵用于在代碼中引用,值是實際的錯誤消息。
NotEmpty.username=用戶名不能為空
NotBlank.password=密碼不能為空
Email.email=郵箱格式不正確
- 在實體類或 DTO 上使用校驗注解。
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.NotEmpty;
public class UserDto {
@NotNull(message = "{NotEmpty.username}")
private String username;
@NotBlank(message = "{NotBlank.password}")
private String password;
@Email(message = "{Email.email}")
private String email;
// Getters and setters
}
- 配置國際化:如果你的應(yīng)用需要支持多語言,可以在 application.properties 或 application.yml 中配置消息源。
spring.messages.basename=messages
spring.messages.encoding=UTF-8
這樣,當校驗失敗時,Spring 將自動從 messages.properties 文件中查找對應(yīng)的錯誤消息,并將其返回給客戶端。這種方法不僅可以使錯誤消息更加靈活和可維護,還可以方便地實現(xiàn)國際化。
三 什么是分組校驗
為什么需要分組校驗?zāi)兀?/p>
假設(shè)我們有一個用戶實體 User,它包含用戶名、密碼和郵箱三個字段。在用戶注冊時,我們需要校驗用戶名和密碼非空,郵箱格式正確。但在用戶信息更新時,我們只需要校驗用戶名和郵箱,密碼可能不會被修改,因此不需要校驗。對于這種需求,我們可以使用分組校驗來實現(xiàn)這一需求。
松哥通過一個具體的案例來和小伙伴們演示下。
首先,我們定義兩個校驗分組,一個用于注冊,一個用于更新:
public interface RegisterGroup {}
public interface UpdateGroup {}
分組其實就是兩個空接口,用來做標記用。
然后,我們在 User 實體上應(yīng)用這些分組:
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class User {
@NotBlank(message = "用戶名不能為空", groups = {RegisterGroup.class, UpdateGroup.class})
private String username;
@NotBlank(message = "密碼不能為空", groups = RegisterGroup.class)
private String password;
@Email(message = "郵箱格式不正確", groups = {RegisterGroup.class, UpdateGroup.class})
private String email;
// Getters and setters
}
上面代碼中,username 和 email 即屬于注冊分組也屬于更新分組,而 password 則只屬于注冊分組。
接下來,在注冊接口中,我們使用 @Validated 注解并指定 RegisterGroup 分組:
/**
* @author:江南一點雨
* @site:http://www.javaboy.org
* @微信公眾號:江南一點雨
* @github:https://github.com/lenve
* @gitee:https://gitee.com/lenve
*/
@RestController
public class UserController {
@GetMapping("/hello")
public String hello(@Validated UserDto userDto, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
// 處理校驗失敗情況
}
return "200";
}
@PostMapping("/register")
public String register(@Validated(RegisterGroup.class) @RequestBody UserDto user) {
// 注冊邏輯
return "注冊成功";
}
@PostMapping("/update")
public String update(@Validated(UpdateGroup.class) @RequestBody UserDto user) {
// 更新邏輯
return "更新成功";
}
}
在這個例子中,當調(diào)用注冊接口時,User 對象會根據(jù) RegisterGroup 分組進行校驗,而調(diào)用更新接口時,則會根據(jù) UpdateGroup 分組進行校驗。這樣,我們就可以根據(jù)不同的業(yè)務(wù)需求來應(yīng)用不同的校驗規(guī)則了。
分組校驗這種方式提供了一種靈活的方式來應(yīng)對不同的校驗場景,使得我們的代碼更加清晰和易于維護。