JWT、OAuth 2.0、session 用戶受權實戰

在不少應用中,咱們都須要向服務端提供本身的身份憑證來得到訪問一些非公開資源的受權。好比在一個博客平臺,咱們要修改本身的博客,那麼服務端要求咱們可以證實 「我是我」 ,纔會容許咱們修改本身的博客。

爲用戶提供受權以容許用戶操做非公開資源,有不少種方式。好比使用 token、session、cookie,還有容許第三方登陸受權的 OAuth 2.0.前端

爲了理解這些技術的機制和它們之間的關係,本文就來一一使用這些方案實現一個前端經過後端驗證受權來訪問後端服務的應用。ios

咱們將用 express 搭建一個簡單的後端,爲了保存用戶信息,咱們使用 mongoDB。前端是一個註冊頁面和一個登陸頁面,此外還有一個修改用戶密碼的頁面,在這個頁面上修改密碼的操做只有在用戶登陸以後才被容許,也就是被服務端受權以後才能修改密碼,不然返回 401 未受權。git

下面就是咱們這個簡單 demo 的文件結構:github

服務端結構:web

前端頁面結構:redis

如上圖,咱們在服務端寫了4個路由分別用於用戶註冊、登陸、修改密碼、和登出。其中在登陸路由中,用戶登陸以後將會生成一個用戶憑證,在後續修改密碼的路由中將會利用這個憑證來受權用戶修改密碼。具體的代碼根據不一樣的受權方案而有所不一樣。前端相應地分爲註冊、登陸、修改密碼 3 個頁面:算法

註冊頁面:
數據庫

登陸頁面:
express

修改密碼頁面:
npm

咱們最終實現的效果就是:
(GIF圖過大,能夠轉到GitHub項目地址查看:地址)

搭建起一個先後端分離的應用框架以後,咱們下面依次使用 token、OAuth 2.0、express-session 來實現用戶受權。

1. 使用 session 受權

1.1 session 原理:

利用 session 來驗證用戶,有兩種機制實現。

  1. 須要服務端在用戶登陸成功後生成一個 session ID 保存在服務端,這個session ID 標識當前會話的用戶,之後用戶的每一次請求中都會包含session ID,服務端能夠識別這個 session ID 驗證用戶身份而後纔會受權。
  2. 把 session ID 和其餘數據加密後發給用戶,由用戶來存儲並在之後每次請求中發給服務端來驗證。好比能夠用 cookie 存儲發送,也可使用其餘客戶端存儲。

1.2 express-session API:

