关于java:告别混乱代码这份-Spring-Boot-后端接口规范来得太及时了

7次阅读

共计 18767 个字符,预计需要花费 47 分钟才能阅读完成。

一、前言

一个后端接口大抵分为四个局部组成:接口地址(url)、接口申请形式(get、post 等)、申请数据(request)、响应数据(response)。尽管说后端接口的编写并没有对立标准要求,而且如何构建这几个局部每个公司要求都不同,没有什么“肯定是最好的”规范,但其中最重要的关键点就是看是否标准。

二、环境阐明

因为解说的重点是后端接口,所以须要导入一个 spring-boot-starter-web 包,而 lombok 作用是简化类,前端显示则应用了 knife4j,具体应用在 Spring Boot 整合 knife4j 实现 Api 文档已写明。

举荐一个开源收费的 Spring Boot 实战我的项目:

https://github.com/javastacks/spring-boot-best-practice

另外从 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 校验是否合乎 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("123@qq.com");

    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("123@qq.com");
    // 留神哦,这里是间接返回的 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 最新版)

2. 劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4. 别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0