Token認證前因後果

在Web領域基於Token的身份驗證隨處可見。在大多數使用Web API的互聯網公司中,tokens 是多用戶下處理認證的最佳方式。html

爲何要用 Token

  • Token 徹底由應用管理,因此它能夠避開同源策略
  • Token 能夠避免 CSRF 攻擊
  • Token 能夠是無狀態的,能夠在多個服務間共享

Token 是在服務端產生的。若是前端使用用戶名/密碼向服務端請求認證,服務端認證成功,那麼在服務端會返回 Token 給前端。前端能夠在每次請求的時候帶上 Token 證實本身的合法地位。若是這個 Token 在服務端持久化(好比存入數據庫),那它就是一個永久的身份令牌。前端

Token有效期

爲何要設置有效期?對於這個問題,咱們不妨先看兩個例子。一個例子是登陸密碼,通常要求按期改變密碼,以防止泄漏,因此密碼是有有效期的;另外一個例子是安全證書。SSL 安全證書都有有效期,目的是爲了解決吊銷的問題。因此不管是從安全的角度考慮,仍是從吊銷的角度考慮,Token 都須要設有效期。ajax

那麼有效期多長合適呢?只能說,根據系統的安全須要,儘量的短,但也不能短得離譜——想像一下手機的自動熄屏時間,若是設置爲 10 秒鐘無操做自動熄屏,再次點亮須要輸入密碼,會不會瘋?若是你以爲不會,那就親自試一試,設置成能夠設置的最短期,堅持一週就好(不排除有人適應這個時間,畢竟手機廠商也是有用戶體驗研究的)。算法

而後新問題產生了,若是用戶在正常操做的過程當中,Token 過時失效了,要求用戶從新登陸……用戶體驗豈不是很糟糕?數據庫

爲了解決在操做過程不能讓用戶感到 Token 失效這個問題,有一種方案是在服務器端保存 Token 狀態,用戶每次操做都會自動刷新(推遲) Token 的過時時間——Session 就是採用這種策略來保持用戶登陸狀態的。然而仍然存在這樣一個問題,在先後端分離、單頁 App 這些狀況下,每秒種可能發起不少次請求,每次都去刷新過時時間會產生很是大的代價。若是 Token 的過時時間被持久化到數據庫或文件,代價就更大了。因此一般爲了提高效率,減小消耗,會把 Token 的過時時保存在緩存或者內存中。express

還有另外一種方案,使用 Refresh Token,它能夠避免頻繁的讀寫操做。這種方案中,服務端不須要刷新 Token 的過時時間,一旦 Token 過時,就反饋給前端,前端使用 Refresh Token 申請一個全新 Token 繼續使用。這種方案中,服務端只須要在客戶端請求更新 Token 的時候對 Refresh Token 的有效性進行一次檢查,大大減小了更新有效期的操做,也就避免了頻繁讀寫。固然 Refresh Token 也是有有效期的,可是這個有效期就能夠長一點了,好比,以小時爲單位的時間。json

登陸與業務請求後端

Token 過時,刷新 Tokenapi

上面的圖中並未提到 Refresh Token 過時怎麼辦。不過很顯然,Refresh Token 既然已通過期,就該要求用戶從新登陸了。緩存

還能夠把這個機制設計得更復雜一些,好比,Refresh Token 每次使用的時候,都更新它的過時時間,直到與它的建立時間相比,已經超過了很是長的一段時間(好比一小時),這等因而在至關長一段時間內容許 Refresh Token 自動續期。

到目前爲止,Token 都是有狀態的,即在服務端須要保存並記錄相關屬性。

無狀態 Token

若是咱們把全部狀態信息都附加在 Token 上,服務器就能夠不保存。可是服務端仍然須要認證 Token 有效。不過只要服務端能確認是本身簽發的 Token,並且其信息未被改動過,那就能夠認爲 Token 有效——「簽名」能夠做此保證。平時常說的簽名都存在一方簽發,另外一方驗證的狀況,因此要使用非對稱加密算法。可是在這裏,簽發和驗證都是同一方,因此對稱加密算法就能達到要求,而對稱算法比非對稱算法要快得多(可達數十倍差距)。更進一步思考,對稱加密算法除了加密,還帶有還原加密內容的功能,而這一功能在對 Token 簽名時並沒有必要——既然不須要解密,摘要(散列)算法就會更快。能夠指定密碼的散列算法,天然是 HMAC。 JWT 已經定義了詳細的規範,並且有各類語言的若干實現。

