開放平臺設計方案與實踐

一、背景

隨着業務的發展,越來越多的系統需要數據往來。那對外提供的接口也越來越多,而且各個接口散落在不同的項目中被調用,多了的話排查問題困難且混亂。基於這個痛點,我們有必要打造一套開放平臺來管理各個 api 的調用情況。

二、開放平臺設計

我們先從整體的功能需求來分析,主要有以下幾點:

這裏老周給出自己的一個架構,大家可以參考下:

上面的設計方案更多的是針對比較大型的公司,想要把整個開平的能力建設完善。但市場上更多的是中小型公司,它們沒有太多的人力去開發與建設這麼全面的開放平臺。

那如果是中小型公司,那它們的開放平臺如何不費很大精力去實現呢?不管中小型還是大型公司的開放平臺,上面說的那個圖中其它部分可以省略,但安全機制是必需的,也就是架構圖中的統一鑑權。試想一下,作爲提供給第三方調用接口的開放平臺,如果安全機制不能保障,那外部誰都可以來調用你們公司的內部資源,危害可想而知。

老周下面就來針對不同的業務場景來給出相應的開放平臺安全機制的保障,也就是根據不同類型的網站給出相對應的開放平臺設計方案。

三、小型網站

3.1 基於 session 的登錄認證

在傳統的用戶登錄認證中,因爲 http 是無狀態的,所以都是採用 session 方式。用戶登錄成功,服務端會保存一個 session,當然會給客戶端一個 sessionId,客戶端會把 sessionId 保存在 cookie 中,每次請求都會攜帶這個 sessionId。服務器收到 sessionId,找到前期保存的數據,由此得知用戶的身份。

對於小型網站,特別是單機系統,基於 session 的登錄認證方案已經夠用了,而且簡單高效。

四、中型網站

隨着用戶量的增多,上面基於 cookie + session 的這種模式缺點就顯現出來了,這種模式通常是保存在內存中,而且服務從單服務到多服務會面臨 session 共享問題,開銷也隨即越來越大。

那中型網站的安全認證機制是啥呢?接下來 JWT(JSON Web Token) 即將登場,關於 JWT 的概念與原理,老周這裏覺得還是有必要說一下。

4.1 JWT 的概念

4.1.1 什麼是 JWT?

JWT 是一個開放的行業標準(RFC 7519),它定義了一種簡潔的、自包含的協議格式,用於在通信雙方傳遞 json 對象,傳遞的信息經過數字簽名可以被驗證和信任。JWT 可以使用 HMAC 算法或使用 RSA 的公鑰 / 私鑰對來簽名,防止被篡改。

說白了 JWT 就是一套基於 token 的身份認證的方案,可以保證安全傳輸的前提下傳送一些基本的信息,以減輕對外部存儲的依賴,減少了分佈式組件的依賴,減少了硬件的資源。

可實現無狀態、分佈式的 Web 應用授權,JWT 的安全特性保證了 token 的不可僞造和不可篡改。

本質上是一個獨立的身份驗證令牌,可以包含用戶標識、用戶角色和權限等信息,以及您可以存儲任何其他信息(自包含)。任何人都可以輕鬆讀取和解析,並使用密鑰來驗證真實性。

4.1.2 JWT 令牌結構

JWT 令牌由三部分組成,每部分中間使用點 (.) 分隔,比如:xxxxx.yyyyy.zzzzz

4.2 JWT 的流程

4.3 JWT 代碼案例

如果你們公司有第三方應用接入的開放平臺,那可以在裏面走相應的接入流程得到 appId 和 appSecret。如果沒有的話,那可以簡單點與第三方約定相應的 appId 和 appSecret。老周這裏假設你們已經約定好了,我這裏直接放在請求頭裏來獲取 token,還有其它的方式,比如放在請求參數或者 cookie 裏。

4.3.1 maven 依賴

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.1</version>
</dependency>

4.3.2 JWTUtil 工具類

