电竞比分网-中国电竞赛事及体育赛事平台

分享

告別混亂代碼:Spring Boot 后端接口規(guī)范

 昵稱10087950 2023-07-05 發(fā)布于江蘇
后臺回復“大禮包”有驚喜禮包!

日英文

What is adhere to? Is day, and one day, you tell yourself, insist again one day.

什么是堅持?就是一天,又一天,你告訴自己,再堅持一天。

每日掏心話

不要沉迷過去,不要害怕未來,過去。得失也好,成敗也罷,無論快樂,還是痛苦,都過去了,你只能回憶,而無法回去。

責編:樂樂 | 來自:魅Lemon

鏈接:blog.csdn.net/lemon_TT/article/details/108309900

編程技術(shù)圈(ID:study_tech)第 1940 期推文

往日回顧:Redis實現(xiàn)分頁+多條件模糊查詢組合方案

      正文     

大家好,我是小樂

一、前言


一個后端接口大致分為四個部分組成:接口地址(url)、接口請求方式(get、post等)、請求數(shù)據(jù)(request)、響應數(shù)據(jù)(response)。雖然說后端接口的編寫并沒有統(tǒng)一規(guī)范要求,而且如何構(gòu)建這幾個部分每個公司要求都不同,沒有什么“一定是最好的”標準,但其中最重要的關(guān)鍵點就是看是否規(guī)范。

Image

二、環(huán)境說明

因為講解的重點是后端接口,所以需要導入一個 spring-boot-starter-web 包,而 lombok 作用是簡化類,前端顯示則使用了 knife4j,具體使用在 Spring Boot 整合 knife4j 實現(xiàn) API 文檔已寫明。另外從 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>


歡迎關(guān)注公眾號:SpringForAll社區(qū)(spring4all.com),專注分享關(guān)于Spring的一切!回復“加群”還可加入Spring技術(shù)交流群!

三、參數(shù)校驗


3.1 介紹


一個接口一般對參數(shù)(請求數(shù)據(jù))都會進行安全校驗,參數(shù)校驗的重要性自然不必多說,那么如何對參數(shù)進行校驗就有講究了。一般來說有三種常見的校驗方式,我們使用了最簡潔的第三種方法:

  • 業(yè)務層校驗
  • Validator + BindResult 校驗
  • Validator + 自動拋出異常

業(yè)務層校驗無需多說,即手動在 Java 的 Service 層進行數(shù)據(jù)校驗判斷。不過這樣太繁瑣了,光校驗代碼就會有很多。

而使用 Validator+ BindingResult 已經(jīng)是非常方便實用的參數(shù)校驗方式了,在實際開發(fā)中也有很多項目就是這么做的,不過這樣還是不太方便,因為你每寫一個接口都要添加一個 BindingResult 參數(shù),然后再提取錯誤信息返回給前端(簡單看一下)。

@PostMapping('/addUser')public String addUser(@RequestBody @Validated User user, BindingResult bindingResult) {    // 如果有參數(shù)校驗失敗,會將錯誤信息封裝成對象組裝在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);}


3.2 Validator + 自動拋出異常(使用)

內(nèi)置參數(shù)校驗如下:

Image

首先,Validator可以非常方便的制定校驗規(guī)則,并自動幫你完成校驗。在入?yún)⒗镄枰r灥淖侄渭由献⒔?,每個注解對應不同的校驗規(guī)則,并可制定校驗失敗后的信息:

@Datapublic 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;}

校驗規(guī)則和錯誤提示信息配置完畢后,接下來只需要在接口僅需要在校驗的參數(shù)上加上 @Valid 注解(去掉 BindingResult 后會自動引發(fā)異常,異常發(fā)生了自然而然就不會執(zhí)行業(yè)務邏輯):

@RestController@RequestMapping('user')public class ValidationController {
@Autowired private ValidationService validationService;
@PostMapping('/addUser') public String addUser(@RequestBody @Validated User user) {
return validationService.addUser(user); }}

