内容简介:传统的 cookie-session 机制可以保证的接口安全,在没有通过认证的情况下会跳转至登入界面或者调用失败。在如今 RESTful 化的 API 接口下,cookie-session 已经不能很好发挥其余热保护好你的 API 。更多的形式下采用的基于 Token 的验证机制,JWT 本质的也是一种 Token,但是其中又有些许不同。
传统的 cookie-session 机制可以保证的接口安全,在没有通过认证的情况下会跳转至登入界面或者调用失败。
在如今 RESTful 化的 API 接口下,cookie-session 已经不能很好发挥其余热保护好你的 API 。
更多的形式下采用的基于 Token 的验证机制,JWT 本质的也是一种 Token,但是其中又有些许不同。
什么是 JWT ?
JWT 及时 JSON Web Token,它是基于 RFC 7519 所定义的一种在各个系统中传递紧凑和自包含的 JSON 数据形式。
- 紧凑 (Compact) :由于传送的数据小,JWT 可以通过GET、POST 和 放在 HTTP 的 header 中,同时也是因为小也能传送的更快。
- 自包含 (self-contained) : Payload 中能够包含用户的信息,避免数据库的查询。
JSON Web Token 由三部分组成使用 . 分割开:
- Header
- Payload
- Signature
一个 JWT 形式上类似于下面的样子:
xxxxx.yyyy.zzzz
Header 一般由两个部分组成:
- alg
- typ
alg 是是所使用的 hash 算法例如 HMAC SHA256 或 RSA,typ 是 Token 的类型自然就是 JWT。
{
"alg": "HS256",
"typ": "JWT"
}
然后使用 Base64Url 编码成第一部分。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<second part>.<third part>
Payload
这一部分是 JWT 主要的信息存储部分,其中包含了许多种的声明(claims)。
Claims 的实体一般包含用户和一些元数据,这些 claims 分成三种类型:reserved, public, 和 private claims。
-(保留声明)reserved claims :预定义的 一些声明,并不是强制的但是推荐,它们包括 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
在创建该部分时候你应该已经有了 编码后的 Header 和 Payload 还需要一个一个秘钥,这个加密的算法应该 Header 中指定。
一个使用 HMAC SHA256 的例子如下:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
这个 signature 是用来验证发送者的 JWT 的同时也能确保在期间不被篡改。
所以,做后你的一个完整的 JWT 应该是如下形式:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意被 . 分割开的三个部分
JSON Web Token 的工作流程
在用户使用证书或者账号密码登入的时候一个 JSON Web Token 将会返回,同时可以把这个 JWT 存储在local storage、或者 cookie 中,用来替代传统的在服务器端创建一个 session 返回一个 cookie。
当用户想要使用受保护的路由时候,应该要在请求得时候带上 JWT ,一般的是在 header 的 Authorization 使用 Bearer 的形式,一个包含的 JWT 的请求头的 Authorization 如下:
Authorization: Bearer <token>
这是一中无状态的认证机制,用户的状态从来不会存在服务端,在访问受保护的路由时候回校验 HTTP header 中 Authorization 的 JWT,同时 JWT 是会带上一些必要的信息,不需要多次的查询数据库。
这种无状态的操作可以充分的使用数据的 APIs,甚至是在下游服务上使用,这些 APIs 和哪服务器没有关系,因此,由于没有 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内容。
安装必要的软件包:
pip install flask pip install Flask-JWT
一个简单的 DEMO:
from flask import Flask
from flask_jwt import JWT, jwt_required, current_identity
from werkzeug.security import safe_str_cmp
class 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.id
users = [
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 user
def identity(payload):
user_id = payload["identity"]
return userid_table.get(user_id, None)
app = Flask(__name__)
app.debug = True
app.config["SECRET_KEY"] = "super-secret"
jwt = JWT(app, authenticate, identity)
@app.route("/protected")
@jwt_required()
def protected():
return "%s" % current_identity
if __name__ == "__main__":
app.run()
首先需要获取用户的 JWT:
% http POST http://127.0.0.1:5000/auth username="user1" password="abcxyz" ~
HTTP/1.0 200 OK
Content-Length: 193
Content-Type: application/json
Date: Sun, 21 Aug 2016 03:48:41 GMT
Server: 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> 才能获取数据
% http http://127.0.0.1:5000/protected Authorization:"JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDcxNzUxMzIxLCJuYmYiOjE0NzE3NTEzMjEsImV4cCI6MTQ3MTc1MTYyMX0.S0825N6IliQb65QoJfUXb3IGq-j9OVJpHBh-bcUz_gc" HTTP/1.0 200 OK Content-Length: 12 Content-Type: text/html; charset=utf-8 Date: Sun, 21 Aug 2016 03:51:20 GMT Server: Werkzeug/0.11.10 Python/2.7.10 User(id="1")
不带 JWT 的时候会返回如下信息:
% http http://127.0.0.1:5000/protected ~
HTTP/1.0 401 UNAUTHORIZED
Content-Length: 125
Content-Type: application/json
Date: Sun, 21 Aug 2016 03:49:51 GMT
Server: Werkzeug/0.11.10 Python/2.7.10
WWW-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 --save npm install express-jwt --save npm install body-parser --save npm install jsonwebtoken --save npm install shortid --save
本例子中只是最简单的使用方法,更多使用方法参看 express-jwt
const express = require("express");
const expressJwt = require("express-jwt");
const bodyParser = require("body-parser");
const jwt = require("jsonwebtoken");
const shortid = require("shortid");
const 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) {
const username = req.body.username;
const 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");
}
const authToken = jwt.sign({username: username}, "secret");
res.status(200).json({token: authToken});
});
app.post("/user", function(req, res) {
const username = req.body.username;
const password = req.body.password;
const country = req.body.country;
const 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 的一个中间件,需要设置 secret 作为秘钥,unless 可以排除某个接口。
默认的情况下,解析 JWT 失败会抛出异常,可以通过以下设置来处理该异常。
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 OK
Connection: keep-alive
Content-Length: 143
Content-Type: application/json; charset=utf-8
Date: Sun, 21 Aug 2016 06:57:42 GMT
ETag: 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 Unauthorized Connection: keep-alive Content-Length: 13 Content-Type: text/html; charset=utf-8 Date: Sun, 21 Aug 2016 07:00:02 GMT ETag: W/"d-j0viHsPPu6FaNJ6cXoiFeQ" X-Powered-By: Express invalid token
使用 JWT 就可以成功调用
% http POST http://localhost:3000/user Authorization:"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNDcxNzYyNjYyfQ.o5RFJB4GiR28HzXbSptU6MsPwW1tSXSDIjlzn7erG0M" username="hexiangyu" password="password" country="CN" age=22
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 66
Content-Type: application/json; charset=utf-8
Date: Sun, 21 Aug 2016 07:04:34 GMT
ETag: W/"42-YnGYuyDLxpVUexEGEcQj1g"
X-Powered-By: Express
{
"age": "22",
"country": "CN",
"id": "r1sFMCUc",
"username": "hexiangyu"
}
Reference
本文转自链接: http://blog.zhengxiaowai.cc/post/safe-jwt-restful-api.html
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- RecyclerView使用指南(一)—— 基本使用
- 如何使用Meteorjs使用URL参数
- 使用 defer 还是不使用 defer?
- 使用 Typescript 加强 Vuex 使用体验
- [译] 何时使用 Rust?何时使用 Go?
- UDP协议的正确使用场合(谨慎使用)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Release It!
Michael T. Nygard / Pragmatic Bookshelf / 2007-03-30 / USD 34.95
“Feature complete” is not the same as “production ready.” Whether it’s in Java, .NET, or Ruby on Rails, getting your application ready to ship is only half the battle. Did you design your system to......一起来看看 《Release It!》 这本书的介绍吧!