開放平臺(tái)設(shè)計(jì)方案與實(shí)踐

點(diǎn)擊上方老周聊架構(gòu)關(guān)注我
一、背景
隨著業(yè)務(wù)的發(fā)展,越來越多的系統(tǒng)需要數(shù)據(jù)往來。那對(duì)外提供的接口也越來越多,而且各個(gè)接口散落在不同的項(xiàng)目中被調(diào)用,多了的話排查問題困難且混亂。基于這個(gè)痛點(diǎn),我們有必要打造一套開放平臺(tái)來管理各個(gè) api 的調(diào)用情況。
二、開放平臺(tái)設(shè)計(jì)
我們先從整體的功能需求來分析,主要有以下幾點(diǎn):
開發(fā)者身份注冊(cè)與數(shù)據(jù)權(quán)限范圍授權(quán)
開發(fā)者獲取相關(guān)資料(接口文檔、使用說明、對(duì)接人聯(lián)系方式等)
平臺(tái)方接入管理,申請(qǐng)審核流程、服務(wù)配置、服務(wù)管理、參數(shù)配置等
平臺(tái)方運(yùn)營(yíng)管理,業(yè)務(wù)交易管理及統(tǒng)計(jì)報(bào)表分析
安全層面需求,加密、應(yīng)用秘鑰、應(yīng)用接口權(quán)限控制、訪問黑白名單、字段脫敏還原等
性能方面要求,客戶端緩存、服務(wù)端緩存、緩存等


上面的設(shè)計(jì)方案更多的是針對(duì)比較大型的公司,想要把整個(gè)開平的能力建設(shè)完善。但市場(chǎng)上更多的是中小型公司,它們沒有太多的人力去開發(fā)與建設(shè)這么全面的開放平臺(tái)。
那如果是中小型公司,那它們的開放平臺(tái)如何不費(fèi)很大精力去實(shí)現(xiàn)呢?不管中小型還是大型公司的開放平臺(tái),上面說的那個(gè)圖中其它部分可以省略,但安全機(jī)制是必需的,也就是架構(gòu)圖中的統(tǒng)一鑒權(quán)。試想一下,作為提供給第三方調(diào)用接口的開放平臺(tái),如果安全機(jī)制不能保障,那外部誰都可以來調(diào)用你們公司的內(nèi)部資源,危害可想而知。
老周下面就來針對(duì)不同的業(yè)務(wù)場(chǎng)景來給出相應(yīng)的開放平臺(tái)安全機(jī)制的保障,也就是根據(jù)不同類型的網(wǎng)站給出相對(duì)應(yīng)的開放平臺(tái)設(shè)計(jì)方案。
三、小型網(wǎng)站
3.1 基于 session 的登錄認(rèn)證
在傳統(tǒng)的用戶登錄認(rèn)證中,因?yàn)?http 是無狀態(tài)的,所以都是采用 session 方式。用戶登錄成功,服務(wù)端會(huì)保存一個(gè) session,當(dāng)然會(huì)給客戶端一個(gè) sessionId,客戶端會(huì)把 sessionId 保存在 cookie 中,每次請(qǐng)求都會(huì)攜帶這個(gè) sessionId。服務(wù)器收到 sessionId,找到前期保存的數(shù)據(jù),由此得知用戶的身份。

