本文主要介紹JWT(JSON Web Token)受權機制在先後端分離中的應用與實踐,包括如下三部分:javascript
先後端分離是一個頗有趣的議題,它不只僅是指先後端工程師之間的相互獨立的合做分工方式,更是先後端之間開發模式與交互模式的模塊化、解耦化。計算機世界的經驗告訴咱們,對於複雜的事物,模塊化老是好的,不管是後端API開發中愈來愈成爲規範的RESTful API風格,仍是Web前端愈來愈多的模板、框架(參見MVC,MVP 和 MVVM 的圖示),包括移動應用中先後端自然分離的特質,都證明了先後端分離的重要性與必要性(更生動的細節與實例說明能夠參看赫門分享的主題淘寶先後端分離實踐)。html
實現先後端分離,對於後端開發人員來講是一件很幸福的事情,由於不須要再考慮怎樣在HTML中套入數據,只關心數據邏輯的處理;而前端則須要承擔接收數據以後界面呈現、用戶交互、數據傳遞等全部任務。雖然這看起來加劇了前端的工做量,但實際上有愈來愈多豐富多樣的前端框架可供選擇,這讓前端開發變得愈來愈結構化、系統化,前端工程師也再也不只是「套版的」。前端
在全部前端框架中,Facebook推出的React無疑是當下最熱門(之一),然而React只負責界面渲染層面,至關於MVC中的V(View),所以只靠React沒法完成一個完整的單頁應用(Single Page App)。Facebook另外推出與之配套的Flux架構,主要爲了不Angular.js之類MVC的架構模式,規避數據雙向綁定而採用單向綁定的數據傳遞方式。實際上React不管是學習仍是使用都是很是簡單的,而Flux則須要花更多時間去理解消化,本文第3部分我採用Flux架構的一種實現Reflux.js,作了一個基於JWT受權機制的登入、登出的例子,順便介紹Flux架構的細節。html5
JWT是我以前作Android應用的時候瞭解到的一種用戶受權機制,雖然原生的移動手機應用與基於瀏覽器的Web應用之間存在不少差別,但不少狀況下後端每每仍是沿用已有的架構跟代碼,因此用戶受權每每仍是採用Cookie+Session的方式,也就是須要原生應用中模擬瀏覽器對Cookie的操做。java
Cookie+Session的存在主要是爲了解決HTTP這一無狀態協議下服務器如何識別用戶的問題,其原理就是在用戶登陸經過驗證後,服務端將數據加密後保存到客戶端瀏覽器的Cookie中,同時服務器保留相對應的Session(文件或DB)。用戶以後發起的請求都會攜帶Cookie信息,服務端須要根據Cookie尋回對應的Session,從而完成驗證,確認這是以前登錄過的用戶。其工做原理以下圖所示:node
JWT是Auth0提出的經過對JSON進行加密簽名來實現受權驗證的方案,編碼以後的JWT看起來是這樣的一串字符:react
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
由.
分爲三段,經過解碼能夠獲得:git
// 1. Headers // 包括類別(typ)、加密算法(alg); { "alg": "HS256", "typ": "JWT" } // 2. Claims // 包括須要傳遞的用戶信息; { "sub": "1234567890", "name": "John Doe", "admin": true } // 3. Signature // 根據alg算法與私有祕鑰進行加密獲得的簽名字串; // 這一段是最重要的敏感信息,只能在服務端解密; HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), SECREATE_KEY )
在使用過程當中,服務端經過用戶登陸驗證以後,將Header+Claim信息加密後獲得第三段簽名,而後將簽名返回給客戶端,在後續請求中,服務端只須要對用戶請求中包含的JWT進行解碼,便可驗證是否能夠受權用戶獲取相應信息,其原理以下圖所示:github
經過比較能夠看出,使用JWT能夠省去服務端讀取Session的步驟,這樣更符合RESTful的規範。可是對於客戶端(或App端)來講,爲了保存用戶受權信息,仍然須要經過Cookie或相似的機制進行本地保存。所以JWT是用來取代服務端的Session而非客戶端Cookie的方案,固然對於客戶端本地存儲,HTML5提供了Cookie以外更多的解決方案(localStorage/sessionStorage),究竟採用哪一種存儲方式,其實從Js操做上來看沒有本質上的差別,不一樣的選擇更可能是出於安全性的考慮。web
用戶受權這樣敏感的信息,安全性固然是首先須要考慮的因素。這裏主要討論在使用JWT時如何防止XSS和XSRF兩種攻擊。
XSS是Web中最多見的一種漏洞(咱們的**學報官網就存在這個漏洞這件事我就不說了=.=),其主要緣由是對用戶輸入信息不加過濾,致使用戶(被誤導)惡意輸入的Js代碼在訪問該網頁時被執行,而Js能夠讀取當前網站域名下保存的Cookie信息。針對這種攻擊,不管是Cookie仍是localStorage中的信息都有可能被竊取,但防止XSS也相對簡單一些,對用戶輸入的全部信息進行過濾便可。另外,如今愈來愈多的CDN服務,讓咱們能夠節省服務器流量,但同時也有可能引入不安全的Js腳本,例如前段時間Github被Great Cannon轟擊的案例,則須要提升對某度之類服務的警戒。
另一種更加棘手的XSRF漏洞主要利用Cookie是按照域名存儲,同時訪問某域名時瀏覽器會自動攜帶該域名所保存的Cookie信息這一特徵。若是執意要將JWT存儲在Cookie中,服務端則須要額外驗證請求來源,或者在提交表單中加入隨機簽名並在處理表單時進行驗證。
我在後面的實例中採用將JWT保存在localStorage中的方案,請求時將JWT放入Request Header中的Authorization位。對JWT安全性問題想要了解更多能夠參考下面幾篇文章:
本節源碼可見Github: react-jwt-example。
前面提到的React.js框架學習成本其實很是低,只要跟着官方教程走一遍,搞清楚props、states、virtual DOM幾個概念,就能夠開始用了。可是隻有View層什麼都作不了,Facebook推出配套的Flux架構,一開始看到下面這張架構圖,當時我就懵逼了。
好在Flux只是一種理論架構,雖然官方也提供了實現方案,可是我更傾向於Reflux.js的實現方式,以下圖所示:
其中View Components即視圖層由React負責,Stores用於存儲數據,Actions則用於監聽全部動做,全部數據的傳遞都是單向綁定的,在分割不一樣模塊時,能夠清楚地看到數據的流動方向。
我嘗試寫了一個簡單的登陸、登出以及獲取用戶我的數據的例子,除了Reflux以外,還用到以下模塊:
另外服務端API採用Go gin框架,依賴於jwt-go。代碼目錄結構以下:
tree -I 'node_modules|.git' . ├── README.md ├── gulpfile.js ├── index.html ├── package.json ├── scripts │ ├── actions │ │ └── actions.js │ ├── app.js │ ├── build │ │ └── dist.js │ ├── components │ │ └── HelloWorld.js │ ├── stores │ │ ├── loginStore.js │ │ └── userStore.js │ └── views │ ├── home.js │ ├── login.js │ └── profile.js └── server.go
完整的頁面放在view中,可複用的組件放在components,用戶的動做包括login、logout以及getBalance,所以須要建立相應的action來監聽這些動做:
// actions.js var actions = Reflux.createActions({ "login": {}, "updateProfile": {}, // login成功更新用戶數據 "loginError": {}, // login失敗錯誤信息 "logout": {}, "getBalance": {asyncResult: true} }); actions.login.listen(function(data){});
用戶點擊view中的Submit Button時,將表單信息提交給login action:
// views/login.js var Login = React.createClass({ ... login: function (e) { e.preventDefault(); actions.login({ name: this.refs.name.getValue(), pass: this.refs.pass.getValue(), }), ... }); // actions.js var req = require('reqwest'); actions.login.listen(function(data){ req({ url: HOST+"/user/token", method: "post", data: JSON.stringify(data), type: 'json', contentType: 'application/json', headers: {'X-Requested-With': 'XMLHttpRequest'}, success: function (resp) { if(resp.code == 200){ actions.updateProfile(resp.jwt) }else{ actions.updateProfile(resp.msg) } }, }) });
根據API返回結果,將再次觸發updateProfile或updateProfile action,而分別由userStore和loginStore接收:
// stores/userStore.js var userStore = Reflux.createStore({ listenables: actions, // 聲明userStore所監聽的action updateProfile: function(jwt){ // 註冊監聽actions.updateProfile localStorage.setItem('jwt', jwt); this.user = jwt_decode(jwt); this.user.logd = true; this.trigger(this.user); }, }) // stores/loginStore.js var loginStore = Reflux.createStore({ listenables: actions, loginError: function(msg){ this.trigger(msg); }, });
store接收action數據後,經過this.trigger(msg)
將處理事後的數據從新傳遞會view:
var Login = React.createClass({ mixins : [ Router.Navigation, Reflux.listenTo(userStore, 'onLoginSucc'), Reflux.listenTo(loginStore, 'onLoginErr') ], onLoginSucc: function(){ // 登陸成功,跳轉回首頁 this.transitionTo('home'); }, onLoginErr: function (msg) { // 登陸失敗,顯示錯誤信息 this.setState({ errorMsg: msg, }); }, ... });
至此,從用戶點擊登陸到登陸結果傳回,整個流程數據在View->Action->Store->View
中完成單向傳遞,這就是Flux架構的基本概念。
在完成登陸後,API會將驗證經過的JWT傳回:
// server.go token := jwt.New(jwt.SigningMethodHS256) // Headers token.Header["alg"] = "HS256" token.Header["typ"] = "JWT" // Claims token.Claims["name"] = validUser.Name token.Claims["mail"] = validUser.Mail token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix() tokenString, err := token.SignedString([]byte(mySigningKey)) if err != nil { c.JSON(200, gin.H{"code": 500, "msg": "Server error!"}) return } c.JSON(200, gin.H{"code": 200, "msg": "OK", "jwt": tokenString})
當登陸以後的用戶在profile頁面發起getBalance請求時,存儲於本地的jwt將一塊兒傳遞,我這裏採用Header的方式傳遞,具體取決於API端的協議:
// actions.js actions.getBalance.listen(function(){ var jwt = localStorage.getItem('jwt'); req({ url: HOST+"/user/balance", method: "post", type: "json", headers: { 'Authorization': "Bearer "+jwt, }, success: function (resp) { if (resp.code == 200) { actions.updateProfile(resp.jwt); }else{ actions.loginError(resp.msg); } } }) })
而服務端面對任何須要驗證權限的請求須要經過Token驗證:
//server.go token, err := jwt.ParseFromRequest(c.Request, func(token *jwt.Token) (interface{}, error) { b := ([]byte(mySigningKey)) return b, nil })
- END -
if(post.content.isHelpful){ $("button#donate").click(); };