首先,HTTP 是無狀態的協議(對於事務處理沒有記憶能力,每次客戶端和服務端會話完成時,服務端不會儲存任何會話資訊)——每個請求都是完全獨立的,服務端無法確認當前訪問者的身份資訊,無法分辨上一次的請求傳送者和這一次的傳送者是不是同一個人。所以伺服器與瀏覽器為了進行會話跟蹤(知道是誰在訪問自己),就必須主動的去維護一個狀態,這個狀態用於告知服務端前後兩個請求是否來自同一瀏覽器。為此,前端開發者便加入了Cookie來實現有狀態的HTTP連線。而後實現授權的方式就有cookie、session、token和JWT。
什麼是 JWT?
JWT.IO 解釋:JSON Web Token (JWT) 是一個開放標準 ( RFC 7519 ),它定義了一種緊湊且自包含的方式,用於在各方之間作為 JSON 物件安全地傳輸資訊。該資訊可以被驗證和信任,因為它是經過數字簽名的。JWT 可以使用祕密(使用HMAC演算法)或使用RSA或ECDSA的公鑰/私鑰對進行簽名。
案例
由於網上許多案例都為HS256(對稱加密),所以這裡我使用RSA256(非對稱加密)作為補充。
-
首先需要生成私鑰和公鑰
-
查閱《Generate OpenSSL RSA Key Pair using genpkey》得到了帶密碼的pem檔案, 但是在使用中會出現
TypeError: Password was not given but private key is encrypted
的錯誤。 -
從《How to generate JWT RS256 key》找到了解決辦法
ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key # Don't add passphrase openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub cat jwtRS256.key cat jwtRS256.key.pub
-
-
選擇Python的JWT庫,我這裡選擇了兩個庫
-
PyJWT(需要cryptography庫)
>>> import jwt >>> with open('jwtRS256.key', 'rb') as f: ... private_key = f.read() ... >>> with open('jwtRS256.key.pub', 'rb') as f: ... public_key = f.read() ... >>> print(encoded) eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg >>> decoded = jwt.decode(encoded, public_key, algorithms=["RS256"]) {'some': 'payload'}
-
>>> from authlib.jose import jwt >>> header = {'alg': 'RS256'} >>> payload = {'iss': 'Authlib', 'sub': '123', ...} >>> with open('jwtRS256.key', 'rb') as f: ... private_key = f.read() ... >>> s = jwt.encode(header, payload, private_key) >>> with open('jwtRS256.key.pub', 'rb') as f: ... public_key = f.read() ... >>> claims = jwt.decode(s, public_key) >>> print(claims) {'iss': 'Authlib', 'sub': '123', ...} >>> print(claims.header) {'alg': 'RS256', 'typ': 'JWT'} >>> claims.validate()
-
-
工作原理
《Using JWT for user authentication in Flask》中的程式碼參考:
# flask imports
from flask import Flask, request, jsonify, make_response
from flask_sqlalchemy import SQLAlchemy
import uuid # for public id
from werkzeug.security import generate_password_hash, check_password_hash
# imports for PyJWT authentication
import jwt
from datetime import datetime, timedelta
from functools import wraps
# creates Flask object
app = Flask(__name__)
# configuration
# NEVER HARDCODE YOUR CONFIGURATION IN YOUR CODE
# INSTEAD CREATE A .env FILE AND STORE IN IT
app.config['SECRET_KEY'] = 'your secret key'
# database name
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///Database.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
# creates SQLALCHEMY object
db = SQLAlchemy(app)
# Database ORMs
class User(db.Model):
id = db.Column(db.Integer, primary_key = True)
public_id = db.Column(db.String(50), unique = True)
name = db.Column(db.String(100))
email = db.Column(db.String(70), unique = True)
password = db.Column(db.String(80))
# decorator for verifying the JWT
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = None
# jwt is passed in the request header
if 'x-access-token' in request.headers:
token = request.headers['x-access-token']
# return 401 if token is not passed
if not token:
return jsonify({'message' : 'Token is missing !!'}), 401
try:
# decoding the payload to fetch the stored details
data = jwt.decode(token, app.config['SECRET_KEY'])
current_user = User.query\
.filter_by(public_id = data['public_id'])\
.first()
except:
return jsonify({
'message' : 'Token is invalid !!'
}), 401
# returns the current logged in users contex to the routes
return f(current_user, *args, **kwargs)
return decorated
# User Database Route
# this route sends back list of users users
@app.route('/user', methods =['GET'])
@token_required
def get_all_users(current_user):
# querying the database
# for all the entries in it
users = User.query.all()
# converting the query objects
# to list of jsons
output = []
for user in users:
# appending the user data json
# to the response list
output.append({
'public_id': user.public_id,
'name' : user.name,
'email' : user.email
})
return jsonify({'users': output})
# route for loging user in
@app.route('/login', methods =['POST'])
def login():
# creates dictionary of form data
auth = request.form
if not auth or not auth.get('email') or not auth.get('password'):
# returns 401 if any email or / and password is missing
return make_response(
'Could not verify',
401,
{'WWW-Authenticate' : 'Basic realm ="Login required !!"'}
)
user = User.query\
.filter_by(email = auth.get('email'))\
.first()
if not user:
# returns 401 if user does not exist
return make_response(
'Could not verify',
401,
{'WWW-Authenticate' : 'Basic realm ="User does not exist !!"'}
)
if check_password_hash(user.password, auth.get('password')):
# generates the JWT Token
token = jwt.encode({
'public_id': user.public_id,
'exp' : datetime.utcnow() + timedelta(minutes = 30)
}, app.config['SECRET_KEY'])
return make_response(jsonify({'token' : token.decode('UTF-8')}), 201)
# returns 403 if password is wrong
return make_response(
'Could not verify',
403,
{'WWW-Authenticate' : 'Basic realm ="Wrong Password !!"'}
)
# signup route
@app.route('/signup', methods =['POST'])
def signup():
# creates a dictionary of the form data
data = request.form
# gets name, email and password
name, email = data.get('name'), data.get('email')
password = data.get('password')
# checking for existing user
user = User.query\
.filter_by(email = email)\
.first()
if not user:
# database ORM object
user = User(
public_id = str(uuid.uuid4()),
name = name,
email = email,
password = generate_password_hash(password)
)
# insert user
db.session.add(user)
db.session.commit()
return make_response('Successfully registered.', 201)
else:
# returns 202 if user already exists
return make_response('User already exists. Please Log in.', 202)
if __name__ == "__main__":
# setting debug to True enables hot reload
# and also provides a debuger shell
# if you hit an error while running the server
app.run(debug = True)
總結
大部分語言都已經支援了JWT,這裡可以從jwt.io
的類庫中可以看出。目前JWT主要運用於OAuth1、OAuth2和OpenID等單點登入功能,而且將來會有更多的企業和系統開發需要使用JWT技術。而且我也非常感謝本文中引用的原作者提供了相關的材料,便於我們學習。