本文使用 express-session 來實現。而且使用上述 session 的第一種機制。因此先來看一下 express-session 主要的 API:

  • session( options ):生成 session 中間件,使用這個中間件會在當前會話中建立 session,session 數據將會被保存在服務端,而 session ID 會保存在 cookie。options 爲傳入的配置參數,有如下這些參數:

    1.  cookie:
           存儲 session ID,
           默認值 { path: ‘/‘, httpOnly: true,secure: false, maxAge: null })
    2.  genid:
           一個函數,返回一個字符串用來做爲新的 session ID,傳入 req 能夠按需在 req 上添加一些值。
    3.  name:
           存儲 session ID 的 cookie 的名字,默認是'connect.sid',可是若是有多個使用 express-session 的 app 運行在同一個服務器主機上,須要用不一樣的名字命名  express-session 的 cookie。
    4.  proxy :
           當設置了secure cookies(經過」x-forwarded-proto」 header )時信任反向代理。
    5.  resave:
           強制保存會話,即便會話在請求期間從未被修改過
    6.  rolling:
           強制在每次響應時,都設置保存會話標識符的cookie。cookie 到期時間會被重置爲原始時間 maxAge。默認值爲`false`。
    7.  saveUninitialized:
           默認 `true`, 強制存儲未初始化的 session。
    8.  secret ( 必需 ):
           用來對session ID cookie簽名,能夠提供一個單獨的字符串做爲 secret,也能夠提供一個字符串數組,此時只有第一個字符串才被用於簽名,可是在 express-session 驗證 session ID   的時候會考慮所有字符串。 
    9.  store:
           存儲 session 的實例。
    10. unset:
           控制 req.session 是否取消。默認是 `keep`,若是是  `destroy`,那麼 session 就會在響應結束後被終止。
  • req.session:這是 express-session 存放 session 數據的地方,注意,只有 session ID 存儲在 cookie,因此 express-session 會自動檢查 cookie 中的 session ID ,並用這個 session ID 來映射到對應的 session 數據,因此使用 express-session 時咱們只需讀取 req.session ,express-session 知道應該讀取哪一個 session ID 標識的 session 數據。

    1. 能夠從 req.session 讀取 session :
           req.session.id:每個 session 都有一個惟一ID來標識,能夠讀取這個ID,並且只讀不可更改,這是 req.sessionID 的別名;
           req.session.cookie:每個 session 都有一個惟一 的cookie來存儲 session ID,能夠經過 req.session.cookie 來設置 cookie 的配置項,好比 req.session.cookie.expires 設置爲 false ,設置 req.session.cookie.maxAge 爲某個時間。
    2. req.session 提供了這些方法來操做 session:
           req.session.regenerate( callback (err) ): 生成一個新的 session, 而後調用 callback;
           req.session.destroy( callback (err) ): 銷燬 session,而後調用 callback;
           req.session.reload( callback (err) ):  從 store 重載 session 並填充 req.session ,而後調用 callback;
           req.session.save( callback (err) ): 將 session 保存到 store,而後調用 callback。這個是在每次響應完以後自動調用的,若是 session 有被修改,那麼 store 中將會保存新的 session;
           req.session.touch(): 用來更新 maxAge。
  • req.sessionID:和 req.session.id 同樣。
  • store:若是配置這個參數,能夠將 session 存儲到 redis和mangodb 。一個使用 rtedis 存儲 session 的例子。store 提供了一下方法來操做 store:

    1. store.all( callback (error, sessions) ) :
           返回一個存儲store的數組;
    2. store.destroy(sid, callback(error)):
           用session ID 來銷燬 session;
    3. store.clear(callback(error)):
           刪除全部 session
    4. store.length(callback(error, len)):
           獲取 store 中全部的 session 的數目
    5. store.get(sid, callbackcallback(error, session)):
           根據所給的 ID 獲取一個 session
    6. store.set(sid, session, callback(error)):
           設置一個 session。
    7. store.touch(sid, session, callback(error)):
            更新一個 session

以上就是 express-session 的所有 API。

1.3 使用 express-session

重點中的重點,巨坑中的巨坑:使用 express-session 是依賴於 cookie 來存儲 session ID 的,而 session ID 用來惟一標識一個會話,若是要在一個會話中驗證當前會話的用戶,那麼就要求用戶前端可以發送 cookie,並且後端可以接收 cookie。因此前端咱們設置 axios 的 withCredentials = true 來設置 axios 能夠發送 cookie,後端咱們須要設置響應頭 Access-Control-Allow-Credentials:true,而且同時設置 Access-Control-Allow-Origin 爲前端頁面的服務器地址,而不能是 * 。咱們能夠用 cors 中間件代替設置:

// 跨域

app.use(cors({
  credentials:  true,
  origin:  'http://localhost:8082',  // web前端服務器地址,,不能設置爲 * 

}))

我開始就是由於沒有設置這個,因此遇到了問題,就是後端登陸接口在session中保存 用戶名( req.session.username = req.body.username) 以後,在修改用戶密碼的接口須要讀取 req.session.username 以驗證用戶的時候讀取不到 req.session.username ,很明顯兩個接口的 req.session 不是同一個 session,果真 console 出來 的 session ID 是不一樣的。這就讓我想到了 cookie,cookie 是生成以後每次請求都會帶上而且後端能夠訪問的,如今存儲在 cookie 中的 session ID 沒有被讀取到而是讀取到了新 session ID,因此問題就出在後端不能拿到 cookie,也有多是由於前端發送不出去 cookie。但是開始的時候搜索關於 session ID 讀取不一致的這個問題我找不到解決辦法,並且發現不少人存在一樣的問題,可是沒有人給出答案,如今經過本身的思考想到了解決辦法,這是不少人須要避免的巨坑。

如今跨過了最大的一個坑,咱們就能夠來編寫先後端全部的邏輯了。關於註冊的邏輯,是一個很簡單的用戶註冊信息填寫頁面,它發送用戶的名字和密碼到後端註冊接口,後端註冊接口保存用戶的名字和密碼到數據庫理。所以我在這裏省略掉前端註冊頁面和後端註冊接口,只講前端登陸頁面和後端登陸接口,前端修改密碼頁面和後端修改密碼接口和登出接口。

    1. 前端登陸接口:
async function login(){ // 登陸
         
        let res = await axios.post('http://localhost:3002/login',{username,password})
        if(res.data.code === 0){
            setLoginSeccess(true)
            alert('登陸成功,請修改密碼')
            
        }else if(res.data.code === 2){
            alert('密碼不正確')
            return
        }else if(res.data.code === 1){
            alert('沒有該用戶')
            return
        }
    }
    1. 後端登陸接口:
const getModel = require('../db').getModel
const router = require('express').Router()
const users = getModel('users')

router.post('/', (req,res,next)=>{
    let {username, password} = req.body
    users.findOne({username},(err,olduser)=>{
        if(!olduser){
            res.send({code:1})// 沒有該用戶
        }else{
            if(olduser.password === password){// 登錄成功,生成 session
                req.session.username = olduser.username
                req.session.userID = olduser._id
                console.log('登陸時的會話 ID:',req.sessionID)
                req.session.save()
                res.send({code:0})// 登陸成功
            }else{

                res.send({code:2}) // 密碼錯誤
            }
        }
    })
})

module.exports = router
    1. 前端修改密碼和登出頁面:
// src/axios.config.js:

// 支持 express-session 的 axios 配置
export function axios_session(){
    axios.defaults.withCredentials = true
    return axios
}
async function modify(){ // 修改密碼
       if(!input.current.value) return alert('請輸入新密碼')
       try{
           // 支持 session 的 axios 調用
           let res = await axios_session().post('http://localhost:3002/modify',{newPassword:input.current.value})
           if(res.data.code === 0)
               alert('密碼修改爲功')
       }catch(err){
           alert('沒有受權 401')  
           console.log(err)
       }
}
async function logout(){ // 登出
        let res = await axios.post('http://localhost:3002/logout')
        if(res.data.code === 0){
            history.back()
        }
}
    1. 後端修改密碼接口:
const getModel = require('../db').getModel
const router = require('express').Router()
const users = getModel('users')
const sessionAuth = require('../middlewere/sessionAuth') 

router.post('/', sessionAuth, (req,res,next)=>{
    let {newPassword} = req.body
    console.log('修改密碼時的會話 ID:',req.session.id)
    if(req.session.username){
        users.findOne({username: req.session.username},(err,olduser)=>{
            olduser.password = newPassword
            olduser.save(err=>{
                if(!err){
                    res.send({code:0})// 修改密碼成功
                }
            })
        })
    }
})

module.exports = router

sessionAuth 驗證中間件:

const sessionAuth = (req,res,next)=>{
    if(req.session && req.session.username){// 驗證用戶成功則進入下一個中間件來修改密碼
        next()
    }else{// 驗證失敗返回 401
        res.sendStatus(401)
    }
}

module.exports = sessionAuth
    1. 後端登出:
const router = require('express').Router()
 
router.post('/', (req,res,next)=>{
    req.session.destroy(()=>console.log('銷燬session,已經推出登陸'))
    res.send({code:0})
})

module.exports = router

咱們還須要調用 session 的中間件,配置一些參數,才能在以後的中間件中使用 req.session 來進行存儲、讀取和銷燬 session 的操做:

// server/app.js:

// session
app.use(session({
    secret: '123456789',// 必需,用來簽名 session
    unset:'destroy',// 在每次會話就熟後銷燬 session
    resave:true,
    saveUninitialized:false,
    rolling:true,
    cookie:{
        maxAge:60*60*1000// session ID 有效時間
    }

}))

2. 使用 JWT 受權

2.1 JWT 的原理:

首先來看看 JWT 的概念,JWT 的 token 由 頭部(head)、數據(payload)、簽名(signature) 3個部分組成 具體每一個部分的結構組成以及JWT更深的講解能夠看看這個。其中頭部(header)和數據(payload)通過 base64 編碼後通過祕鑰 secret的簽名,就生成了第三部分----簽名(signature) ,最後將 base64 編碼的 header 和 payload 以及 signature 這3個部分用圓點 . 鏈接起來就生成了最終的 token。

signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
  token = base64UrlEncode(header) + "." + base64UrlEncode(payload) + signature

