Node with React: Fullstack Web Development 課程手記(二)——Google OAuth

OAuth

OAuth是一個關於受權(authorization)的開放網絡標準,在全世界獲得普遍應用,目前的版本是2.0版。常見的採用微信、QQ、微博、Facebook、Google帳號登錄網站的過程都是採用了OAuth技術。這一章咱們會以使用Google帳號登錄第三方網站爲例,展現如何使用這項技術。node

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.02.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密鑰。若是想要看詳細步驟,參考這裏git

    • 這個流程裏須要注意的點是,設置JavaScript受權域和受權回調URL,由於咱們如今創建的是一個開發項目,二者分別設爲 http://localhost:5000和http://localhost:5000/*
    • clientID: 用於生成登錄用的URL
    • clientSecrect: 用於證實該APP是否有權訪問token
  • 接下來要把剛纔生成的clientID和clientSectrect,傳入Google OAuth模塊中。注意,clientSecrect不能公佈,謹慎起見clientID也應該保密。因此咱們不但願別人經過查看源代碼的形式獲取這兩個值。目前咱們先經過不提交這部分代碼的形式作到隱藏這部分信息。github

  • 建立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);複製代碼

next section數據庫

相關文章
相關標籤/搜索