現(xiàn)在我們進行測試,打開 knife4j 文檔地址,當輸入的請求數(shù)據(jù)為空時,Validator 會將所有的報錯信息全部進行返回,所以需要與全局異常處理一起使用。

// 使用form data方式調(diào)用接口,校驗異常拋出 BindException// 使用 json 請求體調(diào)用接口,校驗異常拋出 MethodArgumentNotValidException// 單個參數(shù)校驗異常拋出ConstraintViolationException// 處理 json 請求體調(diào)用接口校驗失敗拋出的異常@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方式調(diào)用接口,校驗異常拋出 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);}

Image


3.3 分組校驗和遞歸校驗

分組校驗有三個步驟:

  • 定義一個分組類(或接口)
  • 在校驗注解上添加 groups 屬性指定分組
  • Controller 方法的 @Validated 注解添加分組類

public interface Update extends Default{}@Datapublic 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分組的參數(shù)字段;如果繼承了,會校驗了其他默認屬于Default.class分組的字段。

對于遞歸校驗(比如類中類),只要在相應屬性類上增加@Valid注解即可實現(xiàn)(對于集合同樣適用)

3.4 自定義校驗

Spring Validation 允許用戶自定義校驗,實現(xiàn)很簡單,分兩步:

  • 自定義校驗注解
  • 編寫校驗者類

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })@Retention(RUNTIME)@Documented@Constraint(validatedBy = {HaveNoBlankValidator.class})// 標明由哪個類執(zhí)行校驗邏輯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(' ');        // 校驗成功    }}

四、全局異常處理

?

參數(shù)校驗失敗會自動引發(fā)異常,我們當然不可能再去手動捕捉異常進行處理。但我們又不想手動捕捉這個異常,又要對這個異常進行處理,那正好使用 SpringBoot 全局異常處理來達到一勞永逸的效果!另外,搜索公眾號GitHub猿后臺回復“賺錢”,獲取一份驚喜禮包。

4.1 基本使用

首先,我們需要新建一個類,在這個類上加上 @ControllerAdvice 或 @RestControllerAdvice 注解,這個類就配置成全局處理類了。

這個根據(jù)你的 Controller 層用的是 @Controller 還是 @RestController 來決定。

然后在類中新建方法,在方法上加上 @ExceptionHandler 注解并指定你想處理的異常類型,接著在方法內(nèi)編寫對該異常的操作邏輯,就完成了對該異常的全局處理!我們現(xiàn)在就來演示一下對參數(shù)校驗失敗拋出的 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@ResponseBodypublic 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(); } /** * 系統(tǒng)異常 預期以外異常 */ @ExceptionHandler(Exception.class) @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public ResultVO<?> handleUnexpectedServer(Exception ex) { log.error('系統(tǒng)異常:', ex); // GlobalMsgEnum.ERROR是我自己定義的枚舉類 return new ResultVO<>(GlobalMsgEnum.ERROR); }
/** * 所以異常的攔截 */ @ExceptionHandler(Throwable.class) @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public ResultVO<?> exception(Throwable ex) { log.error('系統(tǒng)異常:', ex); return new ResultVO<>(GlobalMsgEnum.ERROR); }}
我們再次進行測試,這次返回的就是我們制定的錯誤提示信息!我們通過全局異常處理優(yōu)雅的實現(xiàn)了我們想要的功能!

以后我們再想寫接口參數(shù)校驗,就只需要在入?yún)⒌某蓡T變量上加上 Validator 校驗規(guī)則注解,然后在參數(shù)上加上 @Valid 注解即可完成校驗,校驗失敗會自動返回錯誤提示信息,無需任何其他代碼!

Image


4.2 自定義異常


在很多情況下,我們需要手動拋出異常,比如在業(yè)務層當有些條件并不符合業(yè)務邏輯,而使用自定義異常有諸多優(yōu)點:

  • 自定義異常可以攜帶更多的信息,不像這樣只能攜帶一個字符串。
  • 項目開發(fā)中經(jīng)常是很多人負責不同的模塊,使用自定義異??梢越y(tǒng)一了對外異常展示的方式。
  • 自定義異常語義更加清晰明了,一看就知道是項目中手動拋出的異常。