token 生成以後,能夠將其發送給客戶端,由客戶端來存儲並在之後每次請求中發送會後端用於驗證用戶。前端存儲和發送 token 的方式有如下兩種:

2.1.1 使用 Header.Authorization + localStorage 存儲和發送 token

在 localStorage 中存儲 token,經過請求頭 Header 的 Authorization 字段將 token發送給後端。

這種方法能夠避免 CSRF 攻擊,由於沒有使用 cookie ,在 cookie 中沒有 token,而 CSRF 就是基於 cookie 來攻擊的。雖然沒有 CSRF ,可是這種方法容易被 XSS 攻擊,由於 XSS 能夠攻擊 localStorage ,從中讀取到 token,若是 token 中的 head 和 payload 部分沒有加密,那麼攻擊者只要將 head 和 payload 的 base64 形式解碼出來就能夠看到head 和payload 的明文了。這個時候,若是 payload 保護敏感信息,咱們能夠加密 payload。

2.1.2 使用 cookie 存儲和發送 token:

在這種狀況下,咱們須要使用 httpOnly 來使客戶端腳本沒法訪問到 cookie,才能保證 token 安全。這樣就避免了 CSRF 攻擊。

2.2 使用 jsonwebtoken 來實現 JWT 用戶受權:

jsonwebtoken 主要 API:

1. jwt.sign(payload, secretOrPrivateKey, [options, callback]) 用於簽發 token

若是有 callback 將異步的簽名 token。

payload 就是咱們要在 token 上裝載的數據,好比咱們能夠在上面添加用戶ID,用於數據庫查詢。payload能夠是一個object, buffer或者string,payload 若是是 object,能夠在裏面設置 exp 過時時間。

secretOrPrivateKey 即包含HMAC算法的密鑰或RSA和ECDSA的PEM編碼私鑰的string或buffer,是咱們用於簽名 token 的密鑰,secretOrPublicKey 應該和下面 的 jwt.verify 的 secretOrPublicKey 一致。

options 的參數有:

1)algorithm (default: HS256) 簽名算法,這個算法和下面將要講的 jwt.verify 所用的算法一個一致
  2)expiresIn: 以秒錶示或描述時間跨度zeit / ms的字符串。如60,"2 days","10h","7d",含義是:過時時間
  3)notBefore: 以秒錶示或描述時間跨度zeit / ms的字符串。如:60,"2days","10h","7d"
  4)audience:Audience,觀衆
  5)issuer: Issuer,發行者
  6)jwtid: JWT ID
  7)subject: Subject,主題
  8)noTimestamp:
  9)header
  10)keyid
  11)mutatePayload

2. jwt.verify(token, secretOrPublicKey, [options, callback]) 用於驗證 token

若是有 callback 將異步的驗證 token。

token 即是咱們保存在前端的token,咱們將它發送給後端,後端調用 jwt.verify 並接受 token 和傳入放在後端的 secretOrPublicKey 來驗證 token。注意這裏的 secretOrPublicKey 與以前用於簽發 token 的 secretOrPublicKey 應該是同一個。

options 的參數有:

1)algorithms: 一個包含簽名算法的數組,好比  ["HS256", "HS384"].
  2)audience: if you want to check audience (aud), provide a value here. The audience can be checked against a string, a regular expression or a list of strings and/or regular expressions.
  Eg: "urn:foo", /urn:f[o]{2}/, [/urn:f[o]{2}/, "urn:bar"]

  3)complete: return an object with the decoded { payload, header, signature } instead of only the usual content of the payload.
  4)issuer (optional): string or array of strings of valid values for the iss field.
  5)ignoreExpiration: if true do not validate the expiration of the token.
  6)ignoreNotBefore...
  7)subject: if you want to check subject (sub), provide a value here
  8)clockTolerance: number of seconds to tolerate when checking the nbf and exp claims, to deal with small clock differences among different servers
  9)maxAge: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span zeit/ms.
  Eg: 1000, "2 days", "10h", "7d". A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default ("120" is equal to "120ms").

  10)clockTimestamp: the time in seconds that should be used as the current time for all necessary comparisons.
  11)nonce: if you want to check nonce claim, provide a string value here. It is used on Open ID for the ID Tokens. (Open ID implementation notes)

3. jwt.decode(token [, options]) 解碼 token

只是解碼 token 中的 payload,不會驗證 token。
options 參數有:

