傳統(tǒng)的 cookie-session 機制可以保證的接口安全,在沒有通過認證的情況下會跳轉(zhuǎn)至登入界面或者調(diào)用失敗。
在如今 RESTful 化的 API 接口下,cookie-session 已經(jīng)不能很好發(fā)揮其余熱保護好你的 API 。
更多的形式下采用的基于 Token 的驗證機制,JWT 本質(zhì)的也是一種 Token,但是其中又有些許不同。
什么是 JWT ?
JWT 及時 JSON Web Token,它是基于 RFC 7519 所定義的一種在各個系統(tǒng)中傳遞緊湊和自包含的 JSON 數(shù)據(jù)形式。
- 緊湊(Compact) :由于傳送的數(shù)據(jù)小,JWT 可以通過GET、POST 和 放在 HTTP 的 header 中,同時也是因為小也能傳送的更快。
- 自包含(self-contained) : Payload 中能夠包含用戶的信息,避免數(shù)據(jù)庫的查詢。
JSON Web Token 由三部分組成使用 .
分割開:
一個 JWT 形式上類似于下面的樣子:
xxxxx.yyyy.zzzz
Header 一般由兩個部分組成:
alg 是是所使用的 hash 算法例如 HMAC SHA256 或 RSA,typ 是 Token 的類型自然就是 JWT。
{ "alg": "HS256", "typ": "JWT"}
然后使用 Base64Url 編碼成第一部分。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<second part>.<third part>
Payload
這一部分是 JWT 主要的信息存儲部分,其中包含了許多種的聲明(claims)。
Claims 的實體一般包含用戶和一些元數(shù)據(jù),這些 claims 分成三種類型:reserved, public, 和 private claims。
(保留聲明)reserved claims :預(yù)定義的 一些聲明,并不是強制的但是推薦,它們包括 iss (issuer), exp (expiration time), sub (subject),aud(audience) 等。
這里都使用三個字母的原因是保證 JWT 的緊湊
(公有聲明)public claims : 這個部分可以隨便定義,但是要注意和 IANA JSON Web Token 沖突。
(私有聲明)private claims : 這個部分是共享被認定信息中自定義部分。
一個 Pyload 可以是這樣子的:
{ "sub": "1234567890", "name": "John Doe", "admin": true}
這部分同樣使用 Base64Url 編碼成第二部分。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.<third part>
Signature
在創(chuàng)建該部分時候你應(yīng)該已經(jīng)有了 編碼后的 Header 和 Payload 還需要一個一個秘鑰,這個加密的算法應(yīng)該 Header 中指定。
一個使用 HMAC SHA256 的例子如下:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
這個 signature 是用來驗證發(fā)送者的 JWT 的同時也能確保在期間不被篡改。
所以,做后你的一個完整的 JWT 應(yīng)該是如下形式:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意被 .
分割開的三個部分
JSON Web Token 的工作流程
在用戶使用證書或者賬號密碼登入的時候一個 JSON Web Token 將會返回,同時可以把這個 JWT 存儲在local storage、或者 cookie 中,用來替代傳統(tǒng)的在服務(wù)器端創(chuàng)建一個 session 返回一個 cookie。
當(dāng)用戶想要使用受保護的路由時候,應(yīng)該要在請求得時候帶上 JWT ,一般的是在 header 的 Authorization 使用 Bearer 的形式,一個包含的 JWT 的請求頭的 Authorization 如下:
Authorization: Bearer <token>
這是一中無狀態(tài)的認證機制,用戶的狀態(tài)從來不會存在服務(wù)端,在訪問受保護的路由時候回校驗 HTTP header 中 Authorization 的 JWT,同時 JWT 是會帶上一些必要的信息,不需要多次的查詢數(shù)據(jù)庫。
這種無狀態(tài)的操作可以充分的使用數(shù)據(jù)的 APIs,甚至是在下游服務(wù)上使用,這些 APIs 和哪服務(wù)器沒有關(guān)系,因此,由于沒有 cookie 的存在,所以在不存在跨域(CORS, Cross-Origin Resource Sharing)的問題。
在 Flask 和 Express 中使用 JSON Web Token
JWT 在各個 Web 框架中都有 JWT 的包可以直接使用,下面使用 Flask 和 Express 作為例子演示。
下面會使用 httpie 作為演示工具:
HTTPie: HTTP client, a user-friendly cURL replacement.- Download a URL to a file: http -d example.org- Send form-encoded data: http -f example.org name="bob" profile-picture@"bob.png"- Send JSON object: http example.org name="bob"- Specify an HTTP method: http HEAD example.org- Include an extra header: http example.org X-MyHeader:123- Pass a user name and password for server authentication: http -a username:password example.org- Specify raw request body via stdin: cat data.txt | http PUT example.org
Flask 中使用 JSON Web Token
這里的演示是 Flask-JWT
的 Quickstart內(nèi)容。
安裝必要的軟件包:
pip install flaskpip install Flask-JWT
一個簡單的 DEMO:
from flask import Flaskfrom flask_jwt import JWT, jwt_required, current_identityfrom werkzeug.security import safe_str_cmpclass User(object): def __init__(self, id, username, password): self.id = id self.username = username self.password = password def __str__(self): return "User(id="%s")" % self.idusers = [ User(1, "user1", "abcxyz"), User(2, "user2", "abcxyz"),]username_table = {u.username: u for u in users}userid_table = {u.id: u for u in users}def authenticate(username, password): user = username_table.get(username, None) if user and safe_str_cmp(user.password.encode("utf-8"), password.encode("utf-8")): return userdef identity(payload): user_id = payload["identity"] return userid_table.get(user_id, None)app = Flask(__name__)app.debug = Trueapp.config["SECRET_KEY"] = "super-secret"jwt = JWT(app, authenticate, identity)@app.route("/protected")@jwt_required()def protected(): return "%s" % current_identityif __name__ == "__main__": app.run()
首先需要獲取用戶的 JWT:
% http POST http://127.0.0.1:5000/auth username="user1" password="abcxyz" ~HTTP/1.0 200 OKContent-Length: 193Content-Type: application/jsonDate: Sun, 21 Aug 2016 03:48:41 GMTServer: Werkzeug/0.11.10 Python/2.7.10{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDcxNzUxMzIxLCJuYmYiOjE0NzE3NTEzMjEsImV4cCI6MTQ3MTc1MTYyMX0.S0825N6IliQb65QoJfUXb3IGq-j9OVJpHBh-bcUz_gc"}
使用 @jwt_required()
裝飾器來保護你的 API
@app.route("/protected")@jwt_required()def protected(): return "%s" % current_identity
這時候你需要在 HTTP 的 header 中使用 Authorization: JWT <token>
才能獲取數(shù)據(jù)
% http http://127.0.0.1:5000/protected Authorization:"JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDcxNzUxMzIxLCJuYmYiOjE0NzE3NTEzMjEsImV4cCI6MTQ3MTc1MTYyMX0.S0825N6IliQb65QoJfUXb3IGq-j9OVJpHBh-bcUz_gc"HTTP/1.0 200 OKContent-Length: 12Content-Type: text/html; charset=utf-8Date: Sun, 21 Aug 2016 03:51:20 GMTServer: Werkzeug/0.11.10 Python/2.7.10User(id="1")
不帶 JWT 的時候會返回如下信息:
% http http://127.0.0.1:5000/protected ~HTTP/1.0 401 UNAUTHORIZEDContent-Length: 125Content-Type: application/jsonDate: Sun, 21 Aug 2016 03:49:51 GMTServer: Werkzeug/0.11.10 Python/2.7.10WWW-Authenticate: JWT realm="Login Required"{ "description": "Request does not contain an access token", "error": "Authorization Required", "status_code": 401}
Express 中使用 JSON Web Token
Auth0 提供了 express-jwt 這個包,在 express 可以很容易的集成。
npm install express --savenpm install express-jwt --savenpm install body-parser --savenpm install jsonwebtoken --savenpm install shortid --save
本例子中只是最簡單的使用方法,更多使用方法參看 express-jwt
var express = require("express");var expressJwt = require("express-jwt");var bodyParser = require("body-parser");var jwt = require("jsonwebtoken");var shortid = require("shortid");var app = express();app.use(bodyParser.json());app.use(expressJwt({secret: "secret"}).unless({path: ["/login"]}));app.use(function (err, req, res, next) { if (err.name === "UnauthorizedError") { res.status(401).send("invalid token"); }});app.post("/login", function(req, res) { var username = req.body.username; var password = req.body.password; if (!username) { return res.status(400).send("username require"); } if (!password) { return res.status(400).send("password require"); } if (username != "admin" && password != "password") { return res.status(401).send("invaild password"); } var authToken = jwt.sign({username: username}, "secret"); res.status(200).json({token: authToken});});app.post("/user", function(req, res) { var username = req.body.username; var password = req.body.password; var country = req.body.country; var age = req.body.age; if (!username) { return res.status(400).send("username require"); } if (!password) { return res.status(400).send("password require"); } if (!country) { return res.status(400).send("countryrequire"); } if (!age) { return res.status(400).send("age require"); } res.status(200).json({ id: shortid.generate(), username: username, country: country, age: age })})app.listen(3000);
express-jwt
作為 express 的一個中間件,需要設(shè)置 secret
作為秘鑰,unless 可以排除某個接口。
默認的情況下,解析 JWT 失敗會拋出異常,可以通過以下設(shè)置來處理該異常。
app.use(expressJwt({secret: "secret"}).unless({path: ["/login"]}));app.use(function (err, req, res, next) { if (err.name === "UnauthorizedError") { res.status(401).send("invalid token"); }});
/login
忽略的 JWT 認證,通過這個接口獲取某個用戶的 JWT
% http POST http://localhost:3000/login username="admin" password="password" country="CN" age=22 HTTP/1.1 200 OKConnection: keep-aliveContent-Length: 143Content-Type: application/json; charset=utf-8Date: Sun, 21 Aug 2016 06:57:42 GMTETag: W/"8f-iMzAS1K5StDQgtNnVSvqtQ"X-Powered-By: Express{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNDcxNzYyNjYyfQ.o5RFJB4GiR28HzXbSptU6MsPwW1tSXSDIjlzn7erG0M"}
不使用 JWT 的時候
% http POST http://localhost:3000/user username="hexiangyu" password="password" ~HTTP/1.1 401 UnauthorizedConnection: keep-aliveContent-Length: 13Content-Type: text/html; charset=utf-8Date: Sun, 21 Aug 2016 07:00:02 GMTETag: W/"d-j0viHsPPu6FaNJ6cXoiFeQ"X-Powered-By: Expressinvalid token
使用 JWT 就可以成功調(diào)用
% http POST http://localhost:3000/user Authorization:"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNDcxNzYyNjYyfQ.o5RFJB4GiR28HzXbSptU6MsPwW1tSXSDIjlzn7erG0M" username="hexiangyu" password="password" country="CN" age=22HTTP/1.1 200 OKConnection: keep-aliveContent-Length: 66Content-Type: application/json; charset=utf-8Date: Sun, 21 Aug 2016 07:04:34 GMTETag: W/"42-YnGYuyDLxpVUexEGEcQj1g"X-Powered-By: Express{ "age": "22", "country": "CN", "id": "r1sFMCUc", "username": "hexiangyu"}