NodeJS 實現基於 token 的認證應用


此段摘自

英文原文

在討論了關於基於 token 認證的一些基礎知識后,我們接下來看一個實例。看一下下面的幾點,然后我們會仔細的分析它:

 1.jpg

  1. 多個終端,比如一個 web 應用,一個移動端等向 API 發送特定的請求。
  2. 類似 https://api.yourexampleapp.com 這樣的請求發送到服務層。如果很多人使用了這個應用,需要多個服務器來響應這些請求操作。
  3. 這時,負載均衡被用於平衡請求,目的是達到最優化的后端應用服務。當你向 https://api.yourexampleapp.com 發送請求,最外層的負載均衡會處理這個請求,然后重定向到指定的服務器。
  4. 一個應用可能會被部署到多個服務器上(server-1, server-2, …, server-n)。當有請求發送到https://api.yourexampleapp.com 時,后端的應用會攔截這個請求頭部並且從認證頭部中提取到 token 信息。使用這個 token 查詢數據庫。如果這個 token 有效並且有請求終端數據所必須的許可時,請求會繼續。如果無效,會返回 403 狀態碼(表明一個拒絕的狀態)。

基於 token 的認證在解決棘手的問題時有幾個優勢:

  • Client Independent Services 。在基於 token 的認證,token 通過請求頭傳輸,而不是把認證信息存儲在 session 或者 cookie 中。這意味着無狀態。你可以從任意一種可以發送 HTTP 請求的終端向服務器發送請求。
  • CDN 。在絕大多數現在的應用中,view 在后端渲染,HTML 內容被返回給瀏覽器。前端邏輯依賴后端代碼。這中依賴真的沒必要。而且,帶來了幾個問題。比如,你和一個設計機構合作,設計師幫你完成了前端的 HTML,CSS 和 JavaScript,你需要拿到前端代碼並且把它移植到你的后端代碼中,目的當然是為了渲染。修改幾次后,你渲染的 HTML 內容可能和設計師完成的代碼有了很大的不同。在基於 token 的認證中,你可以開發完全獨立於后端代碼的前端項目。后端代碼會返回一個 JSON 而不是渲染 HTML,並且你可以把最小化,壓縮過的代碼放到 CDN 上。當你訪問 web 頁面,HTML 內容由 CDN 提供服務,並且頁面內容是通過使用認證頭部的 token 的 API 服務所填充。
  • No Cookie-Session (or No CSRF) 。CSRF 是當代 web 安全中一處痛點,因為它不會去檢查一個請求來源是否可信。為了解決這個問題,一個 token 池被用在每次表單請求時發送相關的 token。在基於 token 的認證中,已經有一個 token 應用在認證頭部,並且 CSRF 不包含那個信息。
  • Persistent Token Store 。當在應用中進行 session 的讀,寫或者刪除操作時,會有一個文件操作發生在操作系統的temp 文件夾下,至少在第一次時。假設有多台服務器並且 session 在第一台服務上創建。當你再次發送請求並且這個請求落在另一台服務器上,session 信息並不存在並且會獲得一個“未認證”的響應。我知道,你可以通過一個粘性 session 解決這個問題。然而,在基於 token 的認證中,這個問題很自然就被解決了。沒有粘性 session 的問題,因為在每個發送到服務器的請求中這個請求的 token 都會被攔截。

這些就是基於 token 的認證和通信中最明顯的優勢。基於 token 認證的理論和架構就說到這里。下面上實例。

這段本來想自己寫,不過自己寫也這些內容,節省點時間

jwt加密和解密

JWT 代表 JSON Web Token ,它是一種用於認證頭部的 token 格式。這個 token 幫你實現了在兩個系統之間以一種安全的方式傳遞信息。出於教學目的,我們暫且把 JWT 作為“不記名 token”。一個不記名 token 包含了三部分:header,payload,signature。

header 是 token 的一部分,用來存放 token 的類型和編碼方式,通常是使用 base-64 編碼。

payload 包含了信息。你可以存放任一種信息,比如用戶信息,產品信息等。它們都是使用 base-64 編碼方式進行存儲。 signature 包括了 header,payload 和密鑰的混合體。密鑰必須安全地保存儲在服務端。

 2.jpg

nodejs實現的jwt代碼

http://github.com/auth0/node-jsonwebtoken

主要3個方法

  • jwt.sign
  • jwt.verify
  • jwt.decode

需要小心的密鑰在多線程或集群下的處理。

加解密一個對象的時間,遠遠比查詢數據庫的代價小,唯一可能有的是token有效期的校驗,代價極其小。

優雅之寫法

授權獲取token

在app/routes/api/index.js里

// auth
router.post('/auth', function(req, res, next) {
  User.one({username: req.body.username},function(err, user){
    if (err) throw err;
    console.log(user);

    if (!user) {
        res.json({ success: false, message: '認證失敗,用戶名找不到' });
    } else if (user) {

      // 檢查密碼
      if (user.password != req.body.password) {
          res.json({ success: false, message: '認證失敗,密碼錯誤' });
      } else {
        // 創建token
        var token = jwt.sign(user, 'app.get(superSecret)', {
            'expiresInMinutes': 1440 // 設置過期時間
        });

        // json格式返回token
        res.json({
            success: true,
            message: 'Enjoy your token!',
            token: token
        });
      }
    }
  });
});

測試

curl -d "username=sang&password=000000" http://127.0.0.1:3019/api/auth

返回