我們現(xiàn)在就來開始寫一個自定義異常:

package com.csdn.demo1.global;
import lombok.Getter;
@Getter //只要getter方法,無需setterpublic 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();}
這樣就對異常的處理就比較規(guī)范了。當然還可以添加對 Exception 的處理,這樣無論發(fā)生什么異常我們都能屏蔽掉然后響應數(shù)據(jù)給前端,不過建議最后項目上線時這樣做,能夠屏蔽掉錯誤信息暴露給前端,在開發(fā)中為了方便調(diào)試還是不要這樣做。

另外,當我們拋出自定義異常的時候全局異常處理只響應了異常中的錯誤信息 msg 給前端,并沒有將錯誤代碼 code 返回。這還需要配合數(shù)據(jù)統(tǒng)一響應。

如果在多模塊使用,全局異常等公共功能抽象成子模塊,則在需要的子模塊中需要將該模塊包掃描加入,@SpringBootApplication(scanBasePackages = {'com.xxx'})。

歡迎關(guān)注公眾號:SpringForAll社區(qū)(spring4all.com),專注分享關(guān)于Spring的一切!回復“加群”還可加入Spring技術(shù)交流群!

五、數(shù)據(jù)統(tǒng)一響應

統(tǒng)一數(shù)據(jù)響應是我們自己自定義一個響應體類,無論后臺是運行正常還是發(fā)生異常,響應給前端的數(shù)據(jù)格式是不變的!這里我包括了響應信息代碼 code 和響應信息說明 msg,首先可以設置一個枚舉規(guī)范響應體中的響應碼和響應信息。

@Getterpublic enum ResultCode {    SUCCESS(1000, '操作成功'),    FAILED(1001, '響應失敗'),    VALIDATE_FAILED(1002, '參數(shù)校驗失敗'),    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;
@Getterpublic class ResultVO<T> { /** * 狀態(tài)碼,比如1000代表響應成功 */ private int code; /** * 響應信息,用來說明響應情況 */ private String msg; /** * 響應的具體數(shù)據(jù) */ 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; }}

最后需要修改全局異常處理類的返回類型:

@RestControllerAdvicepublic 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 層進行接口信息數(shù)據(jù)的返回:

@GetMapping('/getUser')public ResultVO<User> getUser() { User user = new User(); user.setId(1L); user.setAccount('12345678'); user.setPassword('12345678'); user.setEmail('123@qq.com');
return new ResultVO<>(user);}
經(jīng)過測試,這樣響應碼和響應信息只能是枚舉規(guī)定的那幾個,就真正做到了響應數(shù)據(jù)格式、響應碼和響應信息規(guī)范化、統(tǒng)一化!

Image

還有一種全局返回類如下:

@Data@AllArgsConstructor@NoArgsConstructorpublic class Msg {    //狀態(tài)碼    private int code;    //提示信息    private String msg;    //用戶返回給瀏覽器的數(shù)據(jù)    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; }}

六、全局處理響應數(shù)據(jù)(可選擇)

接口返回統(tǒng)一響應體 + 異常也返回統(tǒng)一響應體,其實這樣已經(jīng)很好了,但還是有可以優(yōu)化的地方。要知道一個項目下來定義的接口搞個幾百個太正常不過了,要是每一個接口返回數(shù)據(jù)時都要用響應體來包裝一下好像有點麻煩,有沒有辦法省去這個包裝過程呢?

當然是有的,還是要用到全局處理。但是為了擴展性,就是允許繞過數(shù)據(jù)統(tǒng)一響應(這樣就可以提供多方使用),我們可以自定義注解,利用注解來選擇是否進行全局響應包裝。

首先,創(chuàng)建自定義注解,作用相當于全局處理類開關(guān):

@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD}) // 表明該注解只能放在方法上public @interface NotResponseBody {}

