Say goodbye to confusing code, this SpringBoot backend interface specification is too timely

Say goodbye to confusing code, this SpringBoot backend interface specification is too timely!

Article directory

  • I. Introduction

  • 2. Environmental description

  • 3. Parameter verification

    • 1 Introduction
    • 2. Validator + automatically throws an exception (use)
    • 3. Group verification and recursive verification
    • 4. Custom verification
  • 4. Global exception handling

    • 1. Basic use
    • 2. Custom exceptions
  • 5. Unified Data Response

  • 6. Global processing of response data (optional)

  • Seven, interface version control

    • 1 Introduction
    • 2. Path control implementation
    • 3. Header control implementation
  • Eight, API interface security

    • 1 Introduction
    • 2. Token authorization authentication
    • 3. Timestamp timeout mechanism
    • 4. URL signature
    • 5. Anti-replay
    • 6. Adopt HTTPS communication protocol
  • Nine. Summary


I. Introduction

A backend interface is roughly divided into four parts: interface address (url), interface request method (get, post, etc.), request data (request), and response data (response) . Although there is no unified specification requirement for the writing of the back-end interface, and each company has different requirements on how to build these parts, there is no "must be the best" standard, but the most important key point is to see whether it is standardized.

2. Environmental description

Because the focus of the explanation is the back-end interface, a spring-boot-starter-web package needs to be imported, and lombok is used to simplify the class, and knife4j is used for the front-end display. The specific use of Spring Boot to integrate knife4j to achieve the Api document has been written. In addition, starting from springboot-2.3, the verification package is independent as a starter component, so the following dependencies need to be introduced:

<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>

3. Parameter verification

1 Introduction

An interface generally performs security verification on parameters (request data). Naturally, the importance of parameter verification is needless to say, so how to verify parameters is more important. Generally speaking, there are three common verification methods, and we use the most concise third method

  • business layer validation
  • Validator + BindResult check
  • Validator + automatically throws exceptions

There is no need to say more about the verification of the business layer, that is, manually perform data verification and judgment in the Service layer of java. But this is too cumbersome, there will be a lot of light check codes

Using Validator+BindingResult is already a very convenient and practical method of parameter verification. In actual development, many projects do this, but it is still inconvenient, because you need to add a BindingResult parameter every time you write an interface, and then Extract the error information and return it to the front end (take a look at it briefly).

@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 + automatically throws an exception (use)

The built-in parameter verification is as follows:

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-EvTlHVzi-1683446749155)(https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0 /b3cd056a05ae4d5cae6c69df9c9bac33~tplv-obj.jpg?traceid=20230507160105D9A4C5A5264665B4D0CE&x-expires=2147483647&x-signature=F4lbUZvgcw0zuZjUEJasprU %2FuNk%3D)]

First of all, Validator can easily formulate verification rules and automatically complete the verification for you. First, add annotations to the fields that need to be verified in the input parameters. Each annotation corresponds to a different verification rule, and the information after the verification fails can be formulated:

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

After the verification rules and error message are configured, you only need to add the @Valid annotation on the parameters that need to be verified on the interface (after removing the BindingResult, an exception will be automatically thrown, and the business logic will not be executed naturally if an exception occurs) :

@RestController
@RequestMapping("user")
public class ValidationController {

    @Autowired
    private ValidationService validationService;

    @PostMapping("/addUser")
    public String addUser(@RequestBody @Validated User user) {

        return validationService.addUser(user);
    }
}

Now let's test and open the knife4j document address. When the input request data is empty, Validator will return all error messages, so it needs to be used together with global exception handling.

// 使用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);
}

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-mFyIV4xJ-1683446749156)(https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0 /7abd9a736c524aafa3966bd725dbadf6~tplv-obj.jpg?traceid=20230507160105D9A4C5A5264665B4D0CE&x-expires=2147483647&x-signature=Xkmme49mPx6M%2BmsegEB3%2BIuA 4uY%3D)]

3. Group verification and recursive verification

Group verification has three steps:

  • Define a grouping class (or interface)
  • Add the groups attribute to specify the grouping on the validation annotation
  • The @Validated annotation of the Controller method adds a grouping class
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";
}

If Update does not inherit Default, @Validated({Update.class}) will only verify the parameter fields belonging to the Update.class group; if it inherits, it will verify other fields that belong to the Default.class group by default.

For recursive verification (such as classes in classes), it can be realized by adding @Valid annotations on the corresponding attribute classes (the same applies to collections)

4. Custom verification

Spring Validation allows users to customize validation, and the implementation is very simple, in two steps:

  • Custom validation annotations
  • Write a validator class