四、中型網(wǎng)站
隨著用戶量的增多,上面基于 cookie + session 的這種模式缺點(diǎn)就顯現(xiàn)出來了,這種模式通常是保存在內(nèi)存中,而且服務(wù)從單服務(wù)到多服務(wù)會(huì)面臨 session 共享問題,開銷也隨即越來越大。
那中型網(wǎng)站的安全認(rèn)證機(jī)制是啥呢?接下來 JWT(JSON Web Token) 即將登場(chǎng),關(guān)于 JWT 的概念與原理,老周這里覺得還是有必要說一下。
4.1 JWT 的概念
4.1.1 什么是 JWT?
JWT 是一個(gè)開放的行業(yè)標(biāo)準(zhǔn)(RFC 7519),它定義了一種簡(jiǎn)潔的、自包含的協(xié)議格式,用于在通信雙方傳遞 json 對(duì)象,傳遞的信息經(jīng)過數(shù)字簽名可以被驗(yàn)證和信任。JWT 可以使用 HMAC 算法或使用 RSA的公鑰/私鑰對(duì)來簽名,防止被篡改。
說白了 JWT 就是一套基于 token 的身份認(rèn)證的方案,可以保證安全傳輸?shù)那疤嵯聜魉鸵恍┗镜男畔ⅲ詼p輕對(duì)外部存儲(chǔ)的依賴,減少了分布式組件的依賴,減少了硬件的資源。
可實(shí)現(xiàn)無狀態(tài)、分布式的 Web 應(yīng)用授權(quán),JWT 的安全特性保證了 token 的不可偽造和不可篡改。
本質(zhì)上是一個(gè)獨(dú)立的身份驗(yàn)證令牌,可以包含用戶標(biāo)識(shí)、用戶角色和權(quán)限等信息,以及您可以存儲(chǔ)任何其他信息(自包含)。任何人都可以輕松讀取和解析,并使用密鑰來驗(yàn)證真實(shí)性。
4.1.2 JWT 令牌結(jié)構(gòu)
JWT 令牌由三部分組成,每部分中間使用點(diǎn)(.)分隔,比如:xxxxx.yyyyy.zzzzz
Header
頭部包括令牌的類型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA),例如:{
"alg": "HS256",
"typ": "JWT"
}將上邊的內(nèi)容使用 Base64Url 編碼,得到一個(gè)字符串就是 JWT 令牌的第一部分。
Payload
第二部分是負(fù)載,內(nèi)容也是一個(gè) json 對(duì)象,它是存放有效信息的地方,它可以存放 jwt 提供的現(xiàn)成字段,比如:iss(簽發(fā)者),exp(過期時(shí)間戳),sub(面向的用戶)等,也可自定義字段。此部分不建議存放敏感信息,因?yàn)榇瞬糠挚梢越獯a還原原始內(nèi)容。最后將第二部分負(fù)載使用 Base64Url 編碼,得到一個(gè)字符串就是 JWT 令牌的第二部分。一個(gè)例子:{
"sub": "1234567890",
"name": "微信公眾號(hào)【老周料架構(gòu)】",
"iat": 1516239022
}Signature
第三部分是簽名,此部分用于防止 jwt 內(nèi)容被篡改。這個(gè)部分使用 base64url 將前兩部分進(jìn)行編碼,編碼后使用點(diǎn)(.)連接組成字符串,最后使用 header 中聲明簽名算法進(jìn)行簽名。
secret:簽名所使用的密鑰。HMACSHA256 (
base64UrlEncode(header) + "." + base64UrlEncode(payload), secret
)驗(yàn)簽過程描述:獲取 token 值,讀取 Header 部分并 Base64 解碼,得到簽名算法。根據(jù)以上方法算出簽名,如果簽名信息不一致,說明是非法的。
4.2 JWT 的流程

