Open API設計實踐

一.什么是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);
}

預覽圖如下:
OpenAPI的Swagger預覽圖

三.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&timestamp=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&timestamp=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ǔ)上再深入研究堵第。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市隧出,隨后出現(xiàn)的幾起案子踏志,更是在濱河造成了極大的恐慌,老刑警劉巖胀瞪,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件针余,死亡現(xiàn)場離奇詭異,居然都是意外死亡凄诞,警方通過查閱死者的電腦和手機涵紊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來幔摸,“玉大人摸柄,你說我怎么就攤上這事〖纫洌” “怎么了驱负?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵嗦玖,是天一觀的道長。 經(jīng)常有香客問我跃脊,道長宇挫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任酪术,我火速辦了婚禮器瘪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘绘雁。我一直安慰自己橡疼,他們只是感情好,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布庐舟。 她就那樣靜靜地躺著欣除,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挪略。 梳的紋絲不亂的頭發(fā)上历帚,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天,我揣著相機與錄音杠娱,去河邊找鬼挽牢。 笑死,一個胖子當著我的面吹牛摊求,可吹牛的內(nèi)容都是我干的禽拔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼睹簇,長吁一口氣:“原來是場噩夢啊……” “哼奏赘!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起太惠,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤磨淌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后凿渊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體梁只,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年埃脏,在試婚紗的時候發(fā)現(xiàn)自己被綠了搪锣。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡彩掐,死狀恐怖构舟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情堵幽,我是刑警寧澤狗超,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布弹澎,位于F島的核電站,受9級特大地震影響努咐,放射性物質(zhì)發(fā)生泄漏苦蒿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一渗稍、第九天 我趴在偏房一處隱蔽的房頂上張望佩迟。 院中可真熱鬧,春花似錦竿屹、人聲如沸报强。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽躺涝。三九已至厨钻,卻和暖如春扼雏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背夯膀。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工诗充, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人诱建。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓蝴蜓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親俺猿。 傳聞我的和親對象是個殘疾皇子茎匠,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容