最近網站後臺迎來第三次改版,原來採用的是jquery+bootstrap這樣常規的方式,可是隨着網站的交互愈來愈多,信息量愈來愈大,就很是力不從心了,每次寫動態交互都好痛苦。趁着此次機會,決定採用MVVM的新JS框架,最終評估選擇vue.js大禮包,沒錯!正由於如此,先後端實現了徹底分離,就不能採用session這樣簡單的登錄校驗機制了,取而代之的是令牌+RESTful的方式進行交互,此時JWT閃亮登場!php
JWT(Json Web Token)是一個開放標準(RFC 7519),它基於json對象定義了一種緊湊而且自包含的方式進行安全信息傳輸。因爲消息通過了數字簽名,因此是能夠被校驗和信任的。另外JWT可使用密匙,或者使用RSA的公鑰/私鑰進行簽名。
其中的一些概念:html
session認證
由於http自己是無狀態的協議,因此每一次的請求其實都要校驗,session的原理就初次登錄的時候將相關信息保存到服務端,響應一個cookie保存到客戶端,這樣每次請求都攜帶cookie,服務器可以實現校驗,這會面臨3個問題
一、難以實現單點登陸,除非不一樣服務器之間共享session
二、session默認保存在服務端,增長服務器的存儲壓力
三、API調試麻煩前端
OAuth 2.0
OAuth 通常用於第三方接入的場景,管理對外的權限,好比什麼第三方登陸,微信受權,開放平臺等,相似這些更加嚴謹的場景,相對來講也更加安全,可是部署過程複雜,受權流程也是麻煩,感受是有些小題大作。而JWT更適用於相似RESTful API(微服務)之間的交互。vue
自建token協議
這種狀況固然最靈活,可是除非有雄厚的資金實例,多餘的時間和必要的狀況,不然不必重複造輪子吶。
曾經咱們還用過簡單的辦法,登錄以後根據用戶信息進行加鹽hash,該hash值即爲token,而後以(hash,value)的形式存儲在緩存或者數據庫中,每次請求攜帶hash,而後讀取校驗該hash是否存在,不然校驗失敗。這種方式也不失爲一種簡單快捷的好辦法,可是僅僅只能當作token校驗,而且相關數據存儲在服務器,每次訪問都還須要進行一次查詢,增長服務器開銷react
下面是一些JWT有用的場景
一、身份校驗
這是最多見的的使用場景,一旦用戶完成了登錄校驗,後面每一次的請求豆漿攜帶JWT,從而校驗用戶是否容許訪問路由、服務、資源。更重要的是,經過JWT能夠很是容易實現SSO(Single Sign On)單點登陸,由於開銷很小,這就意味着,在一個主站登錄了,別的站點就均可以輕鬆使用JWT訪問。
二、信息交換
從上文可知,JWT是可以被簽名的的,因此在安全信息傳輸中,是一個不錯的方案,例如使用公鑰私鑰時,你能夠肯定收件人是誰,另外還能夠校驗確保內容是否被篡改。這樣,就能夠在一些相似下單、交易等等重要的場合使用。jquery
JWT由三部分組成,他們中間由.
分隔:ios
所以,典型的JWT看起來是這樣的
xxxxx.yyyyy.zzzzz
git
頭部主要包含2個部分,token類型和採用的加密算法。github
{ "alg": "HS256", "typ": "JWT"}
而後用Base64Url進行編碼,就成了JWT的第一個部分算法
數據部分包含了主要的聲明字段以及相應的值,聲明主要包括3種類型:reserved , public 和 private
iss(issuer): jwt簽發者sub(subject): 簽發的項目aud(audience): 接收jwt的一方exp(exipre): jwt的過時時間,這個過時時間必需要大於簽發時間nbf(not before): 定義在什麼時間以前,該jwt是不可用的.iat(issued at): jwt的簽發時間jti(jwt token id): jwt的惟一身份標識,主要用來做爲一次性token,從而回避重放攻擊
須要注意的是,聲明名稱只有三個字符長度,這是爲了讓JWT保持緊湊
簡單示例以下:
{ "iss": "www", "iat": 1441593502, "exp": 1441594722, "aud": "www.example.com", "sub": "www@example.com", "from_user": "B", "target_user": "A"}
而後用Base64Url進行編碼,就成了JWT的第二個部分
爲了建立簽名,你須要先對前面的部分進行Base64的編碼,而後加上私匙,對其進行簽名。
例如,你想使用HMAC SHA256算法進行前面,那麼建立過程以下:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
簽名的目的是爲了校驗JWT的攜帶者信息,而且檢驗是否有篡改過所攜帶的JWT信息。
HMAC SHA256算法計算以後的二進制數據默認進行Base64編碼,就是JWT的第三個部分了
最終的結果是三段Base64字符串,經過.
拼接在一塊兒,這樣就很容易在HTML和HTTP環境中傳輸,與基於XML的標準相比,更加緊湊節省資源。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
調試工具:https://jwt.io/#debugger-io
項目使用的是基於php的thinkphp5.0框架做爲後端提供服務。前端則是vue+element-ui+axios,至於php類庫,採用的是php中Star最多的
https://github.com/lcobucci/jwt
後端php經過composer安裝以後使用起來很是的簡單,新建一個類專門用於校驗
use Lcobucci\JWT\Builder;use Lcobucci\JWT\Parser;use Lcobucci\JWT\Signer\Hmac\Sha256;use Lcobucci\JWT\ValidationData;class Auth{ const KEY = 'febcbaae13751fa2ds44c2f107afb08d'; const VALID_INFO = [ 'Issuer' => 'http://www.xxxx.com', 'Audience' => 'http://aaa.xxxx.com', 'Subject' => 'test', 'Expire' => 259200 ]; public static function check() { $jwt = request()->header('jwt'); $valid = new ValidationData(); $valid->setIssuer(self::VALID_INFO['Issuer']); $valid->setAudience(self::VALID_INFO['Audience']); $valid->setSubject(self::VALID_INFO['Subject']); //校驗jwt信息,同時校驗簽名,不然能夠僞造信息 $signer = new Sha256(); if ($jwt->validate($valid) && $jwt->verify($signer, self::KEY)) { $uinfo = $jwt->getClaim('uinfo'); //取出數據的時候是對象而不是數組 $uinfo->id //後續的權限校驗過程…… } } public static function getSignedJWT($userinfo) { $signer = new Sha256(); $token = (new Builder()) ->setIssuer(self::VALID_INFO['Issuer']) ->setAudience(self::VALID_INFO['Audience']) ->setSubject(self::VALID_INFO['Subject']) ->setIssuedAt(time()) ->setExpiration(time() + self::VALID_INFO['Expire']) //能夠直接保存數組或對象 ->set('uinfo', $userinfo) ->sign($signer, self::KEY) ->getToken()->__toString(); return $token; }}
登錄的時候保存JWT到localStorage,退出登陸時前端刪除保存的JWT便可。
apiLogin.login(this.$data.loginForm).then(res => { if (res.data.ret === 0) { this.$local.set('jwt', res.data.jwt) this.$local.set('menu', res.data.menu) this.$local.set('rules', res.data.rules) this.$local.set('username', this.loginForm.username) this.$local.set('title', res.data.title) this.$local.set('gpid', res.data.gpid) this.$router.push('index') // 本來沒有jwt,因此登錄獲取以後手動設置一次 this.$http.defaults.headers.common['jwt'] = this.$local.get('jwt') } else { this.isLogining = false this.$message.error(res.data.msg) } }).catch(() => { this.isLogining = false })
base_api.js
import axios from 'axios'import { Message } from 'element-ui'import local from 'store'// Add a request interceptoraxios.interceptors.request.use(function (config) { return config}, function (error) { Message.error({ showClose: true, message: '網絡異常,請檢查您的網絡' }) console.log(error) // Do something with request error return Promise.reject(error)})// Add a response interceptoraxios.interceptors.response.use(function (response) { // 受權過時,無受權信息,跳出登錄 if (response.data.ret === 4011 || response.data.ret === 4013) { window.location.href = '/#/login' // 刪除本地的token令牌 local.remove('jwt') Message.error({ showClose: true, message: response.data.msg }) return } if (response.data.ret === 4012) { // 無權限返回 window.history.back() Message.error({ showClose: true, message: response.data.msg }) return } return response}, function (error) { Message.error({ showClose: true, message: '網絡異常,請檢查您的網絡' }) return Promise.reject(error)})const baseUrl = process.env.API_ROOTaxios.defaults.baseURL = baseUrl// 初始化的時候加載本地儲存過的jwtif (local.get('jwt')) { axios.defaults.headers.common['jwt'] = local.get('jwt')}export const http = axios
Cookie 能夠啓用 HttpOnly 和 Secure:
可是爲了實現正真意義上的無狀態和跨域單點,仍是堅持存儲在LocalStorage,而目前localStorage存儲沒有對XSS攻擊有任何抵禦機制,一旦出現XSS漏洞,那麼存儲在localStorage裏的數據就極易被獲取到。
若是一個網站存在XSS漏洞,那麼攻擊者注入以下代碼,就能夠獲取使用localStorage存儲在本地的全部信息。
因此務必作好過濾安全檢查。
一、JWT並不包含權限校驗部分,只包含Token校驗,因此在Token校驗完成以後,權限部分還需自行校驗一次。
二、jwt的payload數據部分不要存放敏感信息,此部分是任何人均可以解密查看的,而jwt主要依靠簽名校驗身份,同時也不建議存放易改動的信息,不然須要token過時或者從新登陸才能來獲取最新的信息。
三、簽名所用的secret私匙必定要保管好!!!
四、務必使用https,不然用戶被截獲到token,就能夠進行僞造攻擊。
五、JWT使用的場景中,通常是要跨域的,因此服務端須要作好CORS的策略支持。見這裏
六、若須要強制過時JWT,則在用戶表新建一個簽名時間字段便可,在登錄的時候檢查,若JWT保存簽名時間小於服務器簽名時間,即強制過時