一、前言
一个后端接口大抵分为四个局部组成:接口地址(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格局 | |
@Future | 必须是未来的工夫 |
@FutureOrPresent | 以后或未来工夫 |
@Max | 最大值 |
@Min | 最小值 |
@Negative | 正数(不包含0) |
@NegativeOrZero | 正数或0 |
@NotBlank | 不为null并且蕴含至多一个非空白字符 |
@NotEmpty | 不为null并且不为空 |
@NotNull | 不为null |
@Null | 为null |
@Past | 必须是过来的工夫 |
@PastOrPresent | 必须是过来的工夫,蕴含当初 |
@PositiveOrZero | 负数或0 |
@Size | 校验容器的元素个数 |
首先Validator能够十分不便的制订校验规定,并主动帮你实现校验。首先在入参里须要校验的字段加上注解,每个注解对应不同的校验规定,并可制订校验失败后的信息:
@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;}
校验规定和谬误提示信息配置结束后,接下来只须要在接口仅须要在校验的参数上加上@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{}@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
分组的参数字段;如果继承了,会校验了其余默认属于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@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(); } /** * 零碎异样 预期以外异样 */ @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办法,无需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(); }
这样就对异样的解决就比拟标准了,当然还能够增加对Exception的解决,这样无论产生什么异样咱们都能屏蔽掉而后响应数据给前端,不过倡议最初我的项目上线时这样做,可能屏蔽掉错误信息裸露给前端,在开发中为了不便调试还是不要这样做。
另外,当咱们抛出自定义异样的时候全局异样解决只响应了异样中的错误信息msg给前端,并没有将错误代码code返回。这还须要配合数据对立响应。
如果在多模块应用,全局异样等公共性能形象成子模块,则在须要的子模块中须要将该模块包扫描退出,@SpringBootApplication(scanBasePackages = {"com.xxx"})
五、数据对立响应
对立数据响应是咱们本人自定义一个响应体类,无论后盾是运行失常还是产生异样,响应给前端的数据格式是不变的!这里我包含了响应信息代码code和响应信息阐明msg,首先能够设置一个枚举标准响应体中的响应码和响应信息。
@Getterpublic 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;@Getterpublic 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; }}
最初须要批改全局异样解决类的返回类型
@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层进行接口信息数据的返回
@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@NoArgsConstructorpublic 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来接管
@Configurationpublic 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开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞+转发哦!