不過在使用無狀態 Token 的時候在服務端會有一些變化,服務端雖然不保存有效的 Token 了,卻須要保存未到期卻已註銷的 Token。若是一個 Token 未到期就被用戶主動註銷,那麼服務器須要保存這個被註銷的 Token,以便下次收到使用這個仍在有效期內的 Token 時判其無效。

在前端可控的狀況下(好比前端和服務端在同一個項目組內),能夠協商:前端一但註銷成功,就丟掉本地保存(好比保存在內存、LocalStorage 等)的 Token 和 Refresh Token。基於這樣的約定,服務器就能夠假設收到的 Token 必定是沒註銷的(由於註銷以後前端就不會再使用了)。

若是前端不可控的狀況,仍然能夠進行上面的假設,可是這種狀況下,須要儘可能縮短 Token 的有效期,並且必須在用戶主動註銷的狀況下讓 Refresh Token 無效。這個操做存在必定的安全漏洞,由於用戶會認爲已經註銷了,實際上在較短的一段時間內並無註銷。若是應用設計中,這點漏洞並不會形成什麼損失,那採用這種策略就是可行的。

分離認證服務

前端拿到一個有效的 Token,它就能夠在任何同一體系的服務上認證經過——只要它們使用一樣的密鑰和算法來認證 Token 的有效性。就樣這樣:

固然,若是 Token 過時了,前端仍然須要去認證服務更新 Token:

雖然認證和業務分離了,實際即並沒產生多大的差別。固然,這是創建在認證服務器信任業務服務器的前提下,由於認證服務器產生 Token 的密鑰和業務服務器認證 Token 的密鑰和算法相同。換句話說,業務服務器一樣能夠建立有效的 Token。

不受信的業務服務器

遇到不受信的業務服務器時,很容易想到的辦法是使用不一樣的密鑰。認證服務器使用密鑰1簽發,業務服務器使用密鑰2驗證——這是典型非對稱加密簽名的應用場景。認證服務器本身使用私鑰對 Token 簽名,公開公鑰。信任這個認證服務器的業務服務器保存公鑰,用於驗證簽名。幸虧,JWT 不只可使用 HMAC 簽名,也可使用 RSA(一種非對稱加密算法)簽名。

當業務服務器已經不受信任的時候,多個業務服務器之間使用相同的 Token 對用戶來講是不安全的。由於任何一個服務器拿到 Token 均可以仿冒用戶去另外一個服務器處理業務。

爲了防止這種狀況發生,就須要在認證服務器產生 Token 的時候,把使用該 Token 的業務服務器的信息記錄在 Token 中,這樣當另外一個業務服務器拿到這個 Token 的時候,發現它並非本身應該驗證的 Token,就能夠直接拒絕。

如今,認證服務器不信任業務服務器,業務服務器相互也不信任,但前端是信任這些服務器的——若是前端不信任,就不會拿 Token 去請求驗證。那麼爲何會信任?多是由於這些是同一家公司或者同一個項目中提供的若干服務構成的服務體系。

可是,前端信任不表明用戶信任。若是 Token 不攜帶用戶隱私(好比姓名),那麼用戶不會關心信任問題。但若是 Token 含有用戶隱私的時候,用戶得關心信任問題了。這時候認證服務就不得再也不囉嗦一些,當用戶請求 Token 的時候,問上一句,你真的要受權給某某某業務服務嗎?而這個「某某某」,用戶怎麼知道它是否是真的「某某某」呢?用戶固然不知道,甚至認證服務也不知道,由於公鑰已經公開了,任何一個業務均可以聲明本身是「某某某」。

爲了獲得用戶的信任,認證服務就不得不幫助用戶來甄別業務服務。因此,認證服器決定不公開公鑰,而是要求業務服務先申請註冊並經過審覈。只有經過審覈的業務服務器才能獲得認證服務爲它建立的,僅供它使用的公鑰。若是該業務服務泄漏公鑰帶來風險,由該業務服務自行承擔。如今認證服務能夠清楚的告訴用戶,「某某某」服務是什麼了。若是用戶仍是不夠信任,認證服務甚至能夠問,某某某業務服務須要請求 A、B、C 三項我的數據,其中 A 是必須的,否則它不工做,是否容許受權?若是你受權,我就把你受權的幾項數據加密放在 Token 中……

JWT 介紹及其原理

JWT是Auth0提出的經過對JSON進行加密簽名來實現受權驗證的方案,編碼以後的JWT看起來是由.分爲三段,經過解碼能夠獲得:

// 1. Headers
// 包括類別(typ)、加密算法(alg);
{
  "alg": "HS256",
  "typ": "JWT"
}
// 2. payload
// 包括須要傳遞的用戶信息;
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
// 3. Signature
// 根據alg算法與私有祕鑰進行加密獲得的簽名字串;
// 這一段是最重要的敏感信息,只能在服務端解密;
HMACSHA256(  
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    SECREATE_KEY
)

在使用過程當中,服務端經過用戶登陸驗證以後,將Header+Claim信息加密後獲得第三段簽名,而後將簽名返回給客戶端,在後續請求中,服務端只須要對用戶請求中包含的JWT進行解碼,便可驗證是否能夠受權用戶獲取相應信息。

完整示例

前端請求

var token = '' // token應保存在LocalStorage 或 sessionStorage 中
  $('button').on('click', function () {
    $.ajax({
      type: 'post',
      url: '/api/login',
      data: { name: 'admin', pass: '123456' },
      success: function (data) {
        console.log(data);
        token = data.token
      },
      error: function (data) {
        console.log(data);
      }
   })
  })

  $('.hello').on('click', function () {
    $.ajax({
      type: 'get',
      url: '/api/hello',
      headers: { authorization: token },
      success: function (data) {
        console.log(data);
      },
      error: function (data) {
        console.log(data);
      }

    })
  })

後端邏輯

// index.js

var express = require('express');
var token = require('./token.js')
var bodyParser = require('body-parser');

var app = express();

var router = express.Router();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));


// 加載html
router.get('/', function (req, res) {
  res.sendFile(__dirname + '/index.html');
});

// 檢測token
app.use('/api/*', function (req, res, next) {
  if (req.originalUrl !== '/api/login') {
    var k = req.get('authorization')
    if (!token.checkToken(k)) {
      res.status(500).send('token error');
      return
    }
  }
  next();
});

// 登陸請求
router.post('/api/login', function (req, res) {
  if (req.body.name && req.body.pass) {
    // 驗證用戶名與密碼省略...
    var k = token.createToken({ name: req.body.name, possword: req.body.pass }, 1200)
    var data = {
      message: '登陸成功!',
      token: k
    }
    res.send(data);
  } else {
    res.status(500).send('error');
  }
});

// hello請求
router.get('/api/hello', function (req, res) {
  let data = { message: 'Hello word!' }
  res.send(data);
});

app.use('/', router);

var server = app.listen(8080)


// token.js
var crypto=require("crypto");
var token={
    createToken: function(obj,timeout){  // 生成token
        // Headers
        var obj1 = {
            "alg": "HS256",
            "typ": "JWT"
        }
        var headers = Buffer.from(JSON.stringify(obj1), "utf8").toString("base64");

        //payload信息
        var obj2 = {
            data: obj,//payload
            created: parseInt(Date.now()/1000),//token生成的時間的,單位秒
            exp: parseInt(timeout)||600//token有效期
        };
        var base64Str = Buffer.from(JSON.stringify(obj2),"utf8").toString("base64");

        //添加簽名,防篡改
        var secret = 'test';
        var hash = crypto.createHmac('sha256',secret);
            hash.update(headers);
            hash.update(base64Str);
        var signature = hash.digest('base64');

        return headers + "." + base64Str + "." + signature;
    },
    decodeToken: function(token){

        var decArr = token.split(".");
        if (decArr.length < 3){
            //token不合法
            return false;
        }

        var payload = {};

        //將payload json字符串 解析爲對象
        try {
            payload = JSON.parse(Buffer.from(decArr[1],"base64").toString("utf8"));
        } catch(e) {
            return false;
        }

        //生成簽名
        var secret = 'test';        
        var hash = crypto.createHmac('sha256',secret);
            hash.update(decArr[0]);
            hash.update(decArr[1]);
        var checkSignature = hash.digest('base64');

        return {
            payload: payload,
            signature: decArr[2],
            checkSignature: checkSignature
        }
    },
    checkToken: function(token){  // 驗證token
        var resDecode = this.decodeToken(token);
        if(!resDecode){
            return false;
        }

        //是否過時
        var expState=(parseInt(Date.now()/1000)-parseInt(resDecode.payload.created))>parseInt(resDecode.payload.exp) ? false : true;
        // 驗證簽名
        if (resDecode.signature === resDecode.checkSignature && expState) {
            return true;
        }
        return false;
    }
}
module.exports = token
相關文章
相關標籤/搜索