其次,創(chuàng)建一個類并加上注解使其成為全局處理類。然后繼承 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 { // 將數(shù)據(jù)包裝在ResultVO里后,再轉(zhuǎn)換為json字符串響應給前端 return objectMapper.writeValueAsString(new ResultVO<>(data)); } catch (JsonProcessingException e) { throw new APIException('返回String類型錯誤'); } } // 將原本的數(shù)據(jù)包裝在ResultVO里 return new ResultVO<>(data); }}
重寫的這兩個方法是用來在 controller 將數(shù)據(jù)進行返回前進行增強操作,supports 方法要返回為 true 才會執(zhí)行 beforeBodyWrite 方法,所以如果有些情況不需要進行增強操作可以在 supports 方法里進行判斷。

對返回數(shù)據(jù)進行真正的操作還是在 beforeBodyWrite 方法中,我們可以直接在該方法里包裝數(shù)據(jù),這樣就不需要每個接口都進行數(shù)據(jù)包裝了,省去了很多麻煩。此時 controller  只需這樣寫就行了:

@GetMapping('/getUser')//@NotResponseBody //是否繞過數(shù)據(jù)統(tǒng)一響應開關(guān)public User getUser() { User user = new User(); user.setId(1L); user.setAccount('12345678'); user.setPassword('12345678'); user.setEmail('123@qq.com'); // 注意哦,這里是直接返回的User類型,并沒有用ResultVO進行包裝 return user;}

七、接口版本控制

7.1 簡介

在 SpringBoot 項目中,如果要進行 restful 接口的版本控制一般有以下幾個方向:

  • 基于 path 的版本控制
  • 基于 header 的版本控制

在 Spring MVC下,url 映射到哪個 method 是由 RequestMappingHandlerMapping 來控制的,那么我們也是通過 RequestMappingHandlerMapping 來做版本控制的。


7.2 Path 控制實現(xiàn)

首先定義一個注解:

@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) { // 采用最后定義優(yōu)先原則,則方法上的定義覆蓋類上面的定義 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,如果從小開始那么排序反轉(zhuǎn)即可// if(Float.parseFloat(pathVersion)>=Float.parseFloat(version)){// return this;// }
} return null; }
@Override public int compareTo(ApiVersionCondition other, HttpServletRequest request) { return 0; // 優(yōu)先匹配最新的版本號,和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 來接管:

@Configurationpublic class WebMvcConfiguration implements WebMvcRegistrations {
@Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return new PathVersionHandlerMapping(); }}

最后,controller進行測試,默認是 v1.0,如果方法上有注解,以方法上的為準(該方法 vx.x 在路徑任意位置出現(xiàn)都可解析)。

@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'; }}

7.3 header 控制實現(xiàn)

總體原理與 Path 類似,修改 ApiVersionCondition 即可,之后訪問時在 header 帶上 X-VERSION 參數(shù)即可。

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) { // 采用最后定義優(yōu)先原則,則方法上的定義覆蓋類上面的定義 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接口安全

8.1 簡介

APP、前后端分離項目都采用 API 接口形式與服務器進行數(shù)據(jù)通信,傳輸?shù)臄?shù)據(jù)被偷窺、被抓包、被偽造時有發(fā)生,那么如何設計一套比較安全的 API 接口方案至關(guān)重要,一般的解決方案有以下幾點:

  • Token 授權(quán)認證,防止未授權(quán)用戶獲取數(shù)據(jù)
  • 時間戳超時機制
  • URL 簽名,防止請求參數(shù)被篡改
  • 防重放,防止接口被第二次請求,防采集
  • 采用 HTTPS 通信協(xié)議,防止數(shù)據(jù)明文傳輸


8.2 Token 授權(quán)認證


因為 HTTP 協(xié)議是無狀態(tài)的,Token 的設計方案是用戶在客戶端使用用戶名和密碼登錄后,服務器會給客戶端返回一個 Token,并將 Token 以鍵值對的形式存放在緩存(一般是 Redis)中,后續(xù)客戶端對需要授權(quán)模塊的所有操作都要帶上這個 Token,服務器端接收到請求后進行 Token 驗證,如果 Token 存在,說明是授權(quán)的請求。

