NodeJS 實現基於 token 的認證應用

此段摘自html

英文原文前端

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

 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 的認證在解決棘手的問題時有幾個優點:git

  • 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 認證的理論和架構就說到這裏。下面上實例。angularjs

這段原本想本身寫,不過本身寫也這些內容,節省點時間github

jwt加密和解密

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

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

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

 2.jpg

nodejs實現的jwt代碼json

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更簡單的接口

若是之後再提供生成器呢?

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

補一下

相關文章
相關標籤/搜索