原文地址:http://www.moye.me/?p=592
OAuth是什么
OAuth(開放授權)是一個開放標准,允許用戶讓第三方應用訪問該用戶在某一網站上存儲的私密的資源(如照片,視頻,聯系人列表),而無需將用戶名和密碼提供給第三方應用。
OAuth 2.0 
OAuth的版本有v1.0, v1.0a 和 v2.0。OAuth 2.0 的出現主要是解決1.0+中的幾個問題,提升開發簡易度和應用安全性:
- 更好的支持非瀏覽器APP(移動和桌面客戶端,可以省略v1.0的交換token過程,增強用戶體驗)
- 給訪問令牌(Access-Token)添加了續期概念,對令牌泄露多了一層防御
- 不再強制客戶端使用Token Secret,有令牌就夠了
- 引入Bearer 驗證機制
OAuth 2 是目前被廣泛支持的版本,但不向下兼容1.0。
術語
簡單說,OAuth 2涉及到三方
- Client APP:要訪問用戶的程序
- Resource Owner :用戶,由他來給APP授權
- Authorization Server:也稱為 API Provider(Google/Facebook/Twitter…)
- client_id 和 client_secret:Server頒發給 APP 的身份憑證
- Access Token:Server頒發的令牌,訪問資源API需要帶着它
- Scope:APP 向Server 提供需要調用的API種類
- Redirect URL:APP 向 Server提供的回調地址
OAuth 2 交互模型
OAuth 2的 交互模型比較靈活,主要的交互模型分為如下幾種:
- 服務端Authorization code授權:被 Web服務端編程廣泛采用,也是本文重點介紹的模型
- 客戶端隱式授權:Web客戶端編程(JavaScript APP)使用這種方式交互,在用戶授權后直接取到令牌
- Resource Owner登錄授權:需要用戶輸入用戶名和密碼來交換令牌
- 客戶端憑證:這種模型下,APP 可能就是Owner,代表自己進行API調用
准備工作
首先,開發人員需要去API Provider處,為APP 創建應用信息,申請權限,以Google為例:
- 在 Google Developers Console 創建項目
- 在 APIs & Auth -> APIs 中,選擇打開需要調用的API
- 在 APIs & Auth -> Credentials 中,填寫 Redirect URIS(OAuth 2 回調的地址),創建Client ID。成功后,會得到需要的 CLIENT ID 和 CLIENT SECRET,保存好不要泄露
- 在 APIs & Auth -> Consent screen 中,填寫 Email/ Product Name/ Homepage/ Logo等 APP元信息,這些是給用戶看的,在交互過程中會出現在確認授權頁
服務端交互流程
OAuth 2 的服務端交互流程,是一個對用戶透明的三方過程:
如果用Node.js 實現前述的 Google API OAuth 2訪問,編程模型大概如此:
- 判斷是否已有 access_token,過期了嗎?如果不存在或過期,一步步來:
- 將用戶頁面跳向到https://accounts.google.com/o/oauth2/auth(附上一系列Query參數:response_type/ client_id/ redirect_uri/ scope,視需要追加參數:access_type/ approval_prompt/ state…)
- 為 redirect_uri 提供 HTTP GET 方法的處理,以響應回調
- 第3步的回調會收到一個code參數,用它向 https://accounts.google.com/o/oauth2/token 發起一個 POST請求,這次需要提供的參數:code / client_id/ client_secret/ redirect_uri/ grant_type
- 為 第4步 的redirect_uri 提供HTTP GET 方法的處理,以響應回調
- 第5步的回調會收到一個 JSON對象,里面有access_token 和它的過期時間expires_in,將它存下來
- 訪問API,附上得到的 access_token
引入Passport
顯然,自己實現OAuth 流程將需要寫不少東西,且每增加一個API Provider,這個過程需要再來一次。這行里有句黑話:寫得越多,錯得就越多 這時候,應該找個合適的框架
Passport 框架就是為解決類似問題而生的:它以中間件的形式為Node 程序提供身份認證,框架本身將一般形式的認證過程(Basic & Digest/ OAuth/ Open ID)、回調及錯誤處理進行了封裝,而將具體的認證實現抽象為Strategy(策略),與框架本身並無關系,只要是符合Passport 的Strategy都能以插件的形式加入項目被Passport使用。比如基於Google的OAuth 2認證,我們可以用 passport-google-oauth,基於Facebook的OAuth 2認證我們可以用passport-facebook,當然也可以用別的或者自己寫。 這種基於策略的抽象大大簡化了編程模型,所以Passport 有自信稱 " Simple, unobtrusive authentication for Node.js",誠不欺我。
Passport 實現 Google 用戶登錄
Google 作為具體的 API Provider,用Passport 對其進行OAuth 2訪問,需要一個 Strategy 來提供它的交互流程實現,本例中使用 passport-google-oauth
在package.json中確定引用,並用 npm install 安裝模塊:
"dependencies": { //... more libraries "passport": "*", "passport-google-oauth": "*", }
{ //...more configuration "GOOGLE_CLIENT_ID" : "xxxx.apps.googleusercontent.com", "GOOGLE_CLIENT_SECRET" : "wdsfas3_-safdsafasf", "GOOGLE_RETURN_URL" : "http://www.xxx.com/auth/google/return", }
上篇提到的配置讀取器configUtils 將能自動讀取到它,結合這些實現Google OAuth 2認證(authUtils.js:
var passport = require('passport') , GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; var config = require('../configUtils'); passport.use(new GoogleStrategy({ authorizationURL: 'https://accounts.google.com/o/oauth2/auth', tokenURL: 'https://accounts.google.com/o/oauth2/token', clientID: config.getConfigs().GOOGLE_CLIENT_ID, clientSecret: config.getConfigs().GOOGLE_CLIENT_SECRET, callbackURL: config.getConfigs().GOOGLE_RETURN_URL }, function (accessToken, refreshToken, profile, done) { var userInfo = { 'type': 'google', 'userid': profile.id, 'name': profile.displayName, 'email': profile.emails[0].value, 'avatar': profile._json.picture }; return done(null, userInfo); } passport.serializeUser(function (user, done) { done(null, user); }); passport.deserializeUser(function (obj, done) { done(null, obj); }); ));
代碼中的 function (accessToken, refreshToken, profile, done) 就是用戶授權后的回調,Passport 會將獲取到的用戶信息包裝成profile,里面的轉換代碼可以自由發揮,最后記得調用done,並將用戶信息返回。done 就是我們定義在路由里的回調,稍后會提到。
serializeUser 和 deserializeUser 是轉換用的序列化/ 反序列化。
結合Express
在 authUtils.js 中導出給 Express 用的路由:
module.exports = function (app) { app.use(passport.initialize()); app.get('/auth/google', passport.authenticate('google', { scope: 'https://www.google.com/m8/feeds https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile' })); app.get('/auth/google/return', passport.authenticate('google', { failureRedirect: '/login' }), function (req, res) { if (req.user) { //...some user login handling code } res.redirect('/'); }); };
第1個 get 路由是我們可以在UI 上引用的地址,點擊它開始OAuth 2 流程,即 Passport 的入口;第2個 get路由是Passport 的出口,即上節提到被 done 方法回調的地址,此時整個 OAuth 2 流程完成,程序得到了 user 信息(Passport將它注入到了 req上,但僅此一次可用,閱后即焚)。在認證過程發生任何錯誤,將跳轉到 failureRedirect 指定的地址。Passport為我們做了諸如 code 交換 token的一系列瑣碎事情。
app.js 中調用,So easy:
var authUtils = require('../authUtils'); authUtils(app);
小結
OAuth 2 協議為用戶資源的授權提供了一個安全的、開放而又簡易的標准,但即便如此,在Node.js 的 OAuth 2交互實現中,仍需 為Owner/App/Server的三方交互流程編寫很多代碼。使用Passport框架,將大大簡化這一編程模型,使我們可以將更多精力投入到業務實現上。
本文提供的僅是一個基於Passport框架的OAuth 2方案思路,代碼部分也僅是個骨架,望拋磚引玉。
更多文章請移步我的blog新地址: http://www.moye.me/