4.3 JWT 代碼案例
如果你們公司有第三方應(yīng)用接入的開放平臺(tái),那可以在里面走相應(yīng)的接入流程得到 appId 和 appSecret。如果沒有的話,那可以簡(jiǎn)單點(diǎn)與第三方約定相應(yīng)的 appId 和 appSecret。老周這里假設(shè)你們已經(jīng)約定好了,我這里直接放在請(qǐng)求頭里來獲取 token,還有其它的方式,比如放在請(qǐng)求參數(shù)或者 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 進(jìn)行驗(yàn)證
* @param appId 應(yīng)用id
* @param appSecret 應(yīng)用密鑰
* @return 返回一個(gè)加密 JWT token
*/
public static String getToken(String appId, String appSecret) {
String token = JWT.create()
// 存放 payload 數(shù)據(jù)
.withClaim("appId", appId)
.withClaim("appSecret", appSecret)
// 使用 SECRETE 對(duì)稱加密生成 signature
.sign(Algorithm.HMAC256(SECRETE));
return token;
}
/**
* 驗(yàn)證 token
* @param token
* @return
*/
public static boolean verifyToken(String token) {
HashMap<String, String> map = new HashMap<>();
// 通過 SECRETE 和相同的對(duì)稱加密算法反加密
DecodedJWT jwt = JWT.require(Algorithm.HMAC256(SECRETE))
.build().verify(token);
// 獲得你儲(chǔ)存的 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 測(cè)試

拓展:這個(gè)私鑰 secrete 是固定的,為了加強(qiáng)安全,你甚至可以使用動(dòng)態(tài)的 secrete 私鑰,
例如:動(dòng)態(tài)私鑰 = 靜態(tài)私鑰 + 用戶的 ip,這樣即使別人得到了用戶的 token,也會(huì)因?yàn)?ip 不一致而訪問失敗。
拿到了應(yīng)用資源服務(wù)器的 token 令牌了,那我們拿這個(gè)令牌去訪問相應(yīng)的資源看看。
@RequestMapping("/getResource")
public String getResource(String resourceId) {
return resourceId + " 資源獲取成功";
}
簡(jiǎn)單模擬一個(gè)請(qǐng)求,直接返回該資源獲取成功。我們接下來就用 postman 工具來模擬一下這個(gè)資源服務(wù)器的這個(gè)接口請(qǐng)求。

認(rèn)證失敗了,這是因?yàn)槲覀儧]有在請(qǐng)求頭里填剛剛獲取的 token。我們把通過調(diào)用 getToken 接口獲取的 token 值放在在請(qǐng)求頭,然后認(rèn)證通過,獲取到了資源服務(wù)器的資源。