Token 生成的設計要求:

  • 應用內(nèi)一定要唯一,否則會出現(xiàn)授權(quán)混亂,A 用戶看到了 B 用戶的數(shù)據(jù);
  • 每次生成的 Token 一定要不一樣,防止被記錄,授權(quán)永久有效;
  • 一般 Token 對應的是 Redis的key,value 存放的是這個用戶相關(guān)緩存信息,比如:用戶的 id;
  • 要設置 Token 的過期時間:過期后需要客戶端重新登錄,獲取新的 Token。如果 Token 有效期設置較短,會反復需要用戶登錄,體驗比較差,我們一般采用 Token 過期后,客戶端靜默登錄的方式,當客戶端收到 Token 過期后,客戶端用本地保存的用戶名和密碼在后臺靜默登錄來獲取新的 Token,還有一種是單獨出一個刷新 Token 的接口,但是一定要注意刷新機制和安全問題;

根據(jù)上面的設計方案要求,我們很容易得到 Token=md5(用戶ID + 登錄的時間戳 + 服務器端秘鑰)這種方式來獲得 Token。因為用戶 ID 是應用內(nèi)唯一的,登錄的時間戳保證每次登錄的時候都不一樣,服務器端秘鑰是配置在服務器端參與加密的字符串(即:鹽),目的是提高 Token 加密的破解難度,注意一定不要泄漏。

8.3 時間戳超時機制

客戶端每次請求接口都帶上當前時間的時間戳 timestamp,服務端接收到 timestamp 后跟當前時間進行比對,如果時間差大于一定時間(比如:1 分鐘),則認為該請求失效。時間戳超時機制是防御 DOS 攻擊的有效手段。例如

http://url/getInfo?id=1&timetamp=1661061696

8.4 URL 簽名


寫過支付寶或微信支付對接的同學肯定對URL簽名不陌生,我們只需要將原本發(fā)送給server端的明文參數(shù)做一下簽名,然后在server端用相同的算法再做一次簽名,對比兩次簽名就可以確保對應明文的參數(shù)有沒有被中間人篡改過。例如

http://url/getInfo?id=1&timetamp=1559396263&sign=e10adc3949ba59abbe56e057f20f883e

簽名算法過程

  • 首先,對通信的參數(shù)按 key 進行字母排序放入數(shù)組中(一般請求的接口地址也要參與排序和簽名,那么需要額外添加 url=http://url/getInfo 這個參數(shù));
  • 對排序完的數(shù)組鍵值對用&進行連接,形成用于加密的參數(shù)字符串;
  • 在加密的參數(shù)字符串前面或者后面加上私鑰,然后用 md5 進行加密,得到 sign,然后隨著請求接口一起傳給服務器。服務器端接收到請求后,用同樣的算法獲得服務器的 sign,對比客戶端的 sign 是否一致,如果一致請求有效。


8.5 防重放


客戶端第一次訪問時,將簽名 sign 存放到服務器的 Redis 中,超時時間設定為跟時間戳的超時時間一致,二者時間一致可以保證無論在 timestamp 限定時間內(nèi)還是外  URL 都只能訪問一次,如果被非法者截獲,使用同一個 URL 再次訪問,如果發(fā)現(xiàn)緩存服務器中已經(jīng)存在了本次簽名,則拒絕服務。

如果在緩存中的簽名失效的情況下,有人使用同一個 URL 再次訪問,則會被時間戳超時機制攔截,這就是為什么要求 sign 的超時時間要設定為跟時間戳的超時時間一致。拒絕重復調(diào)用機制確保 URL 被別人截獲了也無法使用(如抓取數(shù)據(jù))。

方案流程

  • 客戶端通過用戶名密碼登錄服務器并獲取 Token;
  • 客戶端生成時間戳 timestamp,并將 timestamp 作為其中一個參數(shù);
  • 客戶端將所有的參數(shù),包括 Token 和 timestamp 按照自己的簽名算法進行排序加密得到簽名 sign;
  • 將 token、timestamp 和 sign 作為請求時必須攜帶的參數(shù)加在每個請求的URL后邊,例:http://url/request?token=h40adc3949bafjhbbe56e027f20f583a&timetamp=1559396263&sign=e10adc3949ba59abbe56e057f20f883e
  • 服務端對 token、timestamp 和 sign 進行驗證,只有在 token 有效、timestamp 未超時、緩存服務器中不存在 sign 三種情況同時滿足,本次請求才有效。


8.6 采用 HTTPS 通信協(xié)議


安全套接字層超文本傳輸協(xié)議 HTTPS,為了數(shù)據(jù)傳輸?shù)陌踩?,HTTPS 在 HTTP 的基礎(chǔ)上加入了 SSL 協(xié)議,SSL 依靠證書來驗證服務器的身份,并為客戶端和服務器之間的通信加密。

