node實現基於token的身份驗證

jiangxueyang發表於2019-03-04

最近研究了下基於token的身份驗證,並將這種機制整合在個人專案中。現在很多網站的認證方式都從傳統的seesion+cookie轉向token校驗。對比傳統的校驗方式,token確實有更好的擴充套件性與安全性。

傳統的session+cookie身份驗證

由於HTTP是無狀態的,它並不記錄使用者的身份。使用者將賬號與密碼傳送給伺服器後,後臺通過校驗,但是並沒有記錄狀態,於是下一次使用者的請求仍然需要校驗身份。為了解決這一問題,需要在服務端生成一條包含使用者身份的記錄,也就是session,再將這條記錄傳送給使用者並儲存在使用者本地,即cookie。接下來使用者的請求都會帶上這條cookie,若客戶端的cookie與服務端的session能對應上,則說明使用者身份驗證通過。

token身份校驗

流程大致如下:

  • 第一次請求時,使用者傳送賬號與密碼
  • 後臺校驗通過,則會生成一個有時效性的token,再將此token傳送給使用者
  • 使用者獲得token後,將此token儲存在本地,一般儲存在localstorage或cookie
  • 之後的每次請求都會將此token新增在請求頭裡,所有需要校驗身份的介面都會被校驗token,若token解析後的資料包含使用者身份資訊,則身份驗證通過。

對比傳統的校驗方式,token校驗有如下優勢:

  • 在基於token的認證,token通過請求頭傳輸,而不是把認證資訊儲存在session或者cookie中。這意味著無狀態。你可以從任意一種可以傳送HTTP請求的終端向伺服器傳送請求。
  • 可以避免CSRF攻擊
  • 當在應用中進行 session的讀,寫或者刪除操作時,會有一個檔案操作發生在作業系統的temp 資料夾下,至少在第一次時。假設有多臺伺服器並且 session 在第一臺服務上建立。當你再次傳送請求並且這個請求落在另一臺伺服器上,session 資訊並不存在並且會獲得一個“未認證”的響應。我知道,你可以通過一個粘性 session 解決這個問題。然而,在基於 token 的認證中,這個問題很自然就被解決了。沒有粘性 session 的問題,因為在每個傳送到伺服器的請求中這個請求的 token 都會被攔截。

下面介紹一下利用node+jwt(jwt教程)搭建簡易的token身份校驗

示例

當使用者第一次登入時,提交賬號與密碼至伺服器,伺服器校驗通過,則生成對應的token,程式碼如下:

const fs = require(`fs`);
const path = require(`path`);
const jwt = require(`jsonwebtoken`);
//生成token的方法
function  generateToken(data){
    let created = Math.floor(Date.now() / 1000);
    let cert = fs.readFileSync(path.join(__dirname, `../config/pri.pem`));//私鑰
    let token = jwt.sign({
        data,
        exp: created + 3600 * 24
    }, cert, {algorithm: `RS256`});
    return token;
}

//登入介面
router.post(`/oa/login`, async (ctx, next) => {
    let data = ctx.request.body;
    let {name, password} = data;
    let sql = `SELECT uid FROM t_user WHERE name=? and password=? and is_delete=0`, value = [name, md5(password)];
    await db.query(sql, value).then(res => {
        if (res && res.length > 0) {
            let val = res[0];
            let uid = val[`uid`];
            let token = generateToken({uid});
            ctx.body = {
                ...Tips[0], data: {token}
            }
        } else {
            ctx.body = Tips[1006];
        }
    }).catch(e => {
        ctx.body = Tips[1002];
    });
    
});
複製程式碼

使用者通過校驗將獲取到的token存放在本地:

store.set(`loginedtoken`,token);//store為外掛
複製程式碼

之後客戶端請求需要驗證身份的介面,都會將token放在請求頭裡傳遞給服務端:

service.interceptors.request.use(config => {
    let params = config.params || {};
    let loginedtoken = store.get(`loginedtoken`);
    let time = Date.now();
    let {headers} = config;
    headers = {...headers,loginedtoken};
    params = {...params,_:time};
    config = {...config,params,headers};
    return config;
}, error => {
    Promise.reject(error);
})
複製程式碼

服務端對所有需要登入的介面均攔截token並校驗合法性。

function verifyToken(token){
    let cert = fs.readFileSync(path.join(__dirname, `../config/pub.pem`));//公鑰
    try{
        let result = jwt.verify(token, cert, {algorithms: [`RS256`]}) || {};
        let {exp = 0} = result,current = Math.floor(Date.now()/1000);
        if(current <= exp){
            res = result.data || {};
        }
    }catch(e){
    
    }
    return res;
    
}

app.use(async(ctx, next) => {
    let {url = ``} = ctx;
    if(url.indexOf(`/user/`) > -1){//需要校驗登入態
        let header = ctx.request.header;
        let {loginedtoken} = header;
        if (loginedtoken) {
            let result = verifyToken(loginedtoken);
            let {uid} = result;
            if(uid){
                ctx.state = {uid};
                await next();
            }else{
                return ctx.body = Tips[1005];
            }
        } else {
            return ctx.body = Tips[1005];
        }
    }else{
        await next();
    }
});
複製程式碼

本示例使用的公鑰與私鑰可自己生成,操作如下:

  1. 開啟命令列工具,輸入openssl,開啟openssl;
  2. 生成私鑰:genrsa -out rsa_private_key.pem 2048
  3. 生成公鑰: rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem

閱讀原文
點此檢視node後臺程式碼

點此檢視前端程式碼

相關文章