“知了”开发日志——知了Open API 开发的一些记录

最近要为我的开源项目知了提供Open API,特此写一个博客记录一下整个Open API 开发中的问题。

0. 服务器端的整体工作流程

先放上一张做的比较简单的流程图:)

流程图

关于整体的流程,主要就是在接收到http request 后,经过验签、验参两道关卡后,执行业务处理,最后返回结果。需要说明的是,为了保证后续开发的一致性,在API的开发过程中我做了统一的异常处理和统一的返回值封装,这个后面会具体提到。接下来我将着重记录一下每个部分开发的技术实现和碰到的一些问题。

1. 验签

知了中的实现

关于验签这一块,知了目前的实现其实比较简单,就是对appKey进行比对(有点偷懒的感觉了:))

实现的思路就是实现接口HandlerInterceptor新增了一个拦截器类OpenAPIInterceptor,然后添加到WebMvcConfigurer接口的addInterceptors()方法中。之所以要这么操作,是为了和项目本身提供的Web服务的登录拦截器区分开,同时需要总结的一点是,上述addInterceptors()方法中注册的不同的拦截器添加的路径/排除的路径是互相隔离的,Spring 这样的特性也很方便我们通过不同的拦截器实现不同的效果!

多说一点

然而需要注意的是,本身API的安全机制这件事是一件值得好好设计的事情,在此也特别记录一下还需要考虑的问题以及怎么做,具体参考了这篇博客

需要考虑的问题:

  • 请求的来源是否合规?

  • 请求参数是否被篡改?

  • 请求是否具备唯一性?(不可复制、预防DDOS)

解决整体思路(安全关键在于参与签名的secret,整个过程中secret 是不参与通信的,所以只要保证secret 不泄露,请求就不会被伪造):

  1. 客户端和服务端约定统一的参数加、解密算法
  2. 初始密钥(内置各应用中)+授权密钥token(从服务端获取动态生成,根据用户名密码或header 请求头信息生成token)->形成最终的secret通信密钥
  3. 对所有参数按特定规则进行排序,最后面加上时间戳拼接成一个字符串
  4. 将上述字符串与secret 通信密钥混合加密生成签名字符串,与参数、时间戳一起发送https 请求
  5. 服务端解密,验证签名字符串,验证时效

另外,关于动态Token 的实现参见这篇博客

2. 验参

忘了在哪里看到一句话,”永远不要把参数的校验交给用户完成“,因此,在服务端接受参数时做合规性校验是很有必要的。

SpringBoot 提供了强大的基于AOP的思想的Validation 可以让我们方便的做参数校验,该模块已被集成到SpringBoot 的start-web中,开箱即用。

在Controller 层中,类上添加@Validated注解即可开启,Validation 本身提供了诸如@NotNull@Size()@Min()@Max()等语义直观的注解可以直接加到接收的参数前,也可以通过自定义注解的形式实现丰富的功能,可以参考这篇博客

关于验参失败的异常的统一处理,在后面统一异常处理中具体阐述。

3. 统一返回值处理

知了中的实现

为了按照规定好的格式返回接口数据,例如{"code": 200, "msg": "业务处理成功", "data": "12313123"},封装了一个泛型类Result<T>来便于实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private Integer code;
private String msg;
private T data;

public static<T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(ResultEnum.SUCCESS.getCode());
result.setMsg(ResultEnum.SUCCESS.getMsg());
result.setData(data);
return result;
}

public static<T> Result<T> error(Integer code, String msg) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMsg(msg);
return result;
}

public static<T> Result<T> error(ResultEnum resultEnum) {
return error(resultEnum.getCode(), resultEnum.getMsg());
}

public static<T> Result<T> error(OpenAPIException e) {
return error(e.getCode(), e.getMessage());
}

}

其中,提供了几个静态方法以便于将获取到的数据或异常信息封装到Result<T>中。

同时,为了预置我们默认的状态码及返回信息,做了一个枚举类ResultEnum

1
2
3
4
5
6
7
8
9
10
11
12
@Getter
@AllArgsConstructor
public enum ResultEnum {
SUCCESS(200, "业务处理成功"),
WORK_ERROR(202, "业务处理失败"),
ACCESS_ERROR(401, "验签失败"),
ARGS_ERROR(402, "验参失败");

private Integer code;
private String msg;

}

最后,为了保证输出的统一,使用了@ControllerAdvice注解并实现了ResponseBodyAdvice接口做了一个AOP的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ControllerAdvice
public class GlobalReturnConfig implements ResponseBodyAdvice {


@Override
public boolean supports(MethodParameter returnType, Class converterType) {
//可以通过returnType、converterType获取该响应出自哪个方法/类。返回true将执行beforeBodyWrite()中的操作
return true;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//判断数据是否被封装到Result中
if (body instanceof Result<?>) {
return body;
}else {
return Result.success(body);
}
}
}

上述内容及后续的统一异常处理参考了两篇博客

关于@ControllerAdvice

@ControllerAdvice注解是一个@Component,是一个增强的@Controller,用于定义@ExceptionHandler@InitBinder@ModelAttribute方法,适用于所有使用@RequestMapping方法,在这里再记录一下其他用法:

  • (最常用)全局异常处理:配合@ExceptionHandler
  • 全局数据绑定(公共数据):配合@ModelAttribute
  • 全局数据预处理:配合@InitBinder

其他BUG

关于统一返回值处理中,碰到一个BUG,记录一下。

问题:@ResponseBody标注的方法返回一个String的时候JSON解析的异常

@ResponseBody的作用是将Controller 方法返回的Java 对象转换成JSON 或XML,写入到response 对象的body 区。

但是有一个特例,当返回类型为String的时候,会因为beforeBodyWrite()方法中需要把String塞入Result再返回JSON而这时无法转换JSON的BUG,此问题目前暂时没有完美解决,只是要求后续开发中不要直接在Controller 方法中返回String(这里有一篇博客或许能参考一下)。

4. 统一异常处理

关于统一的异常处理,首先是自定义了一个OpenAPIException类,其中包含了状态码属性,配合ResultEnum抛出相应预设的异常情况:

1
2
3
4
5
6
7
8
9
10
11
@Data
public class OpenAPIException extends RuntimeException {

private Integer code;

public OpenAPIException(ResultEnum resultEnum) {
super(resultEnum.getMsg());
this.code = resultEnum.getCode();
}

}

然后通过上一节中说到的@ControllerAdvice+@ExceptionHandler实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ControllerAdvice
public class GlobalExceptionHandle {

@ExceptionHandler(value = Exception.class)
@ResponseBody
public Result<?> handle(Exception e) {
if (e instanceof OpenAPIException) {
// 捕获OpenAPIException异常
OpenAPIException openAPIException = (OpenAPIException) e;
return Result.error(openAPIException);
} else if (e instanceof ConstraintViolationException ||
e instanceof MissingServletRequestParameterException) {
// 捕获Validation验签时的异常
return Result.error(ResultEnum.ARGS_ERROR);
} else {
// 其他异常
return Result.error(-1, e.toString());
}
}

}

OK本篇博客到这里结束了,希望知了的Open API 能被好的应用起来!

------ 本文结束,感谢观看! ------
 wechat
扫一扫,访问本站