最近一直在負(fù)責(zé)開發(fā)公司的開放平臺(tái)相關(guān)工作,對接淘寶,阿里巴巴等開放平臺(tái),同時(shí)也負(fù)責(zé)開發(fā)系統(tǒng)的開放平臺(tái),在此稍作總結(jié)。本文只稍微分析聊一下授權(quán)碼模式,并且不嘗試解釋OAuth2.0參數(shù)為什么不是駝峰的……
參考資料
使用場景
用戶登錄云管店應(yīng)用,此時(shí)沒有辦法直接登錄阿里巴巴應(yīng)用查看數(shù)據(jù),或者阿里巴巴數(shù)據(jù)還未經(jīng)過處理,不是用戶的目標(biāo)數(shù)據(jù)。
用戶登錄云管店(假設(shè)該應(yīng)用對接了阿里巴巴應(yīng)用的接口)應(yīng)用,查看自己門店當(dāng)前的庫存數(shù)量,同時(shí)為了更直觀的了解到當(dāng)前阿里巴巴上掛的店鋪的庫存,云管店要去訪問阿里巴巴接口拉取到該用戶在阿里巴巴的店鋪的倉庫數(shù)量,統(tǒng)計(jì)成報(bào)表。
如果不適用OAuth2.0 ,云管店應(yīng)該如何讀取到阿里巴巴上的庫存數(shù)量?

用戶提供
阿里巴巴賬號密碼給云管店,云管店通過賬號密碼即可讀取到庫存信息。那么這么做有帶來什么隱患?
-
阿里巴巴賬號密碼泄露給云管店,云管店可以任意獲取用戶在阿里巴巴上的數(shù)據(jù) -
云管店數(shù)據(jù)庫如果泄露,也把阿里巴巴的賬號密碼等數(shù)據(jù)泄露出去 - 為了防止
云管店任意讀取數(shù)據(jù),只能通過修改賬號密碼 - ...
基于數(shù)據(jù)開放,且為了保護(hù)用戶數(shù)據(jù)安全等諸多問題,OAuth2.0應(yīng)運(yùn)而生,并成為當(dāng)前最主流的解決方案。
OAuth2.0 解決方案
OAuth2.0在客戶端與服務(wù)提供商之間,設(shè)置了一個(gè)授權(quán)訪問的屏障。客戶端無法直接拿到服務(wù)提供商的登錄賬號密碼,也就無法直接登錄服務(wù)提供商,只能請求授權(quán)服務(wù)提供商。
此時(shí)會(huì)要求用戶登錄資源提供商(該登錄服務(wù)由服務(wù)提供商提供,不會(huì)存在賬號密碼泄露等問題)。登錄后,授權(quán)服務(wù)提供商提示用戶確認(rèn)授權(quán)后提供給客戶端一個(gè)token令牌。服務(wù)提供商根據(jù)令牌的時(shí)效和授權(quán)范圍,向客戶端開放數(shù)據(jù)。

OAuth2.0客戶端授權(quán)模式
- 授權(quán)碼模式(authorization code)
- 簡化模式(implicit)
- 密碼模式(resource owner password credentials)
- 客戶端模式(client credentials)
授權(quán)碼模式
授權(quán)碼模式(authorization code)是功能最完整、流程最嚴(yán)密的授權(quán)模式。它的特點(diǎn)就是通過客戶端的后臺(tái)服務(wù)器,與"服務(wù)提供商"的認(rèn)證服務(wù)器進(jìn)行互動(dòng)。(本文只提到授權(quán)碼模式,其他相關(guān)客戶端授權(quán)模式請參考上文的參考資料進(jìn)行了解)