4.3.5 繼續(xù)追問
這里你有可能問了,老周,這里咋就帶上 token 在請(qǐng)求頭就可以獲取到了資源服務(wù)器的資源啊。
我把代碼貼出來,你一看就知道了。
這里寫了一個(gè) token 的攔截器,對(duì)請(qǐng)求頭的 token 進(jìn)行驗(yàn)簽,通過才放行。
@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("認(rèn)證失敗,無效的token令牌!");
System.out.println("認(rèn)證失敗,無效的token令牌!");
} catch (Exception e) {
e.printStackTrace();
response.sendError(500);
return false;
}
return false;
}
}
這里有個(gè)攔截器配置類,把需要攔截的 api 路徑放進(jìn)來,然后會(huì)對(duì)某個(gè) api 進(jìn)行細(xì)粒度的管控。
@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/**"); //靜態(tài)資源
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(excludePath);
WebMvcConfigurer.super.addInterceptors(registry);
}
}
這就實(shí)現(xiàn)中型網(wǎng)站安全認(rèn)證機(jī)制了,細(xì)心的讀者可能會(huì)發(fā)現(xiàn),這個(gè) token 是固定的,會(huì)存在一些不安全。是的,我上面也說了,可以用動(dòng)態(tài)的 secrete 私鑰或者 token 過期機(jī)制來繼續(xù)保證更高的安全性。
五、大型網(wǎng)站
大型網(wǎng)站的話,針對(duì)中型網(wǎng)站的方案就不太可行了,為什么呢?由于大型網(wǎng)站的請(qǐng)求流量很大,而 token 由于自包含信息,因此一般數(shù)據(jù)量較大,而且每次請(qǐng)求都需要傳遞,因此比較占帶寬。另外,token 的簽名驗(yàn)簽操作也會(huì)給 cpu 帶來額外的處理負(fù)擔(dān)。可以采用微服務(wù)統(tǒng)一認(rèn)證方案 Spring Cloud OAuth2,那什么情況下需要使用 OAuth2?
第三方授權(quán)登錄的場(chǎng)景:比如,我們經(jīng)常登錄一些網(wǎng)站或者應(yīng)用的時(shí)候,可以選擇使用第三方授權(quán)登錄的方式,比如:微信授權(quán)登錄、QQ授權(quán)登錄、微博授權(quán)登錄等,這是典型的 OAuth2 使用場(chǎng)景。單點(diǎn)登錄的場(chǎng)景:如果項(xiàng)目中有很多微服務(wù)或者公司內(nèi)部有很多服務(wù),可以專?做一個(gè)認(rèn)證中心(充當(dāng)認(rèn)證平臺(tái)?色),所有的服務(wù)都要到這個(gè)認(rèn)證中心做認(rèn)證,只做一次登錄,就可以在多個(gè)授權(quán)范圍內(nèi)的服務(wù)中自由串行。
5.1 OAuth2 構(gòu)建微服務(wù)統(tǒng)一認(rèn)證服務(wù)思路

注意:在我們統(tǒng)一認(rèn)證的場(chǎng)景中,Resource Server 其實(shí)就是我們的各種受保護(hù)的微服務(wù),微服務(wù)中的 各種 API 訪問接口就是資源,發(fā)起 http 請(qǐng)求的瀏覽器就是 Client 客戶端(對(duì)應(yīng)為第三方應(yīng)用)。
5.1.1 搭建認(rèn)證服務(wù)器(Authorization Server)
5.1.1.1 maven 依賴文件


5.1.1.3 OauthServerApplication9999 啟動(dòng)類



5.1.1.6 測(cè)試
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攜帶的參數(shù)
client_id:客戶端id
client_secret:客戶單密碼
grant_type:指定使用哪種頒發(fā)類型,password
username:用戶名
password:密碼

5.1.1.6.2 校驗(yàn) token
http://localhost:9999/oauth/check_token?token=28317df7-4036-4bbb-8bb3-12f71fa07802

下面才是 token 校驗(yàn)成功的效果:

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 搭建資源服務(wù)器(希望訪問被認(rèn)證的微服務(wù))
5.1.2.1 資源服務(wù) Resource Server 配置類



此測(cè)試結(jié)果也印證了代碼的效果

我們加上帶上token測(cè)下看看:

5.2 OAuth2 統(tǒng)一認(rèn)證服務(wù)思考
當(dāng)我們第一次登陸之后,認(rèn)證服務(wù)器頒發(fā) token 并將其存儲(chǔ)在認(rèn)證服務(wù)器中,后期我們 訪問資源服務(wù)器時(shí)會(huì)攜帶 token,資源服務(wù)器會(huì)請(qǐng)求認(rèn)證服務(wù)器驗(yàn)證 token 有效性,如果資源服務(wù)器有很多,那么認(rèn)證服務(wù)器壓力會(huì)很大。
另外,資源服務(wù)器向認(rèn)證服務(wù)器 check_token,獲取的也是用戶信息 UserInfo,能否把用戶信息存儲(chǔ)到令牌中,讓客戶端一直持有這個(gè)令牌,令牌的驗(yàn)證也在資源服務(wù)器進(jìn)行,這樣避免和認(rèn)證服務(wù)器頻繁的交互。
我們可以考慮使用 JWT 進(jìn)行改造,使用 JWT 機(jī)制之后資源服務(wù)器不需要訪問認(rèn)證服務(wù)器。
5.3 JWT 改造統(tǒng)一認(rèn)證授權(quán)中心的令牌存儲(chǔ)機(jī)制
JWT 在上面中型網(wǎng)站那一節(jié)說過了,這里就不重復(fù)說了,老周直接上代碼了。
5.3.1 認(rèn)證服務(wù)器端 JWT 改造(改造主配置類)

5.3.2 修改 JWT 令牌服務(wù)方法

5.3.3 認(rèn)證服務(wù)器端測(cè)試

我們用這個(gè)網(wǎng)站:https://jwt.io/#encoded-jwt 把該 access_token 進(jìn)行解碼,解碼如下:

其他兩個(gè)驗(yàn)證 token、刷新 token 跟上一篇類似。
5.3.4 資源服務(wù)器校驗(yàn) JWT 令牌
不需要和遠(yuǎn)程認(rèn)證服務(wù)器交互,添加本地 tokenStore。

5.3.5 源服務(wù)器端測(cè)試


六、總結(jié)
老周首先從開放平臺(tái)的整體功能設(shè)計(jì)來分析了有如下幾個(gè)要點(diǎn):開發(fā)者認(rèn)證、開放平臺(tái)內(nèi)部管理系統(tǒng)、安全機(jī)制以及性能。
但考慮很多公司它們沒有太多的人力去開發(fā)與建設(shè)這么全面的開放平臺(tái),故抓住其中的最核心的一點(diǎn),那就是安全機(jī)制。
針對(duì)于安全機(jī)制來說,不同類型的網(wǎng)站有不同的安全機(jī)制保障。
小型網(wǎng)站:基于 session 的登錄認(rèn)證,在小型網(wǎng)站特別是單機(jī)系統(tǒng),這種方案夠用了,而且簡(jiǎn)單高效;
中型網(wǎng)站:到了中型網(wǎng)站,服務(wù)肯定是分布式部署的,這個(gè)時(shí)候小型網(wǎng)站中基于 session 的登錄認(rèn)證方案的缺點(diǎn)就暴露出來了。每個(gè)應(yīng)用服務(wù)都需要在 session 中存儲(chǔ)用戶身份信息,通過負(fù)載均衡將本地的請(qǐng)求分配到另一個(gè)應(yīng)用服務(wù)需要將 session 信息帶過去,否則會(huì)重新認(rèn)證。我們還要通過 session 共享、session 黏貼等方案來解決。從而引入了第三方分布式組件,比如 redis,增加了系統(tǒng)的復(fù)雜性。并且 session 方案還有另一個(gè)缺點(diǎn),比如基于 cookie,移動(dòng)端不能有效使用等。所以中型網(wǎng)站的話基于 JWT 的 token 認(rèn)證機(jī)制,服務(wù)端不用存儲(chǔ)認(rèn)證數(shù)據(jù),易維護(hù)擴(kuò)展性強(qiáng),客戶端可以把 token 存在任意地方,并且可以實(shí)現(xiàn) web 和 app 統(tǒng)一認(rèn)證機(jī)制。
大型網(wǎng)站:到了大型網(wǎng)站,請(qǐng)求量也隨之暴漲,中型網(wǎng)站的 token 認(rèn)證機(jī)制的缺點(diǎn)也逐步暴露出來了,token 由于自包含信息,因此 一般數(shù)據(jù)量較大,而且每次請(qǐng)求都需要傳遞,因此比較占帶寬。另外,token 的簽名驗(yàn)簽操作也會(huì)給 cpu 帶來額外的處理負(fù)擔(dān)。這個(gè)時(shí)候得采用微服務(wù)統(tǒng)一認(rèn)證方案 Spring Cloud OAuth2,后面我們又對(duì) OAuth2 進(jìn)行了一些優(yōu)化,因?yàn)榇笮途W(wǎng)站的開平請(qǐng)求流量會(huì)很大,資源服務(wù)器會(huì)請(qǐng)求認(rèn)證服務(wù)器驗(yàn)證 token 有效性,那么認(rèn)證服務(wù)器壓力會(huì)很大。另外,資源服務(wù)器向認(rèn)證服務(wù)器 check_token,獲取的也是用戶信息 UserInfo,能否把用戶信息存儲(chǔ)到令牌中,讓客戶端一直持有這個(gè)令牌,令牌的驗(yàn)證也在資源服務(wù)器進(jìn)行,這樣避免和認(rèn)證服務(wù)器頻繁的交互。所以我們后續(xù)使用 JWT 進(jìn)行改造,使用 JWT 機(jī)制之后資源服務(wù)器不需要訪問認(rèn)證服務(wù)器。性能以及安全機(jī)制都得到了有力保障。
歡迎大家關(guān)注我的公眾號(hào)【老周聊架構(gòu)】,Java后端主流技術(shù)棧的原理、源碼分析、架構(gòu)以及各種互聯(lián)網(wǎng)高并發(fā)、高性能、高可用的解決方案。
喜歡的話,點(diǎn)贊、再看、分享三連。

點(diǎn)個(gè)在看你最好看
