肝!一文讲解JWT用户认证全过程
什么是JWT(what)
JWT(JSON Web Token)是一個開放標準(RFC 7519),它定義了一種緊湊且自包含的方式,以JSON對象的形式在各方之間安全地傳輸信息。
JWT是一個數字簽名,生成的信息是可以驗證并被信任的。
使用密鑰(使用HMAC算法)或使用RSA或ECDSA的公鑰/私鑰對JWT進行簽名。
JWT是目前最流行的跨域認證解決方案
JWT令牌結構
SON Web令牌以緊湊的形式由三部分組成,這些部分由點(.)分隔,分別是:
Header
Payload
Signature
即為:xxxx.yyyy.zzzz
Header
Header通常由兩部分組成:令牌的類型(即JWT)和所使用的簽名算法(例如HMAC SHA256或RSA)。例如:
{"alg": "HS256","typ": "JWT" }Header會被Base64Url編碼為JWT的第一部分。即為:
Payload
Payload是有關實體(通常是用戶)和其他數據的聲明,它包含三部分:
注冊聲明
這些是一組預定義的權利要求,不是強制性的,而是建議使用的,以提供一組有用的可互操作的權利要求。其中一些是:iss(JWT的簽發者), exp(expires,到期時間), sub(主題), aud(JWT接收者),iat(issued at,簽發時間)等。
注意:聲明名稱都是三個字符
公開聲明
公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因為該部分在客戶端可解密。
私有聲明
私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為base64是對稱解密的,意味著該部分信息可以歸類為明文信息。
例子:
{ "iat": 1593955943, "exp": 1593955973, "uid": 10, "username": "test", "scopes": [ "admin", "user" ] }Payload會被Base64Url編碼為JWT的第二部分。即為:
$ echo -n '{"iat":1593955943,"exp":1593955973,"uid":10,"username":"test","scopes":["admin","user"]}'|base64 eyJ1c2VybmFtZSI6InRlc3QiLCJpYXQiOjE1OTM5NTU5NDMsInVpZCI6MTAsImV4cCI6MTU5Mzk1NTk3Mywic2NvcGVzIjpbImFkbWluIiwidXNlciJdfQ注意:對于已簽名的令牌,此信息盡管可以防止篡改,但任何人都可以讀取。除非將其加密,否則請勿將機密信息放入JWT的有效負載或報頭元素中。
Signature
Signature部分的生成需要base64編碼之后的Header,base64編碼之后的Payload,密鑰(secret),Header需要指定簽字的算法。
例如,如果要使用HMAC SHA256算法,則將通過以下方式創建簽名:
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)整合在一起
輸出是三個由點分隔的Base64-URL字符串,可以在HTML和HTTP環境中輕松傳遞這些字符串,與基于XML的標準(例如SAML)相比,它更緊湊。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJpYXQiOjE1OTM5NTU5NDMsInVpZCI6MTAsImV4cCI6MTU5Mzk1NTk3Mywic2NvcGVzIjpbImFkbWluIiwidXNlciJdfQ.VHpxmxKVKpsn2Iytqc_6Z1U1NtiX3EgVki4PmA-J3PgJWT是無狀態授權機制,服務器的受保護路由將Header中檢查有效的token,如果存在,則將允許用戶訪問受保護的資源。如果JWT包含必要的數據,則可以減少查詢數據庫中某些操作的需求。
什么時候使用JWT(when)
授權:一旦用戶登錄,每個后續請求將包括JWT,從而允許用戶訪問該令牌允許的路由,服務和資源。單一登錄是當今廣泛使用JWT的一項功能,因為它的開銷很小并且可以在不同的域中輕松使用。這也是JWT最常見的方案。
信息交換:JSON Web令牌是各方之間安全地傳輸信息的好辦法。對JWT進行簽名,所以您可以確保發件人是他們所說的人。由于,簽名可以設置有效時長,可以驗證內容是否遭到篡改。
如何使用JWT(how)
JWT工作流程
根據下面的這張流程圖來分析一下JWT的工作過程
1 用戶登錄:提供用戶名和密碼;
2 JWT生成token和refreshtoken,返回客戶端;(注意:refreshtoken的過期時間長于token的過期時間)
3 客戶端保存token和refresh_token,并攜帶token,請求服務端資源;
4 服務端判斷token是否過期,若沒有過期,則解析token獲取認證相關信息,認證通過后,將服務器資源返回給客戶端;
5 服務端判斷token是否過期,若token已過期,返回token過期提示;
6 客戶端獲取token過期提示后,用refresh_token接著繼續上一次請求;
7 服務端判斷refreshtoken是否過期,若沒有過期,則生成新的token和refreshtoken,并返回給客戶端,客戶端丟棄舊的token,保存新的token;
8 服務端判斷refreshtoken是否過期,若refreshtoken已過期,則返回給客戶端token過期,需要重新登錄的提示。
python+flask+JWT實戰
import timefrom functools import wraps from flask import Flask, request, jsonify import jwt from jwt import ExpiredSignatureErrorapp = Flask(__name__)max_time = 60 refresh_max_time = 120 token_secret = "This is a secret"def verify_token(func):@wraps(func)def decorator(*args, **kwargs):try:token = request.headers["token"]print(token)data = jwt.decode(token, token_secret, algorithms=['HS256'])now = int(time.time())time_interval = now - data['time']if time_interval >= max_time:# create new tokentoken, refresh_token = creat_token()return jsonify({"token": token, "refresh_token": refresh_token})except ExpiredSignatureError:return "Token expired"except Exception as ex:print(ex)return "Log in again"return func(*args, **kwargs)return decoratordef creat_token(uid):now = int(time.time())payload = {'uid': uid, 'time': now, 'exp': now + max_time}refresh_payload = {'uid': uid, 'time': now, 'exp': now + refresh_max_time}token = jwt.encode(payload, token_secret, algorithm='HS256')refresh_token = jwt.encode(refresh_payload, token_secret, algorithm='HS256')return token, refresh_token@app.route('/login', methods=["POST"]) def login():user_name = request.values.get('user_name')password = request.values.get('password')# @TODO 根據user_name和password 獲取唯一的uiduid = 10token, refresh_token = creat_token(uid=uid)return jsonify({"token": token, "refresh_token": refresh_token})@app.route('/test', methods=['GET']) @verify_token def test():return 'hello world'if __name__ == "__main__":app.run(host="0.0.0.0", port=5000)第三方庫-itsdangerous
isdangerous簡介
itsdangerous支持JSON Web 簽名 (JWS),內部默認使用了HMAC和SHA1來簽名,其中類 JSONWebSignatureSerializer內部與JWT一致,也分成三部分(header,payload,signature),查看源碼可知:
def dumps(self, obj, salt=None, header_fields=None):"""Like :meth:`.Serializer.dumps` but creates a JSON WebSignature. It also allows for specifying additional fields to beincluded in the JWS header."""header = self.make_header(header_fields)signer = self.make_signer(salt, self.algorithm)return signer.sign(self.dump_payload(header, obj)) def dump_payload(self, header, obj):base64d_header = base64_encode(self.serializer.dumps(header, **self.serializer_kwargs))base64d_payload = base64_encode(self.serializer.dumps(obj, **self.serializer_kwargs))return base64d_header + b"." + base64d_payloadobj保存用戶相關信息,類似JWT中的payload
base64url對obj和header進行編碼之后,使用?.拼接
將拼接之后的數據,作為signer的輸入以及初始化?__init__中用戶定義的secret來生成新的token
感興趣的朋友可以直接參看github源碼,這里不再展開贅述。
python+flask+isdangerous實戰
import?timefrom functools import wraps from flask import Flask, request, jsonify from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, SignatureExpiredapp = Flask(__name__)max_time = 60 refresh_max_time = 120 token_secret = "This is a secret"def verify_token(func):@wraps(func)def decorator(*args, **kwargs):try:token = request.headers["token"]print(token)s = Serializer(token_secret)data = s.loads(token)now = int(time.time())time_interval = now - data['time']if time_interval >= max_time:# create new tokentoken, refresh_token = creat_token()return jsonify({"token": token, "refresh_token": refresh_token})except SignatureExpired:return "Token expired"except Exception as ex:print(ex)return "Log in again"return func(*args, **kwargs)return decoratordef creat_token(uid):now = int(time.time())s = Serializer(token_secret, expires_in=max_time)token = s.dumps({"uid": uid, "time": now}).decode("ascii")refresh_s = Serializer(token_secret, expires_in=refresh_max_time)refresh_token = refresh_s.dumps({"uid": uid, "time": now}).decode("ascii")return token, refresh_token@app.route('/token', methods=["POST"]) def token():user_name = request.values.get('user_name')password = request.values.get('password')# @TODO 根據user_name和password 獲取唯一的uiduid = 10token, refresh_token = creat_token(uid=uid)return jsonify({"token": token, "refresh_token": refresh_token})@app.route('/test', methods=['GET']) @verify_token def test():return 'hello world'if __name__ == "__main__":app.run(host="0.0.0.0")TimedJSONWebSignatureSerializer相比 JSONWebSignatureSerializer在header中贈加了過期時間,如果過期會拋出 SignatureExpired異常。
問題
用戶登出,如何設置token無效?
JWT是無狀態的,用戶登出設置token無效就已經違背了JWT的設計原則,但是在實際應用場景中,這種功能是需要的,那該如何實現呢?提供幾種思路:
用戶登出,瀏覽器端丟棄token
使用redis數據庫,用戶登出,從redis中刪除對應的token,請求訪問時,需要從redis庫中取出對應的token,若沒有,則表明已經登出
使用redis,兩個不同的設備,一個設備登出,另外一個設備如何處理?
請思考這樣一種場景:
同一個用戶從兩個設備登陸到服務端(設備1,設備2);
設備1登出,刪除redis中的對應的token
設備2再次請求數據時,redis中的數據為空,需要重新登錄。
很明顯,這種情況是不應該出現的,說一下自己的想法:
每一個設備與用戶生成唯一的key,保存在redis中,即設備1的用戶登出,只刪除對應的token,設備2的token仍然存在
服務器端維護一個版本號,相同用戶不同設備登入,版本號加1,這樣保持key的唯一性(和上面差不多)
?往期推薦?
????
實戰|利用機器學習解決一個多分類任務
某程序員動了公司的祖傳代碼“屎山”,半年后怒交辭職報告!
告別刷抖音!30秒一個Python小例子,總有一款適合你
這個只有1.5M的軟件,能讓你的網速快3倍
- End -
最后說一個題外話,相信大家有不少人開通了視頻號。小詹也開通了一個視頻號,會分享互聯網那些事、讀書心得與副業經驗,歡迎各位掃描下方二維碼關注。
總結
以上是生活随笔為你收集整理的肝!一文讲解JWT用户认证全过程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大写牛逼,用 Python 登录主流 2
- 下一篇: 被遗忘的 10 个Linux命令,很实用