@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(" ");
        // 校验成功
    }
}

4. Global exception handling

If the parameter verification fails, an exception will be automatically raised. Of course, it is impossible for us to manually catch the exception and handle it. But we don't want to manually catch this exception, and we have to deal with this exception, so we just use SpringBoot global exception handling to achieve a once-and-for-all effect!

1. Basic use

First of all, we need to create a new class, add @ControllerAdvice or @RestControllerAdvice annotation to this class, and this class will be configured as a global processing class.

This depends on whether your Controller layer uses @Controller or @RestController.

Then create a new method in the class, add the @ExceptionHandler annotation to the method and specify the type of exception you want to handle, and then write the operation logic for the exception in the method to complete the global processing of the exception! Let's now demonstrate the global handling of MethodArgumentNotValidException thrown for parameter validation failures:

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);
    }
}

Let's test again, this time what we return is the error message we made! We have elegantly realized the functions we want through global exception handling!

In the future, if we want to write interface parameter verification, we only need to add the Validator verification rule annotation to the member variable of the input parameter, and then add the @Valid annotation to the parameter to complete the verification. If the verification fails, an error will be returned automatically Prompt message, no additional code required!

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-MvJc3lnS-1683446749156)(https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0 /67d0a158c3de42b0a1015ccdc5b5cb5d~tplv-obj.jpg?traceid=20230507160105D9A4C5A5264665B4D0CE&x-expires=2147483647&x-signature=R2fbDxrifCosR%2FghpybNyQ DHESo%3D)]

2. Custom exceptions

In many cases, we need to manually throw exceptions. For example, when some conditions do not conform to business logic in the business layer, there are many advantages to using custom exceptions:

  • Custom exceptions can carry more information, unlike this one that can only carry a string.
  • In project development, many people are often responsible for different modules. Using custom exceptions can unify the way of displaying external exceptions.
  • The semantics of custom exceptions are clearer, and you can tell at a glance that they are exceptions thrown manually in the project.

Let's start writing a custom exception now:

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

Then add the following to the global exception class just now:

//自定义的全局异常
  @ExceptionHandler(APIException.class)
  public String APIExceptionHandler(APIException e) {
      return e.getMsg();
  }

In this way, the handling of exceptions is more standardized. Of course, the handling of Exceptions can also be added, so that no matter what exceptions occur, we can shield them and respond to the data to the front end. However, it is recommended to do this when the project goes online in the end, so that error messages can be shielded Exposed to the front end, it is better not to do this in order to facilitate debugging during development.

In addition, when we throw a custom exception, the global exception handling only responds to the error message msg in the exception to the front end, and does not return the error code code. This also requires a unified response with the data.

If it is used in multiple modules, public functions such as global exceptions are abstracted into sub-modules, the module package needs to be scanned and added to the required sub-modules, @SpringBootApplication(scanBasePackages = {“com.xxx”})

5. Unified Data Response

Unified data response is a response body class that we customize ourselves. No matter whether the background is running normally or an exception occurs, the data format of the response to the front end remains unchanged! Here I include the response information code code and response information description msg. First, you can set an enumeration specification response code and response information in the response body.

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

custom response body

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

Finally, you need to modify the return type of the global exception handling class

@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());
    }
}

Finally, the interface information data is returned at the controller layer

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

After testing, the response code and response information can only be those specified in the enumeration, and the response data format, response code and response information are truly standardized and unified!

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-up0CblzN-1683446749157)(https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0 /3036162d7fd4455ab1c7d57124e9c7c9~tplv-obj.jpg?traceid=20230507160105D9A4C5A5264665B4D0CE&x-expires=2147483647&x-signature=MUUG9aP67pcMsQzGFYiyc Ya5rlw%3D)]

There is also a global return class as follows

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

6. Global processing of response data (optional)

The interface returns a unified response body + exceptions also return a unified response body. In fact, this is already very good, but there are still places that can be optimized. You must know that it is normal to have hundreds of interfaces defined in a project. It seems a bit troublesome if each interface returns data with a response body. Is there any way to save this packaging process?

Of course there are, and global processing is still needed. But for scalability, it is to allow to bypass the unified response of data (so that it can be used by multiple parties), we can customize annotations, and use annotations to choose whether to perform global response packaging

First create a custom annotation, which is equivalent to a global processing class switch:

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

Next, create a class and add annotations to make it a global processing class. Then inherit the ResponseBodyAdvice interface and rewrite the methods in it to enhance our controller. See the code and comments for details:

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);
    }
}