{"success":true,"message":"Enjoy your token!","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NTc4MzJkZjk0ZTFjN2YyMDJmYTVlNGUiLCJ1c2VybmFtZSI6InNhbmciLCJwYXNzd29yZCI6IjAwMDAwMCIsImF2YXRhciI6IiIsInBob25lX251bWJlciI6IiIsImFkZHJlc3MiOiIiLCJfX3YiOjB9.Wv5za6GpJSMi346o625_8FxfoM4dJ1cWNuezG10zQG4"}%

路由處理

app/routes/api/groups.js

var express = require('express');
var router = express.Router();

var $ = require('../../controllers/groups_controller');
var $middlewares = require('mount-middlewares');

router.get('/list', $middlewares.check_api_token, $.api.list);

module.exports = router;

核心代碼

router.get('/list', $middlewares.check_api_token, $.api.list);

 

說明

  • 使用了$middlewares.check_api_token中間件
  • 核心業務邏輯在$.api.list
  • 和其他的express路由用法一樣,無他

中間件$middlewares.check_api_token

/*!
 * Moajs Middle
 * Copyright(c) 2015-2019 Alfred Sang <shiren1118@126.com>
 * MIT Licensed
 */

var jwt = require('jsonwebtoken');//用來創建和確認用戶信息摘要
// 檢查用戶會話
module.exports = function(req, res, next) {
  console.log('檢查post的信息或者url查詢參數或者頭信息');
  //檢查post的信息或者url查詢參數或者頭信息
  var token = req.body.token || req.query.token || req.headers['x-access-token'];
  // 解析 token
  if (token) {
    // 確認token
    jwt.verify(token, 'app.get(superSecret)', function(err, decoded) {
      if (err) {
        return res.json({ success: false, message: 'token信息錯誤.' });
      } else {
        // 如果沒問題就把解碼后的信息保存到請求中,供后面的路由使用
        req.api_user = decoded;
        console.dir(req.api_user);
        next();
      }
    });
  } else {
    // 如果沒有token,則返回錯誤
    return res.status(403).send({
        success: false,
        message: '沒有提供token!'
    });
  }
};

這個很容易解釋,只要參數有token或者頭信息里有x-access-token,我們就認定它是一個api接口,

校驗通過了,就把token的decode對象,也就是之前加密的用戶對象返回來,保存為req.api_user

業務代碼

app/controllers/groups_controller.js

exports.api = {
  list: function (req, res, next) {
    console.log(req.method + ' /groups => list, query: ' + JSON.stringify(req.query));

    var user_id = req.api_user._id;

    Group.query({ower_id: user_id}, function(err, groups){
      console.log(groups);
      res.json({
        data:{
          groups : groups
        },
        status:{
          code  : 0,
          msg   : 'success'
        }
      })
    });
  }
}

讓scaffold生成代碼和api共存,清晰明了

說明一下

  • req.api_user是$middlewares.check_api_token里賦值的
  • 寫一個下查詢接口,返回json即可

測試接口

然后讓我們來測試一下

curl http://127.0.0.1:3019/api/groups/list\?token\=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NTc4MzJkZjk0ZTFjN2YyMDJmYTVlNGUiLCJ1c2VybmFtZSI6InNhbmciLCJwYXNzd29yZCI6IjAwMDAwMCIsImF2YXRhciI6IiIsInBob25lX251bWJlciI6IiIsImFkZHJlc3MiOiIiLCJfX3YiOjB9.Wv5za6GpJSMi346o625_8FxfoM4dJ1cWNuezG10zQG4 {"data":{"groups":[{"_id":"557d32a282f9ddcc76a540e8","name":"sjkljkl","desc":"2323","ower_id":"557832df94e1c7f202fa5e4e","users":"","is_public":"","__v":0},{"_id":"557d32b082f9ddcc76a540e9","name":"sjkljkl","desc":"2323","ower_id":"557832df94e1c7f202fa5e4e","users":"","is_public":"","__v":0},{"_id":"557d32f082f9ddcc76a540ea","name":"sjkljkl","desc":"2323","ower_id":"557832df94e1c7f202fa5e4e","users":"","is_public":"","__v":0},{"_id":"557d33804f5905de78e1c25a","name":"sjkljkl","desc":"2323","ower_id":"557832df94e1c7f202fa5e4e","users":"","is_public":"","__v":0},{"_id":"557d33984f5905de78e1c25b","name":"anan","desc":"2323","ower_id":"557832df94e1c7f202fa5e4e","users":"2323","is_public":"232","__v":0}]},"status":{"code":0,"msg":"success"}}

模型,查詢以及其他

模型,查詢以及其他,沿用之前的東西,仍然以mongoosedao為主

  • one
  • all
  • query

基本上夠用了

如果還想玩的更high一點,可以增加一個service層,把多個model的操作放到里面。

總結

以后寫api,可以這樣玩

  1. app/routes/api/目錄下建立對應的api文件,比如groups.js,topics.js,users.js等

  2. 然后在對應的controller里,增加

exports.api = {
  aa:function(req, res, next){
    var user_id = req.api_user._id;
  },
  bb:function(req, res, next){
    var user_id = req.api_user._id;
  }
}
  1. 簡單寫點模型的查詢方法就可以了

是不是很簡單?

  • 使用mount-routes自動掛載routes
  • 使用mongoosedao更簡單的接口

如果以后再提供生成器呢?

想想就很美好,美好就繼續美好吧~

補一下


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM