OAuth
OAuth是一個關於授權(authorization)的開放網路標準,在全世界得到廣泛應用,目前的版本是2.0版。常見的採用微信、QQ、微博、Facebook、Google賬號登陸網站的過程都是採用了OAuth技術。這一章我們會以使用Google賬號登陸第三方網站為例,展示如何使用這項技術。
Google OAuth工作流程
- 整個OAuth過程主要設計三個方面,客戶端(對於網站而言則是瀏覽器)、第三方伺服器(對應網站的伺服器)和Google伺服器。當使用者點選使用Google賬號登陸網站時,第三方伺服器會直接把這個請求傳遞給Google伺服器,響應後頁面跳轉至Google的驗證授權頁面,詢問使用者是否同意授權。使用者同意後,谷歌伺服器會跳轉至第三方伺服器中,並且在跳轉URL上會攜帶一個code引數,第三方伺服器拿到code後會憑藉這個code再次向Google伺服器傳送請求,並換取使用者資訊。拿到使用者資訊後,第三方伺服器會檢查資料庫,如果沒有這個使用者則存入資料庫,並登陸成功,如果有則直接登陸成功。與此同時,給瀏覽器種一個標識使用者資訊的cookie,此後在cookie的有效期內,瀏覽器接下來每次對第三方伺服器的請求中都會攜帶cookie,因此可以表示使用者身份,做一些需要許可權才能做的事情。具體流程如下圖所示:
- 我使用passport這個庫幫助我們實現驗證流程。
passportJS
- 兩個問題:
- passportJS會自動化OAuth流程,但需要程式碼深入到流程細節中,並不能完全自動化整個流程
- 庫的結構,實際上我們需要兩個庫才能使用passportJS——passport、passport strategy,第一個是核心庫,用以提供驗證流程的工具方法,第二個是針對不同的授權提供方(Google、Facebook、Wechat etc.)所需要的定製方法,也就是說你如果需要同時提供Google、Facebook、Wechat三種驗證方式,那你就需要三個strategy庫。在 passportjs.org中提供了很多strategies庫。
- 安裝passport到專案中
npm install --save passport passport-google-oauth20複製程式碼
- 20的意思是版本為
2.0
,因為npm的包名稱中不能有.
,所以就起名為20了,其實這裡也可以不加20,那麼安裝的就是一個1.0
和2.0
的組合版。鑑於現在基本知名的auth provider都已經支援OAuth2.0,所以這裡採用2.0
版本。詳情參見passport-google-oauth github。 - 使用passport
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy());複製程式碼
- 在使用Google OAuth之前,需要兩個引數appid和api secrect,要獲取這兩個引數,需要在 console.developers.google.com 上建立專案(只要有Google賬號,very easy)
- 建立完專案,進入專案皮膚,進入api板塊,點選
啟用Google API
,搜尋Google +
,選擇Google + API
, 點選啟用
現在此API依然不能使用,需要點選點選
建立憑據
按鈕,按照提示流程一直走到最後,生成憑據,主要包含兩個資訊clientID和client金鑰。如果想要看詳細步驟,參考這裡。- 這個流程裡需要注意的點是,設定JavaScript授權域和授權回撥URL,因為我們現在建立的是一個開發專案,兩者分別設為 http://localhost:5000和http://localhost:5000/*
- clientID: 用於生成登陸用的URL
- clientSecrect: 用於證明該APP是否有權訪問token
接下來要把剛才生成的clientID和clientSectrect,傳入Google OAuth模組中。注意,clientSecrect不能公佈,謹慎起見clientID也應該保密。所以我們不希望別人通過檢視原始碼的形式獲取這兩個值。目前我們先通過不提交這部分程式碼的形式做到隱藏這部分資訊。
- 建立cong/keys.js,存放clientID和clientSecrect
module.exports = { googleClientId: '1229722414-eeujg12q0q9gvisar.apps.googleusercontent.com', googleClientSecret: 'ANPiCt5QFTa' };複製程式碼
- 在.gitignore中寫入
keys.js
,確保包含敏感資訊的檔案不會被提交 - 在index.js中,引入keys模組,並將對應的clientKey和clientID傳入GoogleStrategy模組中。
- 注意這裡還新增了回撥URL。這是因為當使用者點選授權後,Google伺服器會返回一個code到應用的伺服器,那我們伺服器應該如何接收並處理這個伺服器的返回呢。Google伺服器返回給app伺服器資訊,可以看做是一次請求(伺服器不就是用來處理請求的嗎),所以我們必須要指定請求的route是什麼,因此我們需要一個回撥URL的引數,google伺服器會將code拼接到這個URL的引數裡。
- 這裡還新增了一個回撥函式,所有驗證的目的就是為了拿到token,以便使用者隨後的操作,回撥函式定義了拿到token做什麼。目前僅僅先把token打出來看一下。
const keys = require('./config/keys'); passport.use(new GoogleStrategy({ clientID: keys.googleClientId, clientSecret: keys.googleClientSecret, callbackURL: '/auth/google/callback' }, (accessToken, refreshToken, profile, done) => {console.log(accessToken)}));複製程式碼
- 最後我們需要新增一個route handler,用以接收使用者login的請求,並進入Google OAuth流程,如下面的程式碼所示。
- 首先解釋一下程式碼的意思:如果伺服器接收到
/auth/google
的請求,使用passport啟用Google OAuth的驗證流程,需要獲取的資訊有使用者資料和郵箱。 - 這裡面的字串'google'看起來很讓人費解,因為在之前的程式碼中我們並沒有任何用這個字串代表Google OAuth strategy的意思。這是passport廣為人詬病的一點。事實上,Google OAuth strategy模組中設定了這一點,也就是說這個模組告訴passport如果
passport.authenticate
方法第一個引數傳入了google
,那麼就採用Google OAuth strategy模組驗證。
- 首先解釋一下程式碼的意思:如果伺服器接收到
app.get(
"/auth/google",
passport.authenticate("google", {
scope: ["profile", "email"]
})
);複製程式碼
- 現在啟動我們的本地伺服器,訪問
localhost:5000/auth/google
,按理應該會彈出google認證的頁面,但是不幸的是並沒有,這時彈出的是一個400頁面,大概的意思是說實際提供的驗證回撥地址和在console.developers.google.com中設定的不一致。還提供了一個連結,直接訪問這個連結就進入了修改驗證回撥URL的頁面。- 為什麼會出現這個錯誤頁?還記得之前我們把
已獲授權的重定向 URI
這一項設為http://localhost:5000/*
,事實上這裡需要嚴格匹配。之前在程式碼中我們設定callbackURL
為/auth/google/callback
,所以我們應該在這個修改頁面中將已獲授權的重定向 URI
這一項設為http://localhost:5000/auth/google/callback
,這樣之後應該就能正常彈出授權頁面了。 - 為什麼需要回撥驗證URL匹配?我們訪問google伺服器要求提供授權時,提供的引數是clientID,並且明文傳輸。攻擊者拿到clientID,並把redirect_uri改為惡意網站,那麼使用者授權後就肯能會跳轉到惡意網站,並提供所有的授權資訊。顯然,這種情況是堅決不能發生的,所以我們需要在google那邊配置允許的回撥URL,並嚴格匹配。如果不匹配是不會成功回撥的。
- 為什麼會出現這個錯誤頁?還記得之前我們把
- 點選對應的Google賬戶登陸,會跳到一個錯誤頁顯示
Cannot GET /auth/google/callback
。我們還沒有設定針對回撥route的handler,所以當然會報錯了。在這個頁面的URL中,會看到一個引數code,這就是在之前流程圖中提到的Google伺服器返回的code。我們app的伺服器拿到code後,就可以通過code再次向Google伺服器發請求,並拿到使用者的資料、郵箱等資訊了。所以接下來需要補上對應的route handler。app.get('/auth/google/callback', passport.authenticate('google'));複製程式碼
- 再次訪問
localhost:5000/auth/google
,點選賬戶登入,可以看到在啟動server的控制檯中列印出了一坨東西。之前我們在配置passport中傳入了一個回撥函式,在回撥函式中列印出了token。這一坨就是取到的token。- 實際上passport在回撥URL的handler中自動將code傳遞給了google伺服器,並換取了token、使用者資訊(資料、郵箱等)。這些資訊時通過函式引數的形式傳遞回來的。 因此,在這之後,那個列印token的函式被呼叫,我們的app可以在這個回撥函式中利用這些資訊做一些不可描述的事情。
- 在繼續之前,我們可以先把這些返回的資訊列印出來,看看長什麼樣子。修改程式碼,重啟server,重新訪問登陸連線,可以看到控制檯中列印出了token(string)、profile(object)、done(function)。
- accessToken: app後續訪問使用者資訊的憑證。
- accessToken過一段時間就會過期,refreshToken會允許我們重新整理得到最新的token。
- profile:使用者所有的資料。
- done函式的引數有三個:err(錯誤資訊),user(使用者資訊),info(其他資訊)
- 為什麼會pending?回撥函式中,我們並沒有給出響應response。
passport.use( new GoogleStrategy( { clientID: keys.googleClientId, clientSecret: keys.googleClientSecret, callbackURL: "/auth/google/callback" }, (accessToken, refreshToken, profile, done) => { console.log('accessToken', accessToken); console.log('refreshToken', refreshToken); console.log('profile', profile); console.log('done', done); } ) );複製程式碼
- 至此,所有授權的工作(在passport的幫助下)已經完成,接下來是建立使用者資訊到資料庫、登陸完成。
使用nodemon使開發自動化
- 至此,應該已經厭倦了修改程式碼,重啟server的過程。幸運的是已經有工具使這一切自動化,這個工具就是
nodemon
。 npm install --save-dev nodemon
- 修改package.json
"scripts": { "start": "node index.js", "dev": "nodemon index.js" },複製程式碼
- 之後只需要在命令列中輸入
npm run dev
,就可以啟動伺服器,並且每次修改程式碼儲存後,nodemon都會幫我們自動重啟伺服器了。重構目前的程式碼
- 之前我們把所有的邏輯都寫在index.js檔案中,為了便於維護和迭代,我們把邏輯分散在不同的目錄下。目前我們把邏輯分為三個部分config,routes,services。三個部分的含義如下圖所示。重構之後的目錄如下所示。基本的工作就是把routehandler的邏輯移動到authRoutes.js中,把配置passport的邏輯,移動到passport.js中,然後在兩個檔案中引入依賴的包或者其他模組。再在index中引入這兩個檔案。
├── config │ └── keys.js ├── index.js ├── package-lock.json ├── package.json ├── routes │ └── authRoutes.js └── services └── passport.js複製程式碼
- routes/authRoutes.js
const passport = require('passport');
module.exports = (app) => {
app.get(
"/auth/google",
passport.authenticate("google", {
scope: ["profile", "email"]
})
);
app.get("/auth/google/callback", passport.authenticate("google"));
}複製程式碼
- servics/passport.js
const passport = require('passport');
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const keys = require("../config/keys");
passport.use(
new GoogleStrategy(
{
clientID: keys.googleClientId,
clientSecret: keys.googleClientSecret,
callbackURL: "/auth/google/callback"
},
(accessToken, refreshToken, profile, done) => {
console.log('accessToken', accessToken);
console.log('refreshToken', refreshToken);
console.log('profile', profile);
console.log('done', done);
}
)
);複製程式碼
- index.js
const express = require("express"); const app = express(); require('./services/passport'); require('./routes/authRoutes')(app); app.get("/", (req, res) => { res.send({ hi: "there" }); }); ) const PORT = process.env.PORT || 5000; app.listen(PORT);複製程式碼