1)json: 強制在 payload 用JSON.parse 序列化,即便頭部沒有聲明 "typ":"JWT"  
  2)complete: true 則返回解碼後的包含  payload 和 header  的對象.

4. 錯誤碼

在驗證 token 的過程當中可能或拋出錯誤,jwt.verify() 的回調的第一個參數就是 err,err 對象有一下幾種類型:

  1. TokenExpiredError:
err = {
        name: 'TokenExpiredError',
        message: 'jwt expired',
        expiredAt: 1408621000
      }
  1. JsonWebTokenError:
err = {
        name: 'JsonWebTokenError',
        message: 'jwt malformed'
        /*
          message 有如下幾個可能的值:
            'jwt malformed'
            'jwt signature is required'
            'invalid signature'
            'jwt audience invalid. expected: [OPTIONS AUDIENCE]'
            'jwt issuer invalid. expected: [OPTIONS ISSUER]'
            'jwt id invalid. expected: [OPTIONS JWT ID]'
            'jwt subject invalid. expected: [OPTIONS SUBJECT]'
      */
      }
  1. NotBeforeError:
err = {
        name: 'NotBeforeError',
        message: 'jwt not active',
        date: 2018-10-04T16:10:44.000Z
      }

5. jsonwebtoken 的簽名算法

HS25六、HS38四、HS5十二、RS256 等。

2.3 開始使用 jsonwebtoken:

後端登陸接口如今須要改用 JWT 來簽發 token,把原來使用 express-session 的代碼去掉:

if(olduser.password === password){// 密碼正確
 
                /*
                
                // 受權方法 1. session 
                req.session.username = olduser.username
                req.session.userID = olduser._id
                console.log('登陸時的會話 ID:',req.sessionID)
                req.session.cookie.maxAge = 60*60*1000
                req.session.save()
                res.send({code:0})// 登陸成功

                */

                // 受權方法 2. JWT
                let token = JWT.sign(
                    {username:olduser.username, exp:Date.now() + 1000 * 60}, // payload
                    secret, // 簽名密鑰
                    {algorithm} // 簽名算法
                )
                res.send({
                    code:0,
                    token
                })
                
            }else{

                res.send({code:2}) // 密碼錯誤
            }

後端給前端發回了 token,前端須要存儲 token 以便於後續請求受權,能夠存儲在 localStorage ,在修改密碼頁面再取出 localStorage 中 的 token,並再 axios 發送請求以前攔截請求,在請求頭的 Authorization 中帶上 token:

前端存儲 token:

// src/pages/login.js:

alert('登陸成功,請修改密碼')
localStorage.setItem('token',res.data.token)

前端攔截 axios 請求,從 localStorage 中取出保存好的 token,在請求頭帶上 token:

// src/axios.config.js:

// 支持 JWT 的 axios 配置
export  function axios_JWT(){
    axios.interceptors.request.use(config => {
        // 在 localStorage 獲取 token
        let token = localStorage.getItem("token");
        console.log('axios配置:token',token)
        // 若是存在則設置請求頭
        if (token) {
            config.headers['Authorization'] = token;
            console.log(config)
        }
        return config;
    });
    return axios
}

前端修改密碼頁面調用能夠攔截請求的 aios 來發送修改密碼的請求:

// src/pages/ModifyUserInfo.js:

 // 支持 JWT 的 axios 調用
           let res = await axios_JWT().post('http://localhost:3002/modify',{newPassword:input.current.value})

後端修改密碼接口調用 JWT 的用戶認證中間件:

認證中間件:

const JWT = require('jsonwebtoken')
const secret = require('../server.config').JWT_config.secret
const algorithm = require('../server.config').JWT_config.algorithm


function JWT_auth(req,res,next){
    let authorization = req.headers["authorization"]
    console.log('authorization',authorization)
    if(authorization)
    try{
        let token = authorization;
        JWT.verify(token,secret,{algorithm:'HS256'},(err,decoded)=>{ // 用戶認證
            if(err){
                console.log(err)
                next(err)
            }else{
                console.log(decoded)
                req.username = decoded.username // 在 req 上添加 username,以便於傳到下一個中間件取出 username 來查詢數據庫
                next()
            }
        })

    }catch(err){
        res.status(401).send("未受權");
    }
    else
    res.status(401).send("未受權");
}
module.exports = JWT_auth