流程解析
(A)用戶訪問客戶端,后者將前者導(dǎo)向認(rèn)證服務(wù)器。
(B)用戶選擇是否給予客戶端授權(quán)。
(C)假設(shè)用戶給予授權(quán),認(rèn)證服務(wù)器將用戶導(dǎo)向客戶端事先指定的"重定向URI"(redirection URI),同時(shí)附上一個(gè)授權(quán)碼。
(D)客戶端收到授權(quán)碼,附上早先的"重定向URI",向認(rèn)證服務(wù)器申請令牌。這一步是在客戶端的后臺(tái)的服務(wù)器上完成的,對用戶不可見。
(E)認(rèn)證服務(wù)器核對了授權(quán)碼和重定向URI,確認(rèn)無誤后,向客戶端發(fā)送訪問令牌(access token)和更新令牌(refresh token)。
A步驟中,客戶端申請認(rèn)證的URI,包含以下參數(shù):
- response_type:表示授權(quán)類型,必選項(xiàng),此處的值固定為"code"
- client_id:表示客戶端的ID,必選項(xiàng)
- redirect_uri:表示重定向URI,可選項(xiàng)
- scope:表示申請的權(quán)限范圍,可選項(xiàng)
- state:表示客戶端的當(dāng)前狀態(tài),可以指定任意值,認(rèn)證服務(wù)器會(huì)原封不動(dòng)地返回這個(gè)值。
C步驟中,服務(wù)器回應(yīng)客戶端的URI,包含以下參數(shù):
- code:表示授權(quán)碼,必選項(xiàng)。該碼的有效期應(yīng)該很短,通常設(shè)為10分鐘,客戶端只能使用該碼一次,否則會(huì)被授權(quán)服務(wù)器拒絕。該碼與客戶端ID和重定向URI,是一一對應(yīng)關(guān)系。
- state:如果客戶端的請求中包含這個(gè)參數(shù),認(rèn)證服務(wù)器的回應(yīng)也必須一模一樣包含這個(gè)參數(shù)。
D步驟中,客戶端向認(rèn)證服務(wù)器申請令牌的HTTP請求,包含以下參數(shù):
- grant_type:表示使用的授權(quán)模式,必選項(xiàng),此處的值固定為"authorization_code"。
- code:表示上一步獲得的授權(quán)碼,必選項(xiàng)。
- redirect_uri:表示重定向URI,必選項(xiàng),且必須與A步驟中的該參數(shù)值保持一致。
- client_id:表示客戶端ID,必選項(xiàng)。
E步驟中,認(rèn)證服務(wù)器發(fā)送的HTTP回復(fù),包含以下參數(shù):
- access_token:表示訪問令牌,必選項(xiàng)。
- token_type:表示令牌類型,該值大小寫不敏感,必選項(xiàng),可以是bearer類型或mac類型。
- expires_in:表示過期時(shí)間,單位為秒。如果省略該參數(shù),必須其他方式設(shè)置過期時(shí)間。
- refresh_token:表示更新令牌,用來獲取下一次的訪問令牌,可選項(xiàng)。
- scope:表示權(quán)限范圍,如果與客戶端申請的范圍一致,此項(xiàng)可省略。
基于規(guī)范,動(dòng)手實(shí)現(xiàn)一個(gè)簡易版的授權(quán)碼模式
對應(yīng)于A步驟,客戶端發(fā)起授權(quán)請求(該請求可以要求登錄,用戶訪問該請求需要登錄)。授權(quán)參數(shù)需要參照OAuth2.0規(guī)范,最好是相應(yīng)的參數(shù)名稱都按照規(guī)范來。
@RequestMapping(value = "/authorize")
public String authorize(ModelMap modelMap, AuthorizeDTO authorizeDTO) {
// 如果是授權(quán)碼模式
if(GrantTypeEnum.AUTHORIZATION_CODE.getValue().equals(authorizeDTO.getResponse_type())) {
// 檢驗(yàn)客戶信息
if(!StoreFactory.getClientStore().isContainsClientId(authorizeDTO.getClient_id())) {
ModelMapUtil.setMessage(modelMap, ResultMessage.ERROR_CLIENT_ID);
return returnErrorPage();
}
// 檢驗(yàn)重定向地址
if(!StoreFactory.getClientStore().isContainsRedirectUri(authorizeDTO.getClient_id(), authorizeDTO.getRedirect_uri())) {
ModelMapUtil.setMessage(modelMap, ResultMessage.ERROR_REDIRECT_URI);
return returnErrorPage();
}
modelMap.put("client_id", authorizeDTO.getClient_id());
modelMap.put("redirect_uri", authorizeDTO.getRedirect_uri());
modelMap.put("state", authorizeDTO.getState());
}
return "/auth";
}
對應(yīng)步驟C,確認(rèn)授權(quán)后可以獲取到相應(yīng)的code與state等參數(shù),附著在回調(diào)地址中,且該回調(diào)地址必須與申請資質(zhì)時(shí)填寫的回調(diào)的地址(申請資質(zhì)需要客戶端應(yīng)用向服務(wù)提供商申請,由服務(wù)提供商頒發(fā)相應(yīng)的key與secret)
@RequestMapping(value = "/confirm")
public String accessConfirm(ModelMap modelMap, AuthorizeDTO authorizeDTO) {
// 檢驗(yàn)客戶信息
if(!StoreFactory.getClientStore().isContainsClientId(authorizeDTO.getClient_id())) {
ModelMapUtil.setMessage(modelMap, ResultMessage.ERROR_CLIENT_ID);
return returnErrorPage();
}
// 檢驗(yàn)重定向地址
if(!StoreFactory.getClientStore().isContainsRedirectUri(authorizeDTO.getClient_id(), authorizeDTO.getRedirect_uri())) {
ModelMapUtil.setMessage(modelMap, ResultMessage.ERROR_REDIRECT_URI);
return returnErrorPage();
}
// 根據(jù)填寫的回調(diào)地址回調(diào)回去
return "redirect:" + authorizeDTO.getRedirect_uri()+"?code="+StoreFactory.getCodeStore().createUUIDCode(authorizeDTO.getClient_id())
+"&state="+authorizeDTO.getState();
}
對應(yīng)步驟E,使用獲取到的code去換取token,或者使用舊的refresh_token去獲取新的token
@RequestMapping(value = "/token")
@ResponseBody
public ResultObject accessToken(ModelMap modelMap, AuthorizeTokenDTO authorizeTokenDTO) {
// 檢驗(yàn)客戶信息
if(!StoreFactory.getClientStore().isConatinsClient(authorizeTokenDTO.getClient_id(), authorizeTokenDTO.getClient_secret())) {
return ResultMessage.ERROR_CLIENT_ID.getResultObject();
}
// 檢驗(yàn)重定向地址
if(!StoreFactory.getClientStore().isContainsRedirectUri(authorizeTokenDTO.getClient_id(), authorizeTokenDTO.getRedirect_uri())) {
return ResultMessage.ERROR_REDIRECT_URI.getResultObject();
}
// 檢驗(yàn)code
if(!StoreFactory.getCodeStore().isRightCode(authorizeTokenDTO.getCode(), authorizeTokenDTO.getClient_id())) {
return ResultMessage.ERROR_CODE.getResultObject();
}
// 生成token
if(GrantTypeEnum.AUTHORIZATION_CODE.equals(authorizeTokenDTO.getGrant_type())) {
// 也可以根據(jù)redirect_uri 回調(diào)回去
// 也可以將返回值包裝成Josn返回
//
return ResultMessage.SUCCESS.getResultObject(StoreFactory.getTokenStore().createUUIDToken(authorizeTokenDTO.getClient_id()));
}
// 刷新token
if(GrantTypeEnum.REFRESH_TOKEN.equals(authorizeTokenDTO.getGrant_type())) {
// 拿到refreshToken 并檢驗(yàn)刷新
// 這里沒有做實(shí)現(xiàn),但是原理一致
return ResultMessage.SUCCESS.getResultObject(StoreFactory.getTokenStore().createUUIDToken(authorizeTokenDTO.getClient_id()));
}
return ResultMessage.ERROR_GRANT_TYPE.getResultObject();
}
如此簡單便可以實(shí)現(xiàn)一個(gè)最簡易的授權(quán)碼模式的服務(wù)。麻雀雖小,卻也五臟俱全,不能直接用于真實(shí)生產(chǎn)環(huán)境,但是對于理解OAuth2.0的授權(quán)過程卻也足以。
代碼地址:https://gitee.com/linweifeng/OAuth/tree/master
分布式環(huán)境
如果是單機(jī)應(yīng)用,我們的授權(quán)服務(wù),資源服務(wù)(開放的接口)都是可以統(tǒng)一放在一個(gè)應(yīng)用上,那么實(shí)現(xiàn)自然是非常簡單,通過攔截器/自定義注解實(shí)現(xiàn)AOP都可以做到非常完美,代碼寫起來也很6很順手。
但是如果是分布式環(huán)境,比如現(xiàn)在最流行的微服務(wù)架構(gòu)就需要考慮的問題比較多,比如token校驗(yàn)合法性。

