一.什么是Open API
隨著業(yè)務的發(fā)展严里,企業(yè)與外界的交互合作變得越來越頻繁,某些時候需要雙方互相傳輸數(shù)據(jù)追城、提供服務刹碾,于是企業(yè)把部分對外服務的功能封裝成一系列API并供對方或第三方使用,這種API就叫做Open API座柱,而集合了各類功能的API的平臺迷帜,就叫做開放平臺。而這篇文章重點講Open API的設計色洞。
二.Open API的設計
在API的設計部分瞬矩,主要采用RESTful API+Swagger規(guī)范來實現(xiàn)。
2.1 RESTful API設計
RESTful API是目前比較成熟的一套互聯(lián)網(wǎng)應用程序的API設計理論锋玲。它結(jié)構(gòu)清晰景用、符合標準、易于理解惭蹂、擴展方便伞插,所以正得到越來越多網(wǎng)站的采用。RESTful API設計主要關(guān)注對資源的抽象及對資源的操作盾碗。
1.資源
資源就是網(wǎng)絡上的一個實體媚污,可以是文本、圖片廷雅、音頻耗美、視頻京髓。每個網(wǎng)址代表一種資源,所以網(wǎng)址中不能有動詞商架,只能有名詞堰怨,而且所用的名詞往往與數(shù)據(jù)庫的表格名對應。一般來說蛇摸,數(shù)據(jù)庫中的表都是同種記錄的"集合"(collection)备图,所以 API 中的名詞也應該使用復數(shù)。
例如系統(tǒng)中關(guān)于試題和題型的信息赶袄,它們的資源路徑應該設計成下面這樣:
https://www.example.cn/open-api/questions //試題資源
https://www.example.cn/open-api/question-types //題型資源
2.對資源的操作
由于RESTful是基于HTTP協(xié)議揽涮,因此對于資源的具體操作類型,由 HTTP 動詞表示饿肺。常用的HTTP動詞有下面五個:
HTTP動詞 | 操作 | 描述 |
---|---|---|
GET | SELECT | 從服務器取出資源(一項或多項) |
POST | INSERT | 在服務器新建一個資源 |
PUT | UPDATE | 在服務器更新資源(客戶端提供改變后的完整資源) |
PATCH | UPDATE | 在服務器更新資源(客戶端提供改變的屬性) |
DELETE | DELETE | 從服務器刪除資源 |
例如系統(tǒng)中的試題資源蒋困,對它的操作應該設計成下面這樣:
GET /questions: 列出所有試題
POST /questions: 創(chuàng)建一個試題
GET /questions/{ID}: 獲取某個指定試題信息
PUT /questions/{ID}: 更新某個指定試題的信息(提供該試題的全部信息)
PATCH /questions/{ID}: 更新某個指定試題的信息(提供該試題的部分信息)
DELETE /questions/{ID}: 刪除某個試題
2.2 Swagger設計
Swagger的目標是為 REST APIs 定義一個標準的,與語言無關(guān)的接口敬辣,使人和計算機在看不到源碼或者看不到文檔或者不能通過網(wǎng)絡流量檢測的情況下能發(fā)現(xiàn)和理解各種服務的功能家破。當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 = "參數(shù)", paramType = "body", dataType = "ParamQuestionDTO", required = true)
})
@RequestMapping(value = "/questions", method = RequestMethod.GET)
PageableData<QuestionDTO> questions(@RequestBody @Valid ParamQuestionDTO param);
}
預覽圖如下:三.Open API簽名和認證的設計
隨著API接口的暴露汰聋,數(shù)據(jù)安全性就顯得尤為重要了,Open API的簽名和認證就是為了保證數(shù)據(jù)安全性而產(chǎn)生的喊积。
Open API需要解決以下3個問題:
1.請求是否合法:是否是我規(guī)定的那個人烹困;
2.請求是否被篡改:是否被第三方劫持并篡改參數(shù);
3.防止重復請求(防重放):是否重復請求乾吻;
3.1 簽名設計
服務提供方為了控制調(diào)用權(quán)限髓梅,會要求第三方開發(fā)者在自己的網(wǎng)站進行注冊,并為其分配唯一的appKey 绎签、 appSecert和預定義的加密方式枯饿。appKey 是為了保證該調(diào)用請求是平臺授權(quán)過的調(diào)用方發(fā)出的,保證請求方唯一性诡必,如果發(fā)現(xiàn) appKey 不再注冊庫中則認為該請求方不合法奢方;appSecert 是組成簽名的一部分,增加暴力解密的難度的爸舒,通常是一段密文蟋字;預定義加密方式是雙方約定好的加密方式,一般為散列非可逆加密扭勉,如 MD5鹊奖、SHA1;
這里以subjects接口為例涂炎,原始參數(shù)為stage=3:
1.在原始參數(shù)的基礎(chǔ)上忠聚,access-key设哗、salt(防止重放攻擊)、timestamp(防止重放攻擊)两蟀,按參數(shù)排序并拼接成新的參數(shù)网梢;
access-key=sxw7HyIH4EhISMolkd4&salt=0.5186529128473576&stage=3×tamp=1638339806
2.將新的參數(shù)字符串和app-secret通過簽名算法(如SHA256)簽名,獲取簽名字符串垫竞;
zBrsd1MlrJHHKqwbMn2/YLrlyq9kWvVePIffNLpzhUM=
3.將簽名字符串和參數(shù)字符串都作為參數(shù)拼接到請求URL澎粟;
http://www.example.com/open-api/subjects?access-key=sxw7HyIH4EhISMolkd4&salt=0.5186529128473576&stage=3×tamp=1638339806&sign=ZBRSD1MLRJHHKQWBMN2%2FYLRLYQ9KWVVEPIFFNLPZHUM%3D
服務提供者在接到這個請求之后蛀序,會將請求包中的所有參數(shù)按以上相同的方式進行加密欢瞪。如果生成的參數(shù)簽名一致,則簽名通過徐裸,請求的合法性和請求參數(shù)都得到保護遣鼓,不會被第三方劫持后篡改變?yōu)樗谩?/p>
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 {
//返回內(nèi)容
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;
}
}
四.總結(jié)
通過以上的設計和實現(xiàn),只完成了最核心气笙、最基本的Open API功能次企,滿足了基本需求。并未進行OAuth認證捅厂、流量控制租副、數(shù)據(jù)加解密码撰、黑白名單和API治理等功能,后續(xù)在此基礎(chǔ)上再深入研究堵第。