3. 使用 OAuth 2.0 受權:

3.1 OAuth 2.0 是什麼

有的應用會提供第三方應用登陸,好比掘金 web 客戶端提供了微信、QQ帳號登陸,咱們能夠不用註冊掘金帳號,而能夠用已有的微信帳號登陸掘金。看看用微信登陸掘金的過程:

step1: 打開掘金,未登陸狀態,點擊登陸,掘金給咱們彈出一個登陸框,上面有微信、QQ登陸選項,咱們選擇微信登陸;<br/>
step2: 以後掘金會將咱們重定向到微信的登陸頁面,這個頁面給出一個二維碼供咱們掃描,掃描以後;<br/>
step3: 咱們打開微信,掃描微信給的二維碼以後,微信詢問咱們是否贊成掘金使用咱們的微信帳號信息,咱們點擊贊成;<br/>
step4: 掘金剛纔重定向到微信的二維碼頁面,如今咱們贊成掘金使用咱們的微信帳號信息以後,又重定向回掘金的頁面,同時咱們能夠看到如今掘金的頁面上顯示咱們已經處於登陸狀態,因此咱們已經完成了用微信登陸掘金的過程。<br/>

這個過程比咱們註冊掘金後才能登陸要快捷多了。這歸功於 OAuth2.0 ,它容許客戶端應用(掘金)能夠訪問咱們的資源服務器(微信),咱們就是資源的擁有者,這須要咱們容許客戶端(掘金)可以經過認證服務器(在這裏指微信,認證服務器和資源服務器能夠分開也能夠是部署在同一個服務上)的認證。很明顯,OAuth 2.0 提供了4種角色,資源服務器、資源的擁有者、客戶端應用 和 認證服務器,它們之間的交流實現了 OAuth 2.0 整個認證受權的過程。

OAuth 2.0 登陸的原理,根據4中不一樣的模式有所不一樣。本文使用受權碼模式,因此只講受權碼模式下 OAuth2.0 的登陸過程,其餘模式能夠自行搜索學習。

3.2 使用 GitHub OAuth 來登陸咱們的項目客戶端

能夠參考GitHub 官網
下面咱們改用 OAuth2.0 來使用 GitHub 帳號來受權咱們上面的應用,從而修改咱們應用的密碼。

步驟:

  1. 在 GitHub 上申請註冊一個 OAuth application:https://github.com/settings/a...

填寫咱們的應用名稱、應用首頁和受權須要的回調 URL:

  1. 而後GitHub 生成了 Client ID 和 Client Secret:

  1. 以後咱們在咱們原有的登陸頁面增長一個使用 GitHub 帳號登陸的入口:

這個登陸入口其實就是一個指向 GitHub 登陸頁面的鏈接

<a href='https://github.com/login/oauth/authorize?client_id=211383cc22d28d9dac52'> 使用 GitHub 帳號登陸 </a>
  1. 用戶進入上面的 GitHub 登陸頁面以後,能夠輸入本身的GitHub用戶名和密碼登陸,而後 GitHub 會將受權碼以回調形式傳回以前咱們設置的 http://localhost:3002/login/callback 這個頁面上,好比 http://localhost:3002/login/callback?code=37646a38a7dc853c8a77,

咱們能夠在 http://localhost:3002/login/callback 這個路由獲取 code 受權碼,並結合咱們以前得到的 client-id、client_secret,向https://github.com/login/oaut...,token 獲取以後,咱們能夠用這個 token向 https://api.github.com/user?a... 請求到用戶的GitHub帳號信息好比GitHub用戶名、頭像等等。

// server/routes/login.js:

// 使用 OAuth2.0 時的登陸接口,
router.get('/callback',async (req,res,next)=>{//這是一個受權回調,用於獲取受權碼 code
    var code = req.query.code; // GitHub 回調傳回 code 受權碼
    console.log(code)
    
    // 帶着 受權碼code、client_id、client_secret 向 GitHub 認證服務器請求 token
    let res_token = await axios.post('https://github.com/login/oauth/access_token',
    {
        client_id:Auth_github.client_id,
        client_secret:Auth_github.client_secret,
        code:code
    })
   console.log(res_token.data)

   let token = res_token.data.split('=')[1].replace('&scope','')
   

   // 帶着 token 從 GitHub 獲取用戶信息
   let github_API_userInfo = await axios.get(`https://api.github.com/user?access_token=${token}`)
   console.log('github 用戶 API:',github_API_userInfo.data)

   let userInfo = github_API_userInfo.data

   // 用戶使用 GitHub 登陸後,在數據庫中存儲 GitHub 用戶名
   users.findOne({username:userInfo.name},(err,oldusers)=>{ // 看看用戶以前有沒有登陸過,沒有登陸就會在數據庫中新增 GitHub 用戶
    if(oldusers) {
        res.cookie('auth_token',res_token.data)
        res.cookie('userAvatar',userInfo.avatar_url)
        res.cookie('username',userInfo.name)

        res.redirect(301,'http://localhost:8082') // 從GitHub的登陸跳轉回咱們的客戶端頁面
        return
    }else
    new users({
        username:userInfo.name,
        password:'123', // 爲使用第三方登陸的可以用戶初始化一個密碼,後面用戶能夠本身去修改
    }).save((err,savedUser)=>{
        if(savedUser){
            res.cookie('auth_token',res_token.data)
            res.cookie('userAvatar',userInfo.avatar_url)
            res.cookie('username',userInfo.name)
         
            res.redirect(301,'http://localhost:8082') // 從GitHub的登陸跳轉回咱們的客戶端頁面
        }
    })
   })
},
)
module.exports = router

在請求到用戶的GitHub信息以後,咱們能夠將用戶頭像和用戶名存在cookie、裏,便於發送給前端在頁面上顯示出來,告訴用戶他已經用GitHub帳號登陸了咱們的客戶端。
同時,咱們把GitHub用戶名存到咱們本身的數據庫裏,並給一個‘123’簡單的初始化密碼,後面用戶能夠在得到權限後修改密碼。

  1. 接下來,咱們使用GitHub登陸後,咱們須要得到受權以修改咱們的密碼。

咱們使用和 JWT 同樣的發送token的方式,前面咱們從GitHub得到用戶的token以後有已經用cookie的方式將其發送給前端,咱們在前端能夠讀取cookie裏的token,而後將其經過 Authorization 頭方式給後端驗證:

前端讀取 token,並加到 Authorization 裏:

// OAuth2.0
    axios.interceptors.request.use(config => {
        // 在 localStorage 獲取 token
        let token = localStorage.getItem("token");
        console.log('axios配置:token',token)
        // 若是存在則設置請求頭
        if(document.cookie){
            let OAtuh_token = unescape(document.cookie.split(';').filter(e=>/auth_token/.test(e))[0].replace(/auth_token=/,''))
            config.headers['Authorization'] = OAtuh_token;
            console.log(config)
        }
       
        return config;
    });

後端驗證中間件 :

const axios = require('axios')

const OAuth=async (req,res,next)=>{
    let OAuth_token = req.headers["authorization"]
    console.log('authorization',OAuth_token)
    console.log('OAuth 中間件拿到cookie中的token:',OAuth_token)
    if(OAuth_token) {
        let token = OAuth_token.split('=')[1].replace('&scope','')
        let github_API_userInfo = await axios.get(`https://api.github.com/user?access_token=${token}`)
        let username = github_API_userInfo.data.name
        req.username = username
        next()
    }
    else res.status(401)
}
module.exports = OAuth

3.3 使用 GitHub OAuth2.0 登陸受權的效果圖:

GIF地址

總結

session、JWT、OAuth2.0 這三種受權方式每一種裏面都會有其餘方式的影子,主要是體如今用戶憑證的存儲和發送上,好比一般所說的基於服務端的 session,它能夠把用戶憑證,也就是 session ID 存儲在服務端(內存或者數據庫redis等),可是也是能夠發給前端經過cookie保存的。JWT 能夠把做爲用戶憑證的 token 在服務端簽發後發給用戶保存,能夠在 localStorage 保存,一樣也能夠保存在 cookie 。OAuth2.0是比較複雜的一種受權方式,可是它後面得到 token 後也能夠像 JWT 同樣處理 token 的保存和驗證來受權用戶。

不論是哪一種方式,都會有一些要注意的安全問題,還有性能上須要兼顧的地方。這裏有關這方面再也不贅述。

最後,本項目的地址:https://github.com/qumuchegi/auth-demo

相關文章
相關標籤/搜索