These two rewritten methods are used to perform enhanced operations before the controller returns the data. The beforeBodyWrite method will only be executed if the supports method returns true, so if there are some cases where no enhanced operations are required, you can judge in the supports method.

The real operation on the returned data is still in the beforeBodyWrite method. We can directly wrap the data in this method, so that there is no need to wrap data for each interface, saving a lot of trouble. At this point, the controller only needs to write like this:

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

Seven, interface version control

1 Introduction

In the spring boot project, if you want to perform version control of the restful interface, there are generally the following directions:

  • Path-based version control
  • Header-based version control

Under spring MVC, which method the URL is mapped to is controlled by RequestMappingHandlerMapping, so we also do version control through RequestMappingHandlerMapping.

2. Path control implementation

First define an annotation

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    // 默认接口版本号1.0开始,这里我只做了两级,多级可在正则进行控制
    String value() default "1.0";
}

ApiVersionCondition is used to control which method the current request points to

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 is used to inject spring for management

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());
    }
}

The WebMvcConfiguration configuration class lets spring take over

@Configuration
public class WebMvcConfiguration implements WebMvcRegistrations {

    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new PathVersionHandlerMapping();
    }
}

Finally, the controller is tested, and the default is v1.0. If there is an annotation on the method, the method shall prevail (the method vx.x can be parsed anywhere in the path)

@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 control implementation

The overall principle is similar to Path, just modify the ApiVersionCondition, and then add the X-VERSION parameter to the header when accessing

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

}

Eight, API interface security

1 Introduction

APP, front-end and back-end separation projects all use API interface form to communicate with the server, and the transmitted data is peeped, captured, and forged from time to time, so how to design a relatively safe API interface solution is very important. The solution has the following points:

  • Token authorization authentication to prevent unauthorized users from obtaining data;
  • Timestamp timeout mechanism;
  • URL signature to prevent tampering of request parameters;
  • Anti-replay, prevent the interface from being requested for the second time, and prevent collection;
  • Adopt HTTPS communication protocol to prevent data transmission in plain text;

2. Token authorization authentication

Because the HTTP protocol is stateless, the Token design scheme is that after the user logs in with the user name and password on the client, the server will return a Token to the client, and store the Token in the cache (usually Redis) in the form of key-value pairs. , the subsequent client must bring this Token for all operations that require the authorization module, and the server will perform Token verification after receiving the request. If the Token exists, it means that it is an authorized request.

Design requirements for token generation

  • It must be unique within the app, otherwise there will be authorization confusion, and user A sees the data of user B;
  • The Token generated each time must be different to prevent it from being recorded, and the authorization is permanently valid;
  • Generally, the Token corresponds to the key of Redis, and the value stores the relevant cache information of the user, such as: the user's id;
  • To set the expiration time of the Token, after the expiration, the client needs to log in again to obtain a new Token. If the validity period of the Token is set to be short, the user will be required to log in repeatedly, and the experience is relatively poor. We generally use the way that the client logs in silently after the Token expires. , when the client receives the expired Token, the client uses the locally saved user name and password to log in silently in the background to obtain a new Token. There is also a separate interface for refreshing the Token, but you must pay attention to the refresh mechanism and security issues ;

According to the requirements of the above design scheme, we can easily get Token=md5 (user ID + login timestamp + server-side secret key) to obtain Token, because the user ID is unique in the application, and the login timestamp guarantees every time It is different when logging in. The server-side secret key is a string configured on the server side to participate in encryption (ie: salt). The purpose is to increase the difficulty of cracking the Token encryption. Be careful not to leak it

3. Timestamp timeout mechanism

Each time the client requests the interface, it carries the timestamp of the current time. The server receives the timestamp and compares it with the current time. If the time difference is greater than a certain time (for example: 1 minute), the request is considered invalid. The timestamp timeout mechanism is an effective means to defend against DOS attacks. For example http://url/getInfo?id=1&timetamp=1661061696

4. URL signature

Students who have written about Alipay or WeChat payment docking must be familiar with URL signatures. We only need to sign the plaintext parameters originally sent to the server, and then use the same algorithm to sign again on the server, and compare the two signatures. Make sure that the parameters corresponding to the plaintext have not been tampered with by an intermediary. For example http://url/getInfo?id=1&timetamp=1559396263&sign=e10adc3949ba59abbe56e057f20f883e

