## 一、前言 一個後端介面大致分為四個部分組成:**介面地址(url)、介面請求方式(get、post等)、請求數據(request)、響應數據(response)**。雖然說後端介面的編寫並沒有統一規範要求,而且如何構建這幾個部分每個公司要求都不同,沒有什麼“一定是最好的”標準,但其中最重要的關 ...
一、前言
一個後端介面大致分為四個部分組成:介面地址(url)、介面請求方式(get、post等)、請求數據(request)、響應數據(response)。雖然說後端介面的編寫並沒有統一規範要求,而且如何構建這幾個部分每個公司要求都不同,沒有什麼“一定是最好的”標準,但其中最重要的關鍵點就是看是否規範。
二、環境說明
因為講解的重點是後端介面,所以需要導入一個spring-boot-starter-web
包,而lombok作用是簡化類,前端顯示則使用了knife4j,具體使用在Spring Boot整合knife4j實現Api文檔已寫明。
推薦一個開源免費的 Spring Boot 實戰項目:
另外從springboot-2.3開始,校驗包被獨立成了一個starter組件,所以需要引入如下依賴:
<dependency>
<!--新版框架沒有自動引入需要手動引入-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<!--在引用時請在maven中央倉庫搜索最新版本號-->
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
三、參數校驗
1、介紹
一個介面一般對參數(請求數據)都會進行安全校驗,參數校驗的重要性自然不必多說,那麼如何對參數進行校驗就有講究了。一般來說有三種常見的校驗方式,我們使用了最簡潔的第三種方法
- 業務層校驗
- Validator + BindResult校驗
- Validator + 自動拋出異常
業務層校驗無需多說,即手動在java的Service層進行數據校驗判斷。不過這樣太繁瑣了,光校驗代碼就會有很多
而使用Validator+ BindingResult
已經是非常方便實用的參數校驗方式了,在實際開發中也有很多項目就是這麼做的,不過這樣還是不太方便,因為你每寫一個介面都要添加一個BindingResult參數,然後再提取錯誤信息返回給前端(簡單看一下)。
@PostMapping("/addUser")
public String addUser(@RequestBody @Validated User user, BindingResult bindingResult) {
// 如果有參數校驗失敗,會將錯誤信息封裝成對象組裝在BindingResult里
List<ObjectError> allErrors = bindingResult.getAllErrors();
if(!allErrors.isEmpty()){
return allErrors.stream()
.map(o->o.getDefaultMessage())
.collect(Collectors.toList()).toString();
}
// 返回預設的錯誤信息
// return allErrors.get(0).getDefaultMessage();
return validationService.addUser(user);
}
2、Validator + 自動拋出異常(使用)
內置參數校驗如下:
註解 | 校驗功能 |
---|---|
@AssertFalse | 必須是false |
@AssertTrue | 必須是true |
@DecimalMax | 小於等於給定的值 |
@DecimalMin | 大於等於給定的值 |
@Digits | 可設定最大整數位數和最大小數位數 |
校驗是否符合Email格式 | |
@Future | 必須是將來的時間 |
@FutureOrPresent | 當前或將來時間 |
@Max | 最大值 |
@Min | 最小值 |
@Negative | 負數(不包括0) |
@NegativeOrZero | 負數或0 |
@NotBlank | 不為null並且包含至少一個非空白字元 |
@NotEmpty | 不為null並且不為空 |
@NotNull | 不為null |
@Null | 為null |
@Past | 必須是過去的時間 |
@PastOrPresent | 必須是過去的時間,包含現在 |
@PositiveOrZero | 正數或0 |
@Size | 校驗容器的元素個數 |
首先Validator可以非常方便的制定校驗規則,並自動幫你完成校驗。首先在入參里需要校驗的欄位加上註解,每個註解對應不同的校驗規則,並可制定校驗失敗後的信息:
@Data
public class User {
@NotNull(message = "用戶id不能為空")
private Long id;
@NotNull(message = "用戶賬號不能為空")
@Size(min = 6, max = 11, message = "賬號長度必須是6-11個字元")
private String account;
@NotNull(message = "用戶密碼不能為空")
@Size(min = 6, max = 11, message = "密碼長度必須是6-16個字元")
private String password;
@NotNull(message = "用戶郵箱不能為空")
@Email(message = "郵箱格式不正確")
private String email;
}
校驗規則和錯誤提示信息配置完畢後,接下來只需要在介面僅需要在校驗的參數上加上@Valid
註解(去掉BindingResult後會自動引發異常,異常發生了自然而然就不會執行業務邏輯):
@RestController
@RequestMapping("user")
public class ValidationController {
@Autowired
private ValidationService validationService;
@PostMapping("/addUser")
public String addUser(@RequestBody @Validated User user) {
return validationService.addUser(user);
}
}
現在我們進行測試,打開knife4j文檔地址,當輸入的請求數據為空時,Validator會將所有的報錯信息全部進行返回,所以需要與全局異常處理一起使用。
// 使用form data方式調用介面,校驗異常拋出 BindException
// 使用 json 請求體調用介面,校驗異常拋出 MethodArgumentNotValidException
// 單個參數校驗異常拋出ConstraintViolationException
// 處理 json 請求體調用介面校驗失敗拋出的異常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> MethodArgumentNotValidException(MethodArgumentNotValidException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
List<String> collect = fieldErrors.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
return new ResultVO(ResultCode.VALIDATE_FAILED, collect);
}
// 使用form data方式調用介面,校驗異常拋出 BindException
@ExceptionHandler(BindException.class)
public ResultVO<String> BindException(BindException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
List<String> collect = fieldErrors.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
return new ResultVO(ResultCode.VALIDATE_FAILED, collect);
}
3、分組校驗和遞歸校驗
分組校驗有三個步驟:
- 定義一個分組類(或介面)
- 在校驗註解上添加groups屬性指定分組
- Controller方法的
@Validated
註解添加分組類
public interface Update extends Default{
}
@Data
public class User {
@NotNull(message = "用戶id不能為空",groups = Update.class)
private Long id;
......
}
@PostMapping("update")
public String update(@Validated({Update.class}) User user) {
return "success";
}
如果Update不繼承Default,@Validated({Update.class})
就只會校驗屬於Update.class
分組的參數欄位;如果繼承了,會校驗了其他預設屬於Default.class
分組的欄位。
對於遞歸校驗(比如類中類),只要在相應屬性類上增加@Valid
註解即可實現(對於集合同樣適用)
4、自定義校驗
Spring Validation允許用戶自定義校驗,實現很簡單,分兩步:
- 自定義校驗註解
- 編寫校驗者類
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {HaveNoBlankValidator.class})// 標明由哪個類執行校驗邏輯
public @interface HaveNoBlank {
// 校驗出錯時預設返回的消息
String message() default "字元串中不能含有空格";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
/**
* 同一個元素上指定多個該註解時使用
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface List {
NotBlank[] value();
}
}
public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlank, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// null 不做檢驗
if (value == null) {
return true;
}
// 校驗失敗
return !value.contains(" ");
// 校驗成功
}
}
四、全局異常處理
參數校驗失敗會自動引發異常,我們當然不可能再去手動捕捉異常進行處理。但我們又不想手動捕捉這個異常,又要對這個異常進行處理,那正好使用SpringBoot全局異常處理來達到一勞永逸的效果!
1、基本使用
首先,我們需要新建一個類,在這個類上加上@ControllerAdvice
或@RestControllerAdvice
註解,這個類就配置成全局處理類了。
這個根據你的Controller層用的是
@Controller
還是@RestController
來決定。
然後在類中新建方法,在方法上加上@ExceptionHandler
註解並指定你想處理的異常類型,接著在方法內編寫對該異常的操作邏輯,就完成了對該異常的全局處理!我們現在就來演示一下對參數校驗失敗拋出的MethodArgumentNotValidException
全局處理:
package com.csdn.demo1.global;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@ResponseBody
public class ExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 從異常對象中拿到ObjectError對象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然後提取錯誤提示信息進行返回
return objectError.getDefaultMessage();
}
/**
* 系統異常 預期以外異常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public ResultVO<?> handleUnexpectedServer(Exception ex) {
log.error("系統異常:", ex);
// GlobalMsgEnum.ERROR是我自己定義的枚舉類
return new ResultVO<>(GlobalMsgEnum.ERROR);
}
/**
* 所以異常的攔截
*/
@ExceptionHandler(Throwable.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public ResultVO<?> exception(Throwable ex) {
log.error("系統異常:", ex);
return new ResultVO<>(GlobalMsgEnum.ERROR);
}
}
我們再次進行測試,這次返回的就是我們制定的錯誤提示信息!我們通過全局異常處理優雅的實現了我們想要的功能!
以後我們再想寫介面參數校驗,就只需要在入參的成員變數上加上Validator校驗規則註解,然後在參數上加上@Valid
註解即可完成校驗,校驗失敗會自動返回錯誤提示信息,無需任何其他代碼!
2、自定義異常
在很多情況下,我們需要手動拋出異常,比如在業務層當有些條件並不符合業務邏輯,而使用自定義異常有諸多優點:
- 自定義異常可以攜帶更多的信息,不像這樣只能攜帶一個字元串。
- 項目開發中經常是很多人負責不同的模塊,使用自定義異常可以統一了對外異常展示的方式。
- 自定義異常語義更加清晰明瞭,一看就知道是項目中手動拋出的異常。
我們現在就來開始寫一個自定義異常:
package com.csdn.demo1.global;
import lombok.Getter;
@Getter //只要getter方法,無需setter
public class APIException extends RuntimeException {
private int code;
private String msg;
public APIException() {
this(1001, "介面錯誤");
}
public APIException(String msg) {
this(1001, msg);
}
public APIException(int code, String msg) {
super(msg);
this.code = code;
this.msg = msg;
}
}
然後在剛纔的全局異常類中加入如下:
//自定義的全局異常
@ExceptionHandler(APIException.class)
public String APIExceptionHandler(APIException e) {
return e.getMsg();
}
這樣就對異常的處理就比較規範了,當然還可以添加對Exception的處理,這樣無論發生什麼異常我們都能屏蔽掉然後響應數據給前端,不過建議最後項目上線時這樣做,能夠屏蔽掉錯誤信息暴露給前端,在開發中為了方便調試還是不要這樣做。
另外,當我們拋出自定義異常的時候全局異常處理只響應了異常中的錯誤信息msg給前端,並沒有將錯誤代碼code返回。這還需要配合數據統一響應。
如果在多模塊使用,全局異常等公共功能抽象成子模塊,則在需要的子模塊中需要將該模塊包掃描加入,@SpringBootApplication(scanBasePackages = {"com.xxx"})
五、數據統一響應
統一數據響應是我們自己自定義一個響應體類,無論後臺是運行正常還是發生異常,響應給前端的數據格式是不變的!這裡我包括了響應信息代碼code和響應信息說明msg,首先可以設置一個枚舉規範響應體中的響應碼和響應信息。
@Getter
public enum ResultCode {
SUCCESS(1000, "操作成功"),
FAILED(1001, "響應失敗"),
VALIDATE_FAILED(1002, "參數校驗失敗"),
ERROR(5000, "未知錯誤");
private int code;
private String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
自定義響應體
package com.csdn.demo1.global;
import lombok.Getter;
@Getter
public class ResultVO<T> {
/**
* 狀態碼,比如1000代表響應成功
*/
private int code;
/**
* 響應信息,用來說明響應情況
*/
private String msg;
/**
* 響應的具體數據
*/
private T data;
public ResultVO(T data) {
this(ResultCode.SUCCESS, data);
}
public ResultVO(ResultCode resultCode, T data) {
this.code = resultCode.getCode();
this.msg = resultCode.getMsg();
this.data = data;
}
}
最後需要修改全局異常處理類的返回類型
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(APIException.class)
public ResultVO<String> APIExceptionHandler(APIException e) {
// 註意哦,這裡傳遞的響應碼枚舉
return new ResultVO<>(ResultCode.FAILED, e.getMsg());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 註意哦,這裡傳遞的響應碼枚舉
return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());
}
}
最後在controller層進行介面信息數據的返回
@GetMapping("/getUser")
public ResultVO<User> getUser() {
User user = new User();
user.setId(1L);
user.setAccount("12345678");
user.setPassword("12345678");
user.setEmail("[email protected]");
return new ResultVO<>(user);
}
經過測試,這樣響應碼和響應信息只能是枚舉規定的那幾個,就真正做到了響應數據格式、響應碼和響應信息規範化、統一化!
還有一種全局返回類如下
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Msg {
//狀態碼
private int code;
//提示信息
private String msg;
//用戶返回給瀏覽器的數據
private Map<String,Object> data = new HashMap<>();
public static Msg success() {
Msg result = new Msg();
result.setCode(200);
result.setMsg("請求成功!");
return result;
}
public static Msg fail() {
Msg result = new Msg();
result.setCode(400);
result.setMsg("請求失敗!");
return result;
}
public static Msg fail(String msg) {
Msg result = new Msg();
result.setCode(400);
result.setMsg(msg);
return result;
}
public Msg(ReturnResult returnResult){
code = returnResult.getCode();
msg = returnResult.getMsg();
}
public Msg add(String key,Object value) {
this.getData().put(key, value);
return this;
}
}
六、全局處理響應數據(可選擇)
介面返回統一響應體 + 異常也返回統一響應體,其實這樣已經很好了,但還是有可以優化的地方。要知道一個項目下來定義的介面搞個幾百個太正常不過了,要是每一個介面返回數據時都要用響應體來包裝一下好像有點麻煩,有沒有辦法省去這個包裝過程呢?
當然是有的,還是要用到全局處理。但是為了擴展性,就是允許繞過數據統一響應(這樣就可以提供多方使用),我們可以自定義註解,利用註解來選擇是否進行全局響應包裝
首先創建自定義註解,作用相當於全局處理類開關:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD}) // 表明該註解只能放在方法上
public @interface NotResponseBody {
}
其次創建一個類並加上註解使其成為全局處理類。然後繼承ResponseBodyAdvice
介面重寫其中的方法,即可對我們的controller進行增強操作,具體看代碼和註釋:
package com.csdn.demo1.global;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@RestControllerAdvice(basePackages = {"com.scdn.demo1.controller"}) // 註意哦,這裡要加上需要掃描的包
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
// 如果介面返回的類型本身就是ResultVO那就沒有必要進行額外的操作,返回false
// 如果方法上加了我們的自定義註解也沒有必要進行額外的操作
return !(returnType.getParameterType().equals(ResultVO.class) || returnType.hasMethodAnnotation(NotResponseBody.class));
}
@Override
public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
// String類型不能直接包裝,所以要進行些特別的處理
if (returnType.getGenericParameterType().equals(String.class)) {
ObjectMapper objectMapper = new ObjectMapper();
try {
// 將數據包裝在ResultVO里後,再轉換為json字元串響應給前端
return objectMapper.writeValueAsString(new ResultVO<>(data));
} catch (JsonProcessingException e) {
throw new APIException("返回String類型錯誤");
}
}
// 將原本的數據包裝在ResultVO里
return new ResultVO<>(data);
}
}
重寫的這兩個方法是用來在controller將數據進行返回前進行增強操作,supports方法要返回為true才會執行beforeBodyWrite
方法,所以如果有些情況不需要進行增強操作可以在supports方法里進行判斷。
對返回數據進行真正的操作還是在beforeBodyWrite
方法中,我們可以直接在該方法里包裝數據,這樣就不需要每個介面都進行數據包裝了,省去了很多麻煩。此時controller只需這樣寫就行了:
@GetMapping("/getUser")
//@NotResponseBody //是否繞過數據統一響應開關
public User getUser() {
User user = new User();
user.setId(1L);
user.setAccount("12345678");
user.setPassword("12345678");
user.setEmail("[email protected]");
// 註意哦,這裡是直接返回的User類型,並沒有用ResultVO進行包裝
return user;
}
七、介面版本控制
1、簡介
在spring boot項目中,如果要進行restful介面的版本控制一般有以下幾個方向:
- 基於path的版本控制
- 基於header的版本控制
在spring MVC下,url映射到哪個method是由RequestMappingHandlerMapping
來控制的,那麼我們也是通過 RequestMappingHandlerMapping
來做版本控制的。
2、Path控制實現
首先定義一個註解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
// 預設介面版本號1.0開始,這裡我只做了兩級,多級可在正則進行控制
String value() default "1.0";
}
ApiVersionCondition
用來控制當前request 指向哪個method
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
private static final Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+\\.\\d+)");
private final String version;
public ApiVersionCondition(String version) {
this.version = version;
}
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
// 採用最後定義優先原則,則方法上的定義覆蓋類上面的定義
return new ApiVersionCondition(other.getApiVersion());
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());
if (m.find()) {
String pathVersion = m.group(1);
// 這個方法是精確匹配
if (Objects.equals(pathVersion, version)) {
return this;
}
// 該方法是只要大於等於最低介面version即匹配成功,需要和compareTo()配合
// 舉例:定義有1.0/1.1介面,訪問1.2,則實際訪問的是1.1,如果從小開始那麼排序反轉即可
// if(Float.parseFloat(pathVersion)>=Float.parseFloat(version)){
// return this;
// }
}
return null;
}
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
return 0;
// 優先匹配最新的版本號,和getMatchingCondition註釋掉的代碼同步使用
// return other.getApiVersion().compareTo(this.version);
}
public String getApiVersion() {
return version;
}
}
PathVersionHandlerMapping
用於註入spring用來管理
public class PathVersionHandlerMapping extends RequestMappingHandlerMapping {
@Override
protected boolean isHandler(Class<?> beanType) {
return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
}
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType,ApiVersion.class);
return createCondition(apiVersion);
}
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method,ApiVersion.class);
return createCondition(apiVersion);
}
private RequestCondition<ApiVersionCondition>createCondition(ApiVersion apiVersion) {
return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());
}
}
WebMvcConfiguration
配置類讓spring來接管
@Configuration
public class WebMvcConfiguration implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new PathVersionHandlerMapping();
}
}
最後controller進行測試,預設是v1.0,如果方法上有註解,以方法上的為準(該方法vx.x在路徑任意位置出現都可解析)
@RestController
@ApiVersion
@RequestMapping(value = "/{version}/test")
public class TestController {
@GetMapping(value = "one")
public String query(){
return "test api default";
}
@GetMapping(value = "one")
@ApiVersion("1.1")
public String query2(){
return "test api v1.1";
}
@GetMapping(value = "one")
@ApiVersion("3.1")
public String query3(){
return "test api v3.1";
}
}
3、header控制實現
總體原理與Path類似,修改ApiVersionCondition
即可,之後訪問時在header帶上X-VERSION
參數即可
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
private static final String X_VERSION = "X-VERSION";
private final String version ;
public ApiVersionCondition(String version) {
this.version = version;
}
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
// 採用最後定義優先原則,則方法上的定義覆蓋類上面的定義
return new ApiVersionCondition(other.getApiVersion());
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
String headerVersion = httpServletRequest.getHeader(X_VERSION);
if(Objects.equals(version,headerVersion)){
return this;
}
return null;
}
@Override
public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {
return 0;
}
public String getApiVersion() {
return version;
}
}
八、API介面安全
1、簡介
APP、前後端分離項目都採用API介面形式與伺服器進行數據通信,傳輸的數據被偷窺、被抓包、被偽造時有發生,那麼如何設計一套比較安全的API介面方案至關重要,一般的解決方案有以下幾點:
- Token授權認證,防止未授權用戶獲取數據;
- 時間戳超時機制;
- URL簽名,防止請求參數被篡改;
- 防重放,防止介面被第二次請求,防採集;
- 採用HTTPS通信協議,防止數據明文傳輸;
2、Token授權認證
因為HTTP協議是無狀態的,Token的設計方案是用戶在客戶端使用用戶名和密碼登錄後,伺服器會給客戶端返回一個Token,並將Token以鍵值對的形式存放在緩存(一般是Redis)中,後續客戶端對需要授權模塊的所有操作都要帶上這個Token,伺服器端接收到請求後進行Token驗證,如果Token存在,說明是授權的請求。
Token生成的設計要求
- 應用內一定要唯一,否則會出現授權混亂,A用戶看到了B用戶的數據;
- 每次生成的Token一定要不一樣,防止被記錄,授權永久有效;
- 一般Token對應的是Redis的key,value存放的是這個用戶相關緩存信息,比如:用戶的id;
- 要設置Token的過期時間,過期後需要客戶端重新登錄,獲取新的Token,如果Token有效期設置較短,會反覆需要用戶登錄,體驗比較差,我們一般採用Token過期後,客戶端靜默登錄的方式,當客戶端收到Token過期後,客戶端用本地保存的用戶名和密碼在後臺靜默登錄來獲取新的Token,還有一種是單獨出一個刷新Token的介面,但是一定要註意刷新機制和安全問題;
根據上面的設計方案要求,我們很容易得到Token=md5(用戶ID+登錄的時間戳+伺服器端秘鑰)這種方式來獲得Token,因為用戶ID是應用內唯一的,登錄的時間戳保證每次登錄的時候都不一樣,伺服器端秘鑰是配置在伺服器端參與加密的字元串(即:鹽),目的是提高Token加密的破解難度,註意一定不要泄漏
3、時間戳超時機制
客戶端每次請求介面都帶上當前時間的時間戳timestamp,服務端接收到timestamp後跟當前時間進行比對,如果時間差大於一定時間(比如:1分鐘),則認為該請求失效。時間戳超時機制是防禦DOS攻擊的有效手段。 例如http://url/getInfo?id=1&timetamp=1661061696
4、URL簽名
寫過支付寶或微信支付對接的同學肯定對URL簽名不陌生,我們只需要將原本發送給server端的明文參數做一下簽名,然後在server端用相同的演算法再做一次簽名,對比兩次簽名就可以確保對應明文的參數有沒有被中間人篡改過。例如http://url/getInfo?id=1&timetamp=1559396263&sign=e10adc3949ba59abbe56e057f20f883e
簽名演算法過程
- 首先對通信的參數按key進行字母排序放入數組中(一般請求的介面地址也要參與排序和簽名,那麼需要額外添加
url=http://url/getInfo
這個參數) - 對排序完的數組鍵值對用&進行連接,形成用於加密的參數字元串
- 在加密的參數字元串前面或者後面加上私鑰,然後用md5進行加密,得到sign,然後隨著請求介面一起傳給伺服器。伺服器端接收到請求後,用同樣的演算法獲得伺服器的sign,對比客戶端的sign是否一致,如果一致請求有效
5、防重放
客戶端第一次訪問時,將簽名sign存放到伺服器的Redis中,超時時間設定為跟時間戳的超時時間一致,二者時間一致可以保證無論在timestamp限定時間內還是外 URL都只能訪問一次,如果被非法者截獲,使用同一個URL再次訪問,如果發現緩存伺服器中已經存在了本次簽名,則拒絕服務。
如果在緩存中的簽名失效的情況下,有人使用同一個URL再次訪問,則會被時間戳超時機制攔截,這就是為什麼要求sign的超時時間要設定為跟時間戳的超時時間一致。拒絕重覆調用機制確保URL被別人截獲了也無法使用(如抓取數據)
方案流程
- 客戶端通過用戶名密碼登錄伺服器並獲取Token;
- 客戶端生成時間戳timestamp,並將timestamp作為其中一個參數;
- 客戶端將所有的參數,包括Token和timestamp按照自己的簽名演算法進行排序加密得到簽名sign
- 將token、timestamp和sign作為請求時必須攜帶的參數加在每個請求的URL後邊,例:
http://url/request?token=h40adc3949bafjhbbe56e027f20f583a&timetamp=1559396263&sign=e10adc3949ba59abbe56e057f20f883e
- 服務端對token、timestamp和sign進行驗證,只有在token有效、timestamp未超時、緩存伺服器中不存在sign三種情況同時滿足,本次請求才有效;
6、採用HTTPS通信協議
安全套接字層超文本傳輸協議HTTPS,為了數據傳輸的安全,HTTPS在HTTP的基礎上加入了SSL協議,SSL依靠證書來驗證伺服器的身份,併為客戶端和伺服器之間的通信加密。
HTTPS也不是絕對安全的,比如中間人劫持攻擊,中間人可以獲取到客戶端與伺服器之間所有的通信內容
九、總結
自此整個後端介面基本體系就構建完畢了
- 通過Validator + 自動拋出異常來完成了方便的參數校驗
- 通過全局異常處理 + 自定義異常完成了異常操作的規範
- 通過數據統一響應完成了響應數據的規範
- 多個方面組裝非常優雅的完成了後端介面的協調,讓開發人員有更多的經歷註重業務邏輯代碼,輕鬆構建後端介面
這裡再說幾點
- controller做好try-catch工作,及時捕獲異常,可以再次拋出到全局,統一格式返回前端
- 做好日誌系統,關鍵位置一定要有日誌
- 做好全局統一返回類,整個項目規範好定義好
- controller入參欄位可以抽象出一個公共基類,在此基礎上進行繼承擴充
- controller層做好入參參數校驗
- 介面安全驗證
參考文章:
https://blog.csdn.net/xingfuzhijianxia/article/details/87623903
https://www.ithere.net/article/405
https://juejin.cn/post/6887019320666161165
http://learn.lianglianglee.com/
版權聲明:本文為CSDN博主「魅Lemon」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。原文鏈接:https://blog.csdn.net/lemon_TT/article/details/108309900
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!
覺得不錯,別忘了隨手點贊+轉發哦!