基於Token的身份驗證
在實現登錄功能的時候,正常的B/S應用都會使用cookie+session的方式來做身份驗證,后台直接向cookie中寫數據,但是由於移動端的存在,移動端是沒有cookie機制的,所以使用token可以實現移動端和客戶端的token通信。
驗證流程
整個基於Token的驗證流程如下:
- 客戶端使用用戶名跟密碼請求登錄
- 服務器收到請求,去驗證用戶名和密碼
- 驗證成功后,服務端會簽發一個Token,再把這個Token發送到客戶端
- 客戶端收到的Token以后可以把它存儲起來,比如放在Cookie或LocalStorage里
- 客戶端每次向服務器發送其他請求的時候都要帶着服務器簽發的Token
- 服務器收到請求,去驗證客戶端請求里面帶着的Token,如果驗證成功,就像客戶端返回請求的數據
JWT
構造Token的方法挺多的,可以說只要是客戶端和服務器端約定好了格式,是想怎么寫就怎么寫的,然而還有一些標准寫法,例如JWT讀作/jot/,表示:JSON Web Tokens.
JWT標准的Token有三個部分:
- header
- payload
- signature
三個部分會用點分割開,並且都會使用Base64編碼,所以真正的Token看起來像這樣
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
Header
header部分主要是兩部分內容,一個是Token的類型,另一個是使用的算法,比如下面的類型就是JWT,使用的算法是HS256:
{
"typ": "JWT",
"alg": "HS256"
}
上面的內容要用 Base64 的形式編碼一下,所以就變成這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload
Payload 里面是 Token 的具體內容,這些內容里面有一些是標准字段,你也可以添加其它需要的內容。下面是標准字段:
- iss:Issuer,發行者
- sub:Subject,主題
- aud:Audience,觀眾
- exp:Expiration time,過期時間
- nbf:Not before
- iat:Issued at,發行時間
- jti:JWT ID
Signature
JWT的最后一部分是Signature,這部分相當於前兩段的摘要,用來防止其他人來篡改Token中的信息,在處理時可以首先將前兩段生成的內容使用Base64生成一下再加鹽然后利用MD5等摘要算法在生成一遍
服務端生成Token
在服務端生成Token的時候,需要解決兩個問題
- 使用什么加密算法
- Token如何存儲
加密算法
這里的加密算法並不是MD5,SHA1這樣的哈希算法,因為這種算法是無法解密的,只能用來生成摘要,在Django中內置了一個加密前面模塊django.core.signing模塊,可以用來加密和解密任何數據,使用簽名模塊的dumps和load函數來實現
示例:
from django.core import signing
value = signing.dumps({"foo":"bar"})
src = signing.loads(value)
print(value)
print(src)
結果:
eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI
{‘foo’: ‘bar’}
Token如何存儲
用什么存儲
在服務器中Token可以存儲在內存中,因為本質是字符串,所以並不會占用很大的內存空間,如果是分布式的存儲可以將所有的token信息分段存儲在不同的服務器中,也可以存儲在數據庫中,在Django中提供了緩存類,可以用來存儲Token,Django的緩存可以結合Redis來使用,可以借助django-redis來實現
安裝
pip install django-redis
配置
為了使用django-redis,需要將django cache setting修改,修改settings.py,默認是沒有Cache的配置信息的,在其中添加:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
當然,需要你首先安裝了redis
cache使用
cache使用的時候基本可以使用set和get方法來進行存/取數據
>>> cache.set('my_key', 'hello, world!', 30)
>>> cache.get('my_key')
'hello, world!'
如何存
由於redis是使用k-v模式來進行存儲數據的,我們可以使用用戶名作為key,而token信息作為value,相較於直接使用token作為key的方式,好處是我們可以使用更少的空間實現一些功能,例如當用戶修改了密碼或點擊注銷之后,它的token可以直接失效,直接將該用戶名所對應的數據刪除就好了,或者用戶在一次登錄成功后,又一次請求了登錄接口,我們可以很簡單的更新該用戶的token信息,而這樣存儲所依賴於我們的token可以進行解密,如果你是直接生成了一串無法解密的數據作為token,不能使用用戶名作為token了
code
import time
from django.core import signing
import hashlib
from django.core.cache import cache
HEADER = {'typ': 'JWP', 'alg': 'default'}
KEY = 'CHEN_FENG_YAO'
SALT = 'www.lanou3g.com'
TIME_OUT = 30 * 60 # 30min
def encrypt(obj):
"""加密"""
value = signing.dumps(obj, key=KEY, salt=SALT)
value = signing.b64_encode(value.encode()).decode()
return value
def decrypt(src):
"""解密"""
src = signing.b64_decode(src.encode()).decode()
raw = signing.loads(src, key=KEY, salt=SALT)
print(type(raw))
return raw
def create_token(username):
"""生成token信息"""
# 1. 加密頭信息
header = encrypt(HEADER)
# 2. 構造Payload
payload = {"username": username, "iat": time.time()}
payload = encrypt(payload)
# 3. 生成簽名
md5 = hashlib.md5()
md5.update(("%s.%s" % (header, payload)).encode())
signature = md5.hexdigest()
token = "%s.%s.%s" % (header, payload, signature)
# 存儲到緩存中
cache.set(username, token, TIME_OUT)
return token
def get_payload(token):
payload = str(token).split('.')[1]
payload = decrypt(payload)
return payload
# 通過token獲取用戶名
def get_username(token):
payload = get_payload(token)
return payload['username']
pass
def check_token(token):
username = get_username(token)
last_token = cache.get(username)
if last_token:
return last_token == token
return False