Signature algorithm process

  • First, sort the communication parameters alphabetically by key and put them into the array (generally, the interface address of the request also participates in sorting and signing, so you need to add the parameter url=http://url/getInfo)
  • Connect the sorted array key-value pairs with & to form a parameter string for encryption
  • Add the private key before or after the encrypted parameter string, then encrypt it with md5, get the sign, and then send it to the server along with the request interface. After receiving the request, the server uses the same algorithm to obtain the server's sign, and compares whether the client's sign is consistent. If the same request is valid

5. Anti-replay

When the client visits for the first time, the signature sign is stored in the server's Redis, and the timeout time is set to be consistent with the timeout time of the timestamp. The time consistency between the two can ensure that only the external URL can only be accessed within the time limit of the timestamp Once, if it is intercepted by an illegal person, use the same URL to access again, and if it is found that this signature already exists in the cache server, the service will be refused.

If someone uses the same URL to visit again when the signature in the cache is invalid, it will be intercepted by the timestamp timeout mechanism, which is why the timeout time of sign is required to be set to be consistent with the timeout time of the timestamp. Refuse to repeat the call mechanism to ensure that the URL cannot be used even if it is intercepted by others (such as grabbing data)

Program flow

  • The client logs in to the server with a username and password and obtains a Token;
  • The client generates a timestamp timestamp and takes timestamp as one of the parameters;
  • The client sorts and encrypts all parameters, including Token and timestamp, according to its own signature algorithm to obtain the signature sign
  • Add token, timestamp and sign as parameters that must be carried in the request to the URL of each request, for example: http://url/request?token=h40adc3949bafjhbbe56e027f20f583a&timetamp=1559396263&sign=e10adc3949ba59abbe56e057f20f883e
  • The server verifies the token, timestamp and sign. Only when the token is valid, the timestamp has not timed out, and the sign does not exist in the cache server, the request is valid;

6. Adopt HTTPS communication protocol

Secure Socket Layer Hypertext Transfer Protocol HTTPS, for the security of data transmission, HTTPS adds the SSL protocol on the basis of HTTP, SSL relies on certificates to verify the identity of the server, and encrypts the communication between the client and the server.

HTTPS is not absolutely secure, such as man-in-the-middle hijacking attacks, where the man-in-the-middle can obtain all communication content between the client and the server

Nine. Summary

Since then, the basic system of the entire back-end interface has been constructed.

  • Convenient parameter verification is completed through Validator + automatic exception throwing
  • The specification of exception operation is completed through global exception handling + custom exception
  • The specification of the response data is completed through the data unified response
  • The assembly of multiple aspects elegantly completes the coordination of the back-end interface, allowing developers to have more experience Focus on business logic codes and easily build back-end interfaces

A few more words here

  • The controller does a good job of try-catch, catches the exception in time, can throw it to the whole world again, and returns to the front end in a unified format
  • Do a good job in the log system, and there must be logs in key positions
  • Do a good job in the global unified return class, and the whole project is well defined
  • The controller input field can abstract a common base class, and inherit and expand on this basis
  • The controller layer does a good job of parameter verification
    estamp as one of the parameters;
  • The client sorts and encrypts all parameters, including Token and timestamp, according to its own signature algorithm to obtain the signature sign
  • Add token, timestamp and sign as parameters that must be carried in the request to the URL of each request, for example: http://url/request?token=h40adc3949bafjhbbe56e027f20f583a&timetamp=1559396263&sign=e10adc3949ba59abbe56e057f20f883e
  • The server verifies the token, timestamp and sign. Only when the token is valid, the timestamp has not timed out, and the sign does not exist in the cache server, the request is valid;

6. Adopt HTTPS communication protocol

Secure Socket Layer Hypertext Transfer Protocol HTTPS, for the security of data transmission, HTTPS adds the SSL protocol on the basis of HTTP, SSL relies on certificates to verify the identity of the server, and encrypts the communication between the client and the server.

HTTPS is not absolutely secure, such as man-in-the-middle hijacking attacks, where the man-in-the-middle can obtain all communication content between the client and the server

Nine. Summary

Since then, the basic system of the entire back-end interface has been constructed.

  • Convenient parameter verification is completed through Validator + automatic exception throwing
  • The specification of exception operation is completed through global exception handling + custom exception
  • The specification of the response data is completed through the data unified response
  • The assembly of multiple aspects elegantly completes the coordination of the back-end interface, allowing developers to have more experience Focus on business logic codes and easily build back-end interfaces

A few more words here

  • The controller does a good job of try-catch, catches the exception in time, can throw it to the whole world again, and returns to the front end in a unified format
  • Do a good job in the log system, and there must be logs in key positions
  • Do a good job in the global unified return class, and the whole project is well defined
  • The controller input field can abstract a common base class, and inherit and expand on this basis
  • The controller layer does a good job of parameter verification
  • Interface Security Verification

Guess you like

Origin blog.csdn.net/u014001523/article/details/130543979