public class JWTUtil {
    private static String SECRETE = "default_secrete";
    private static String APP_ID = "zhifubao";
    private static String APP_SECRETE = "123abc";

    /**
     * 傳入 appId、appSecret 進行驗證
     * @param appId 應用id
     * @param appSecret 應用密鑰
     * @return 返回一個加密 JWT token
     */
    public static String getToken(String appId, String appSecret) {
        String token = JWT.create()
                // 存放 payload 數據
                .withClaim("appId", appId)
                .withClaim("appSecret", appSecret)
                // 使用 SECRETE 對稱加密生成 signature
                .sign(Algorithm.HMAC256(SECRETE));
        return token;
    }

    /**
     * 驗證 token
     * @param token
     * @return
     */
    public static boolean verifyToken(String token) {
        HashMap<String, String> map = new HashMap<>();
        // 通過 SECRETE 和相同的對稱加密算法反加密
        DecodedJWT jwt = JWT.require(Algorithm.HMAC256(SECRETE))
                .build().verify(token);
        // 獲得你儲存的 payload 信息
        String appId = jwt.getClaim("appId").asString();
        String appSecret = jwt.getClaim("appSecret").asString();
        if (APP_ID.equals(appId) && APP_SECRETE.equals(appSecret)) {
            return true;
        }
        return false;
    }
}

4.3.3 JWTController 類

@RestController
public class JWTController {
    @RequestMapping("/getToken")
    public String getToken(@RequestHeader("appId") String appId, @RequestHeader("appSecret") String appSecret) {
        return JWTUtil.getToken(appId, appSecret);
    }
}

4.3.4 測試

拓展:這個私鑰 secrete 是固定的,爲了加強安全,你甚至可以使用動態的 secrete 私鑰,
例如:動態私鑰 = 靜態私鑰 + 用戶的 ip,這樣即使別人得到了用戶的 token,也會因爲 ip 不一致而訪問失敗。

拿到了應用資源服務器的 token 令牌了,那我們拿這個令牌去訪問相應的資源看看。

@RequestMapping("/getResource")
public String getResource(String resourceId) {
    return resourceId + " 資源獲取成功";
}

簡單模擬一個請求,直接返回該資源獲取成功。我們接下來就用 postman 工具來模擬一下這個資源服務器的這個接口請求。

認證失敗了,這是因爲我們沒有在請求頭裏填剛剛獲取的 token。我們把通過調用 getToken 接口獲取的 token 值放在在請求頭,然後認證通過,獲取到了資源服務器的資源。

4.3.5 繼續追問

這裏你有可能問了,老周,這裏咋就帶上 token 在請求頭就可以獲取到了資源服務器的資源啊。

我把代碼貼出來,你一看就知道了。

這裏寫了一個 token 的攔截器,對請求頭的 token 進行驗籤,通過才放行。

@Component
public class TokenInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("token");
        if (token != null) {
            boolean result = JWTUtil.verifyToken(token);
            if (result) {
                System.out.println("通過攔截器");
                return true;
            }
        }

        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try{
            response.getWriter().append("認證失敗,無效的token令牌!");
            System.out.println("認證失敗,無效的token令牌!");
        } catch (Exception e) {
            e.printStackTrace();
            response.sendError(500);
            return false;
        }
        return false;
    }
}

這裏有個攔截器配置類,把需要攔截的 api 路徑放進來,然後會對某個 api 進行細粒度的管控。

@Configuration
public class IntercepterConfig implements WebMvcConfigurer {
    private TokenInterceptor tokenInterceptor;

    public IntercepterConfig(TokenInterceptor tokenInterceptor){
        this.tokenInterceptor = tokenInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        List<String> excludePath = new ArrayList<>();
        excludePath.add("/getResource/");
        excludePath.add("/static/**");  //靜態資源
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(excludePath);
        WebMvcConfigurer.super.addInterceptors(registry);
    }
}

