一、JWT:
在用戶注冊或登錄后,我們想記錄用戶的登錄狀態,或者為用戶創建身份認證的憑證。我們不再使用Session認證機制,而使用Json Web Token認證機制。
1.什么是JWT:
Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標准((RFC 7519).該token被設計為緊湊且安全的,特別適用於分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
1.1 起源:
說起JWT,我們應該來談一談基於token的認證和傳統的session認證的區別。
1.1.1 傳統的session認證:
我們知道,http協議本身是一種無狀態的協議,而這就意味着如果用戶向我們的應用提供了用戶名和密碼來進行用戶認證,那么下一次請求時,用戶還要再一次進行用戶認證才行,因為根據http協議,我們並不能知道是哪個用戶發出的請求,所以為了讓我們的應用能識別是哪個用戶發出的請求,我們只能在服務器存儲一份用戶登錄的信息,這份登錄信息會在響應時傳遞給瀏覽器,告訴其保存為cookie,以便下次請求時發送給我們的應用,這樣我們的應用就能識別請求來自哪個用戶了,這就是傳統的基於session認證。
但是這種基於session的認證使應用本身很難得到擴展,隨着不同客戶端用戶的增加,獨立的服務器已無法承載更多的用戶,而這時候基於session認證應用的問題就會暴露出來.
基於session認證所顯露的問題
session: 每個用戶經過我們的應用認證之后,我們的應用都要在服務端做一次記錄,以方便用戶下次請求的鑒別,通常而言session都是保存在內存中,而隨着認證用戶的增多,服務端的開銷會明顯增大。
擴展性: 用戶認證之后,服務端做認證記錄,如果認證的記錄被保存在內存中的話,這意味着用戶下次請求還必須要請求在這台服務器上,這樣才能拿到授權的資源,這樣在分布式的應用上,相應的限制了負載均衡器的能力。這也意味着限制了應用的擴展能力。
CSRF: 因為是基於cookie來進行用戶識別的, cookie如果被截獲,用戶就會很容易受到跨站請求偽造的攻擊。
1.1.2 基於token的鑒權機制:
基於token的鑒權機制類似於http協議也是無狀態的,它不需要在服務端去保留用戶的認證信息或者會話信息。這就意味着基於token認證機制的應用不需要去考慮用戶在哪一台服務器登錄了,這就為應用的擴展提供了便利。
流程上是這樣的:
用戶使用用戶名密碼來請求服務器
服務器進行驗證用戶的信息
服務器通過驗證發送給用戶一個token
客戶端存儲token,並在每次請求時附送上這個token值
服務端驗證token值,並返回數據
這個token必須要在每次請求時傳遞給服務端,它應該保存在請求頭里, 另外,服務端要支持CORS(跨來源資源共享)策略,一般我們在服務端這么做就可以了Access-Control-Allow-Origin: *。
2. JWT長什么樣?
JWT是由三段信息構成的,將這三段信息文本用.鏈接一起就構成了Jwt字符串。就像這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
2.1 JWT的構成:
第一部分我們稱它為頭部(header);
第二部分我們稱其為載荷(payload, 類似於飛機上承載的物品);
第三部分是簽證(signature).
2.1.1 header:
jwt的頭部承載兩部分信息:
聲明類型,這里是jwt
聲明加密的算法 通常直接使用 HMAC SHA256
完整的頭部就像下面這樣的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后將頭部被進行base64加密(該加密是可以被對稱解密的), 構成第一部分
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
2.1.2 payload
在和就是存放有效信息的地方。這個名字像是特指飛機上承載的貨品,這些有效信息包含三個部分
標准中注冊的聲明
公共的聲明
私有的聲明
標准中注冊的聲明 (建議但不強制使用) :
iss: jwt簽發者
sub: jwt所面向的用戶
aud: 接收jwt的一方
exp: jwt的過期時間,這個過期時間必須要大於簽發時間
nbf: 定義在什么時間之前,該jwt都是不可用的.
iat: jwt的簽發時間
jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
公共的聲明 : 公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因為該部分在客戶端可解密.
私有的聲明 : 私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為base64是對稱解密的,意味着該部分信息可以歸類為明文信息。
定義一個payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
2.1.3 signature
JWT的第三部分是一個簽證信息,這個簽證信息由三部分組成:
header (base64后的)
payload (base64后的)
secret
這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret組合加密,然后就構成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
將這三部分用 . 連接成一個完整的字符串,構成了最終的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
3. 如何應用:
一般是在請求頭里加入Authoriztion, 並加上Beaer標注:
fetch('api/user/1', {
headers: {
'Authorization': 'Bearer ' + token
}
})
服務端會驗證token,如果驗證通過就會返回相應的資源。整個流程就是這樣的:
4.總結
4.1優點
因為json的通用性,所以JWT是可以進行跨語言支持的,像JAVA,JavaScript,NodeJS,PHP等很多語言都可以使用。
因為有了payload部分,所以JWT可以在自身存儲一些其他業務邏輯所必要的非敏感信息。
便於傳輸,jwt的構成非常簡單,字節占用很小,所以它是非常便於傳輸的。
它不需要在服務端保存會話信息, 所以它易於應用的擴展
4.2安全相關
不應該在jwt的payload部分存放敏感信息,因為該部分是客戶端可解密的部分。
保護好secret私鑰,該私鑰非常重要。
如果可以,請使用https協議
二、Django REST framework JWT:
我們在驗證完用戶的身份后(檢驗用戶名和密碼), 需要向用戶欠打JWT,在需要用到用戶身份信息的時候,還需核驗用戶的JWT。
關於簽發和核驗JWT, 我們可以使用Django REST framework JWT擴展來完成。
文檔網站:http://getblimp.github.io/django-rest-framework-jwt/
1.安裝配置:
安裝:
pip install djangorestframework-jwt
配置:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
}
# JWT_EXPIRATION_DELTA 指明token的有效期
2. 使用
Django REST framework JWT擴展的說明文檔中提供了手動簽發JWT的方法:
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
在注冊成功后,聯通返回token,需要在注冊視圖中創建token。
修改CreateUserSerializer序列化器,在create方法中增加手動創建token的方法
from rest_framework_jwt.settings import api_settings
class CreateUserSerializer(serializers.ModelSerializer):
"""
創建用戶序列化器
"""
...
token = serializers.CharField(label='登錄狀態token', read_only=True) # 增加token字段
class Meta:
...
fields = ('id', 'username', 'password', 'password2', 'sms_code', 'mobile', 'allow', 'token') # 增加token
...
def create(self, validated_data):
"""
創建用戶
"""
# 移除數據庫模型類中不存在的屬性
del validated_data['password2']
del validated_data['sms_code']
del validated_data['allow']
user = super().create(validated_data)
# 調用django的認證系統加密密碼
user.set_password(validated_data['password'])
user.save()
# 補充生成記錄登錄狀態的token
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
user.token = token
return user
前端保存token
我們可以將JWT保存在cookie中,也可以保存在瀏覽器的本地存儲里,我們保存在瀏覽器本地存儲中
瀏覽器的本地存儲提供了sessionStorage 和 localStorage 兩種:
sessionStorage 瀏覽器關閉即失效
localStorage 長期有效
使用方法
sessionStorage.變量名 = 變量值 // 保存數據
sessionStorage.變量名 // 讀取數據
sessionStorage.clear() // 清除所有sessionStorage保存的數據
localStorage.變量名 = 變量值 // 保存數據
localStorage.變量名 // 讀取數據
localStorage.clear() // 清除所有localStorage保存的數據
在前端js/register.js文件中增加保存token
var vm = new Vue({
...
methods: {
...
on_submit: function(){
axios.post(...)
.then(response => {
// 記錄用戶的登錄狀態
sessionStorage.clear();
localStorage.clear();
localStorage.token = response.data.token;
localStorage.username = response.data.username;
localStorage.user_id = response.data.id;
location.href = '/index.html';
})
.catch(...)
}
}
})