HTTPS 也不是絕對安全的,比如中間人劫持攻擊,中間人可以獲取到客戶端與服務器之間所有的通信內(nèi)容。

九、總結(jié)

自此整個后端接口基本體系就構(gòu)建完畢了。

  • 通過 Validator + 自動拋出異常 來完成了方便的參數(shù)校驗
  • 通過 全局異常處理 + 自定義異常 完成了異常操作的規(guī)范
  • 通過數(shù)據(jù)統(tǒng)一響應完成了響應數(shù)據(jù)的規(guī)范
  • 多個方面組裝非常優(yōu)雅的完成了后端接口的協(xié)調(diào),讓開發(fā)人員有更多的經(jīng)歷注重業(yè)務邏輯代碼,輕松構(gòu)建后端接口
這里再說幾點:

  • controller 做好 try-catch 工作,及時捕獲異常,可以再次拋出到全局,統(tǒng)一格式返回前端
  • 做好日志系統(tǒng),關(guān)鍵位置一定要有日志
  • 做好全局統(tǒng)一返回類,整個項目規(guī)范好定義好
  • controller 入?yún)⒆侄慰梢猿橄蟪鲆粋€公共基類,在此基礎(chǔ)上進行繼承擴充
  • controlle r層做好入?yún)?shù)校驗
  • 接口安全驗證

為了跟上AI時代我干了一件事兒,我創(chuàng)建了一個知識星球社群:ChartGPT與副業(yè)。想帶著大家一起探索ChatGPT和新的AI時代。

很多小伙伴搞不定ChatGPT賬號,于是我們決定,凡是這三天之內(nèi)加入ChatPGT的小伙伴,我們直接送一個正常可用的永久ChatGPT獨立賬戶。

不光是增長速度最快,我們的星球品質(zhì)也絕對經(jīng)得起考驗,短短一個月時間,我們的課程團隊發(fā)布了8個專欄、18個副業(yè)項目

簡單說下這個星球能給大家提供什么:

1、不斷分享如何使用ChatGPT來完成各種任務,讓你更高效地使用ChatGPT,以及副業(yè)思考、變現(xiàn)思路、創(chuàng)業(yè)案例、落地案例分享。

2、分享ChatGPT的使用方法、最新資訊、商業(yè)價值。

3、探討未來關(guān)于ChatGPT的機遇,共同成長。

4、幫助大家解決ChatGPT遇到的問題。

5、提供一整年的售后服務,一起搞副業(yè)

星球福利:

1、加入星球4天后,就送ChatGPT獨立賬號。

2、邀請你加入ChatGPT會員交流群。

3、贈送一份完整的ChatGPT手冊和66個ChatGPT副業(yè)賺錢手冊。

其它福利還在籌劃中... 不過,我給你大家保證,加入星球后,收獲的價值會遠遠大于今天加入的門票費用 !

本星球第一期原價399,目前屬于試運營,早鳥價139,每超過50人漲價10元,星球馬上要來一波大的漲價,如果你還在猶豫,可能最后就要以更高價格加入了。。

早就是優(yōu)勢。建議大家盡早以便宜的價格加入!

歡迎有需要的同學試試,如果本文對您有幫助,也請幫忙點個 贊 + 在看 啦!??
你還有什么想要補充的嗎?

    本站是提供個人知識管理的網(wǎng)絡存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多