這就實現中型網站安全認證機制了,細心的讀者可能會發現,這個 token 是固定的,會存在一些不安全。是的,我上面也說了,可以用動態的 secrete 私鑰或者 token 過期機制來繼續保證更高的安全性。

五、大型網站

大型網站的話,針對中型網站的方案就不太可行了,爲什麼呢?由於大型網站的請求流量很大,而 token 由於自包含信息,因此一般數據量較大,而且每次請求都需要傳遞,因此比較佔帶寬。另外,token 的簽名驗籤操作也會給 cpu 帶來額外的處理負擔。可以採用微服務統一認證方案 Spring Cloud OAuth2,那什麼情況下需要使用 OAuth2?

5.1 OAuth2 構建微服務統一認證服務思路


注意:在我們統一認證的場景中,Resource Server 其實就是我們的各種受保護的微服務,微服務中的 各種 API 訪問接口就是資源,發起 http 請求的瀏覽器就是 Client 客戶端 (對應爲第三方應用)。

5.1.1 搭建認證服務器 (Authorization Server)

5.1.1.1 maven 依賴文件

5.1.1.2 application.yml 文件

5.1.1.3 OauthServerApplication9999 啓動類

5.1.1.4 認證服務器配置類

5.1.1.5 認證服務器安全配置類

5.1.1.6 測試

5.1.1.6.1 獲取 token

http://localhost:9999/oauth/token?client_secret=abcxyz&grant_type=password&username=admin&password=123456&client_id=client_riemann

endpoint:/oauth/token

獲取 token 攜帶的參數
client_id: 客戶端 id
client_secret: 客戶單密碼
grant_type: 指定使用哪種頒發類型,password
username: 用戶名
password: 密碼

5.1.1.6.2 校驗 token

http://localhost:9999/oauth/check_token?token=28317df7-4036-4bbb-8bb3-12f71fa07802

如果出現以上頁面,表明 token 過期了,設置的是 20s。所以要在 20s 以內校驗纔會生效。

下面纔是 token 校驗成功的效果:

5.1.1.6.3 刷新 token

http://localhost:9999/oauth/token?grant_type=refresh_token&client_id=client_riemann&client_secret=abcxyz&refresh_token=68582d02-3a1d-4c31-ae22-ac7e84824d0d

5.1.2 搭建資源服務器 (希望訪問被認證的微服務)

5.1.2.1 資源服務 Resource Server 配置類

5.1.2.2 測試

此測試結果也印證了代碼的效果

我們加上帶上 token 測下看看:


5.2 OAuth2 統一認證服務思考

5.3 JWT 改造統一認證授權中心的令牌存儲機制

JWT 在上面中型網站那一節說過了,這裏就不重複說了,老周直接上代碼了。

5.3.1 認證服務器端 JWT 改造 (改造主配置類)

5.3.2 修改 JWT 令牌服務方法

5.3.3 認證服務器端測試

可以看出,使用 jwt 令牌生成的 access_token 和上一篇的不一樣。

我們用這個網站:https://jwt.io/#encoded-jwt 把該 access_token 進行解碼,解碼如下:

其他兩個驗證 token、刷新 token 跟上一篇類似。

5.3.4 資源服務器校驗 JWT 令牌

不需要和遠程認證服務器交互,添加本地 tokenStore。

5.3.5 源服務器端測試

這樣就完成了資源服務根據事先約定的算法自行完成令牌校驗,無需每次都請求認證服務完成授權。

六、總結

老周首先從開放平臺的整體功能設計來分析了有如下幾個要點:開發者認證、開放平臺內部管理系統、安全機制以及性能。

但考慮很多公司它們沒有太多的人力去開發與建設這麼全面的開放平臺,故抓住其中的最核心的一點,那就是安全機制。

針對於安全機制來說,不同類型的網站有不同的安全機制保障。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Qu_n1l3IY6Htmucpmu_myQ