授權(quán)服務(wù)獨(dú)立一個(gè)應(yīng)用,功能簡單,輕量.
資源服務(wù)可能由于訪問量較大,需要部署多臺(tái)服務(wù),通過負(fù)載均衡來保證服務(wù)穩(wěn)定。
當(dāng)客戶端授權(quán)完成并成功拿到token之后即可用它來訪問資源服務(wù),拉取數(shù)據(jù)。那么此時(shí)就需要校驗(yàn)token的合法性,那么誰來校驗(yàn)token才是最合適的呢?
資源服務(wù)提供者進(jìn)行token校驗(yàn)
資源服務(wù)提供token合法性校驗(yàn)
- 資源服務(wù)需要校驗(yàn)
token的合法性,相對復(fù)雜 - 受理了校驗(yàn)
token合法性的業(yè)務(wù),不能為其他應(yīng)用提供服務(wù),接口受制。
網(wǎng)管中心進(jìn)行token校驗(yàn)
網(wǎng)管中心是掌管一切請求的入口,在這一層做token校驗(yàn)也是極為合理的。
- 就如同需要校驗(yàn)請求是否登錄一樣,在網(wǎng)管中心校驗(yàn)
token - 接口不受理校驗(yàn)
token合法性的業(yè)務(wù),接口可以作為其他服務(wù)提供者。 - 實(shí)現(xiàn)相對復(fù)雜
授權(quán)服務(wù)進(jìn)行token校驗(yàn)
授權(quán)服務(wù)提供token合法性校驗(yàn),通過feign將請求再轉(zhuǎn)發(fā)到資源服務(wù)
- 把所有與授權(quán)相關(guān)的處理都統(tǒng)一在一個(gè)應(yīng)用處理。
- 授權(quán)服務(wù)的壓力甚至比
資源服務(wù)壓力更大,因?yàn)樗姓埱笕家?jīng)過授權(quán)服務(wù),所以授權(quán)服務(wù)也需要多臺(tái)部署。 - 接口不受理校驗(yàn)
token合法性業(yè)務(wù),接口可以作為其他服務(wù)提供者。
從架構(gòu)上來說,更加推薦使用網(wǎng)管中心進(jìn)行token校驗(yàn),業(yè)務(wù)方接口方可復(fù)用。授權(quán)服務(wù)進(jìn)行token檢驗(yàn)亦有其優(yōu)勢,業(yè)務(wù)方接口亦可復(fù)用,但是服務(wù)壓力大。
后記
OAuth2.0 目前已經(jīng)被各大互聯(lián)網(wǎng)公司所使用,足以證明它的優(yōu)秀與不凡。