Open API设计实践
一.什么是Open API
随着业务的发展,企业与外界的交互合作变得越来越频繁,某些时候需要双方互相传输数据、提供服务,于是企业把部分对外服务的功能封装成一系列API并供对方或第三方使用,这种API就叫做Open API,而集合了各类功能的API的平台,就叫做开放平台。而这篇文章重点讲Open API的设计。
二.Open API的设计
在API的设计部分,主要采用RESTful API+Swagger规范来实现。
2.1 RESTful API设计
RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。RESTful API设计主要关注对资源的抽象及对资源的操作。
1.资源
资源就是网络上的一个实体,可以是文本、图片、音频、视频。每个网址代表一种资源,所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以 API 中的名词也应该使用复数。
例如系统中关于试题和题型的信息,它们的资源路径应该设计成下面这样:
https://www.example.cn/open-api/questions //试题资源
https://www.example.cn/open-api/question-types //题型资源
2.对资源的操作
由于RESTful是基于HTTP协议,因此对于资源的具体操作类型,由 HTTP 动词表示。常用的HTTP动词有下面五个:
HTTP动词 | 操作 | 描述 |
---|---|---|
GET | SELECT | 从服务器取出资源(一项或多项) |
POST | INSERT | 在服务器新建一个资源 |
PUT | UPDATE | 在服务器更新资源(客户端提供改变后的完整资源) |
PATCH | UPDATE | 在服务器更新资源(客户端提供改变的属性) |
DELETE | DELETE | 从服务器删除资源 |
例如系统中的试题资源,对它的操作应该设计成下面这样:
GET /questions: 列出所有试题
POST /questions: 创建一个试题
GET /questions/{ID}: 获取某个指定试题信息
PUT /questions/{ID}: 更新某个指定试题的信息(提供该试题的全部信息)
PATCH /questions/{ID}: 更新某个指定试题的信息(提供该试题的部分信息)
DELETE /questions/{ID}: 删除某个试题
2.2 Swagger设计
Swagger的目标是为 REST APIs 定义一个标准的,与语言无关的接口,使人和计算机在看不到源码或者看不到文档或者不能通过网络流量检测的情况下能发现和理解各种服务的功能。当Open API通过 Swagger 定义,可以提高API文档的可维护性和可读性。
RESTful+Swagger设计成下面的样子:
@Api(value = "question open Api Client", description = "试题开放 API", protocols = "application/json")
@RequestMapping("/open-api")
public interface QuestionOpenApi {
@ApiOperation(value = "获取所有学段", notes = "获取所有学段", nickname = "periods")
@ApiImplicitParams({
@ApiImplicitParam(name = "access-key", value = "access-key", dataType = "string", paramType = "header", required = true),
@ApiImplicitParam(name = "access-token", value = "access-token", dataType = "string", paramType = "header", required = true)
})
@RequestMapping(value = "/periods", method = RequestMethod.GET)
List<OpenPeriodDTO> periods();
@ApiOperation(value = "获取所有学科", notes = "获取所有学科", nickname = "subjects")
@ApiImplicitParams({
@ApiImplicitParam(name = "access-key", value = "access-key", dataType = "string", paramType = "header", required = true),
@ApiImplicitParam(name = "access-token", value = "access-token", dataType = "string", paramType = "header", required = true),
@ApiImplicitParam(name = "periodId", value = "学段id", paramType = "path", dataType = "string", required = true)
})
@RequestMapping(value = "/subjects/{periodId}", method = RequestMethod.GET)
List<OpenSubjectDTO> subjects(@PathVariable("periodId") String periodId);
@ApiOperation(value = "获取某科目学科题型", notes = "获取某科目学科题型", nickname = "subjectTypes")
@ApiImplicitParams({
@ApiImplicitParam(name = "access-key", value = "access-key", dataType = "string", paramType = "header", required = true),
@ApiImplicitParam(name = "access-token", value = "access-token", dataType = "string", paramType = "header", required = true),
@ApiImplicitParam(name = "periodId", value = "学段id", paramType = "query", dataType = "string", required = true),
@ApiImplicitParam(name = "subjectId", value = "学科id", paramType = "query", dataType = "string", required = true)
})
@RequestMapping(value = "/subject-types", method = RequestMethod.GET)
List<OpenSubjectTypeDTO> subjectTypes(@RequestParam("periodId") String periodId, @RequestParam("subjectId") String subjectId);
@ApiOperation(value = "获取某科目知识点", notes = "获取某科目知识点", nickname = "points")
@ApiImplicitParams({
@ApiImplicitParam(name = "access-key", value = "access-key", dataType = "string", paramType = "header", required = true),
@ApiImplicitParam(name = "access-token", value = "access-token", dataType = "string", paramType = "header", required = true),
@ApiImplicitParam(name = "periodId", value = "学段id", paramType = "query", dataType = "string", required = true),
@ApiImplicitParam(name = "subjectId", value = "学科id", paramType = "query", dataType = "string", required = true)
})
@RequestMapping(value = "/points", method = RequestMethod.GET)
KnowledgePointTreeListDTO points(@RequestParam("periodId") String periodId, @RequestParam("subjectId") String subjectId);
@ApiOperation(value = "搜索试题", notes = "搜索试题", nickname = "search")
@ApiImplicitParams({
@ApiImplicitParam(name = "access-key", value = "access-key", dataType = "string", paramType = "header", required = true),
@ApiImplicitParam(name = "access-token", value = "access-token", dataType = "string", paramType = "header", required = true),
@ApiImplicitParam(name = "param", value = "参数", paramType = "body", dataType = "ParamQuestionDTO", required = true)
})
@RequestMapping(value = "/questions", method = RequestMethod.GET)
PageableData<QuestionDTO> questions(@RequestBody @Valid ParamQuestionDTO param);
}
预览图如下:
OpenAPI的Swagger预览图
三.Open API签名和认证的设计
随着API接口的暴露,数据安全性就显得尤为重要了,Open API的签名和认证就是为了保证数据安全性而产生的。
Open API需要解决以下3个问题:
1.请求是否合法:是否是我规定的那个人;
2.请求是否被篡改:是否被第三方劫持并篡改参数;
3.防止重复请求(防重放):是否重复请求;
3.1 签名设计
服务提供方为了控制调用权限,会要求第三方开发者在自己的网站进行注册,并为其分配唯一的appKey 、 appSecert和预定义的加密方式。appKey 是为了保证该调用请求是平台授权过的调用方发出的,保证请求方唯一性,如果发现 appKey 不再注册库中则认为该请求方不合法;appSecert 是组成签名的一部分,增加暴力解密的难度的,通常是一段密文;预定义加密方式是双方约定好的加密方式,一般为散列非可逆加密,如 MD5、SHA1;
这里以subjects接口为例,原始参数为stage=3:
1.在原始参数的基础上,access-key、salt(防止重放攻击)、timestamp(防止重放攻击),按参数排序并拼接成新的参数;
access-key=sxw7HyIH4EhISMolkd4&salt=0.5186529128473576&stage=3×tamp=1638339806
2.将新的参数字符串和app-secret通过签名算法(如SHA256)签名,获取签名字符串;
zBrsd1MlrJHHKqwbMn2/YLrlyq9kWvVePIffNLpzhUM=
3.将签名字符串和参数字符串都作为参数拼接到请求URL;
http://www.example.com/open-api/subjects?access-key=sxw7HyIH4EhISMolkd4&salt=0.5186529128473576&stage=3×tamp=1638339806&sign=ZBRSD1MLRJHHKQWBMN2%2FYLRLYQ9KWVVEPIFFNLPZHUM%3D
服务提供者在接到这个请求之后,会将请求包中的所有参数按以上相同的方式进行加密。如果生成的参数签名一致,则签名通过,请求的合法性和请求参数都得到保护,不会被第三方劫持后篡改变为它用。
3.2 认证设计
由于签名功能已能保证大部分安全性,所以没有采用OAuth认证,只校验请求头中的access-key和access-token,核心代码如下:
public class AuthFilter extends OncePerRequestFilter implements Ordered {
private final static Logger log = LoggerFactory.getLogger(AuthFilter.class);
private static final String ACCESS_KEY = "access-key";
private static final String ACCESS_VALUE = "access-token";
private static final PathMatcher PATH_MATCHER = new AntPathMatcher();
private AuthProperties authProperties;
public AuthFilter(AuthProperties authProperties) {
this.authProperties = authProperties;
}
@Override
public int getOrder() {
int order = 0 - 106;
log.info("======= AuthFilter has Order:{} ======= ", order);
return order;
}
protected boolean hasContextFilter(HttpServletRequest request) {
return Arrays.stream(authProperties.getPattern())
.anyMatch(path -> PATH_MATCHER.match(path, request.getServletPath()));
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
if (hasContextFilter(request)) {
// into the Filter
return Boolean.FALSE;
}
return Boolean.TRUE;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
if (log.isDebugEnabled()) {
log.debug(">>>>>> Enter AuthFilter >>>>");
}
try {
String sxwAccessKey = request.getHeader(SXW_ACCESS_KEY);
String sxwAccessValue = request.getHeader(SXW_ACCESS_VALUE);
if (isLegal(sxwAccessKey, sxwAccessValue)) {
//通过
filterChain.doFilter(request, response);
} else {
//返回内容
JSONObject object = new JSONObject();
object.put("message", "非法访问");
//Http 200正常返回
response.setStatus(HttpStatus.OK.value());
response.setContentType(APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(JSON.toJSONString(object));
}
} catch (Exception e) {
e.printStackTrace();
throw new BusinessException(e);
}
log.debug(">>>>>> Complete SecurityCheckFilter >>>>\n\n\n");
}
private boolean isLegal(String accessKey, String accessToken) {
System.out.println("AccessKey:" + accessKey);
System.out.println("AccessToken:" + accessToken);
log.info("header AccessKey:{},header AccessValue:{}", accessKey, accessToken);
if (StringUtils.isEmpty(accessKey)) {
return false;
}
if (StringUtils.isEmpty(accessToken)) {
return false;
}
List<AuthProperties.AuthAccess> authAccesses = authProperties.getAuthAccesses();
if (CollectionUtils.isEmpty(authAccesses)) {
return false;
}
for (AuthProperties.AuthAccess authAccess : authAccesses) {
log.info("test accessKey:{},test accessToken:{}", authAccess.getAccessKey(), authAccess.getAccessToken());
if (Objects.equals(authAccess.getAccessKey(), accessKey) && Objects.equals(authAccess.getAccessToken(), accessToken)) {
return true;
}
}
return false;
}
}
四.总结
通过以上的设计和实现,只完成了最核心、最基本的Open API功能,满足了基本需求。并未进行OAuth认证、流量控制、数据加解密、黑白名单和API治理等功能,后续在此基础上再深入研究。