傳統(tǒng)鑒權(quán)方案是通過(guò)Session ID完成的,現(xiàn)在一般使用Token鑒權(quán)。
1.認(rèn)證與鑒權(quán)
- 分布式session
用戶(hù)認(rèn)證信息存儲(chǔ)在共享存儲(chǔ)中,通常以用戶(hù)會(huì)話(huà)作為key來(lái)實(shí)現(xiàn)簡(jiǎn)單分布式哈希映射。優(yōu)點(diǎn)是高可用且可擴(kuò)展,缺點(diǎn)是共享存儲(chǔ)需要安全鏈接等復(fù)雜的保護(hù)機(jī)制。
- OAuth2 Token方案
相比于Session,token方案一般會(huì)包含更豐富的用戶(hù)信息,token一般放在HTTP請(qǐng)求頭中。有點(diǎn)是服務(wù)器無(wú)狀態(tài),token驗(yàn)證不需要訪問(wèn)數(shù)據(jù)庫(kù)所以性能更好,支持多端(Web和移動(dòng)端等)
2.基本流程
最簡(jiǎn)單的用戶(hù)系統(tǒng)的功能包括注冊(cè)、登錄和鑒權(quán)三個(gè)方面
- 注冊(cè),用戶(hù)發(fā)送用戶(hù)名和密碼,服務(wù)器存儲(chǔ);

- 登錄,用戶(hù)發(fā)送用戶(hù)名和密碼,服務(wù)器鑒定是否成功,token方式成功后返回token;

- 鑒權(quán),用戶(hù)將token發(fā)送給服務(wù)器,服務(wù)器鑒定token是否legal。

這個(gè)過(guò)程可能的問(wèn)題包括
- 密碼泄漏
- 生成token的secret key/salt泄漏
- token泄漏/偽造
3.JWT
JWT(JSON Web Token)是一種典型的token,由header+payload+signature組成。
- header,包括加密方式和token類(lèi)型
{
"alg": "HS256",
"typ": "JWT"
}
- paload,包括用戶(hù)信息
{
"id": 1,
"name": "Tom",
"role": "admin"
}
- signature,由header和payload的Base64值+secret key生成的字符串,再對(duì)該字符串使用header規(guī)定的散列方式(一般是HS256)取散列值后得到的字符串
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
因?yàn)镠S256是可逆的,所以嚴(yán)格意義上來(lái)講JWT不是保密的。
安全策略:
- 用戶(hù)注冊(cè)的時(shí)候,根據(jù)密碼+隨機(jī)生成的鹽生成新的加密密碼存儲(chǔ)于數(shù)據(jù)庫(kù),同時(shí)每個(gè)用戶(hù)隨機(jī)的鹽也存放在數(shù)據(jù)庫(kù);
- JWT唯一的安全信息是secret key,使用時(shí)間戳 + 用戶(hù)名的MD5值作為secret key。
前面提到的安全問(wèn)題就都解決了,獲取的密碼是每個(gè)用戶(hù)唯一salt加密后的值,token不存儲(chǔ)在服務(wù)器或者數(shù)據(jù)庫(kù),secret key是計(jì)算而得的值。
為了提升服務(wù)器效率,可以用redis緩存每個(gè)用戶(hù)的salt,但不要緩存token,會(huì)引發(fā)數(shù)據(jù)庫(kù)token泄漏的風(fēng)險(xiǎn)。
4.權(quán)限管理
4.1資源與狀態(tài)轉(zhuǎn)換
配合JWT的資源訪問(wèn)模式一般是REST,representational state transfer,這種模式重點(diǎn)是資源和狀態(tài)轉(zhuǎn)換。
資源是網(wǎng)上實(shí)體,包括文本、圖片、音頻、視頻、服務(wù)等等;狀態(tài)轉(zhuǎn)換是HTTP協(xié)議里四種基本狀態(tài)的操作:GET(瀏覽資源browse), POST(新建資源create), PUT(更新資源update), DELETE(刪除資源delete)。
在權(quán)限管理的話(huà)題下,即討論用戶(hù)關(guān)于轉(zhuǎn)換資源狀態(tài)的管理。
用戶(hù)的資源一般分為三類(lèi):
- 私人資源Personal Source
某個(gè)用戶(hù)私有資源,只有用戶(hù)本人能操作,例如訂單信息、收貨地址等 - 角色資源Roles Source
角色可以包含多個(gè)用戶(hù),同角色用戶(hù)享有規(guī)定好的權(quán)限 - 公共資源Public Source
無(wú)差別用戶(hù),任意角色都能訪問(wèn)并操作資源
4.2權(quán)限
權(quán)限就是資源與操作的組合,所以任何一種資源的權(quán)限有且只有四種,瀏覽、新建、更新、刪除資源。
角色和用戶(hù)是多對(duì)多的關(guān)系:一個(gè)角色對(duì)應(yīng)多個(gè)用戶(hù),一個(gè)用戶(hù)對(duì)應(yīng)多個(gè)角色;
角色與權(quán)限是多對(duì)一的關(guān)系:一個(gè)權(quán)限對(duì)應(yīng)一個(gè)角色,一個(gè)角色對(duì)應(yīng)多個(gè)權(quán)限;
權(quán)限與用戶(hù)是多對(duì)多的關(guān)系:一個(gè)用戶(hù)對(duì)應(yīng)多個(gè)權(quán)限,一個(gè)權(quán)限對(duì)應(yīng)多個(gè)用戶(hù)。
4.3表設(shè)計(jì)

- source表
permissions字段:1個(gè)資源有4個(gè)權(quán)限——CRUD; - permission表
name字段:source某條記錄的唯一標(biāo)識(shí)identity;
action字段:對(duì)資源的操作,只能是CRUD之一;
relation字段:標(biāo)記資源類(lèi)型,私人、角色或者公共;
roles字段:擁有該權(quán)限的角色;
4.4策略

- SessionAuthPolicy
檢測(cè)用戶(hù)是否已經(jīng)登錄,用戶(hù)登錄是進(jìn)行下面檢測(cè)的前提。 - SourcePolicy
檢測(cè)訪問(wèn)的資源是否存在,主要檢測(cè)Source表的記錄 - PermissionPolicy
檢測(cè)該用戶(hù)所屬的角色,是否有對(duì)所訪問(wèn)資源進(jìn)行對(duì)應(yīng)操作的權(quán)限。 - OwnerPolicy
如果所訪問(wèn)的資源屬于私人資源,則檢測(cè)當(dāng)前用戶(hù)是否該資源的擁有者。
5.實(shí)踐
通過(guò)JWT進(jìn)行鑒權(quán),在API的views層進(jìn)行權(quán)限管理。以WORKER登出作為例子說(shuō)明如何實(shí)現(xiàn)鑒權(quán)和權(quán)限管理。
通過(guò)enum管理roles,方便后續(xù)代碼修改,而不是用硬編碼的magic number實(shí)現(xiàn)
# enum.py
from enum import Enum
# 命名最好以Enum結(jié)尾,避免在應(yīng)用中和其他package關(guān)于role的namespace沖突,同時(shí)可讀性更強(qiáng)
class RoleEnum(Enum):
"""
用戶(hù)角色
"""
ADMIN = 1
COMPANY = 2
WORKER = 3
class ApiExceptionEnum(Enum):
"""
接口異常
"""
InvalidInput = 10000
建立roles的權(quán)限規(guī)則,即role對(duì)API的endpoint的CRUD的權(quán)限
# role.py
from enum import RoleEnum
Role = {
RoleEnum.WORKER.value: {
"worker.sign_out": {"GET": True}
# WORKER的role在worker.sign_out的endpoint有GET的權(quán)限
}
}
建立好權(quán)限規(guī)則后,接著要實(shí)現(xiàn)鑒權(quán)和權(quán)限管理。
通過(guò)JWT實(shí)現(xiàn)鑒權(quán)
# token.py
import datetime
import jwt # JWT package,需要其提供的encode和decode的方法,也可以DIY實(shí)現(xiàn)
from enum import ApiExceptionEnum
secret = "some_secret_string" # JWT signature的secret,可以是動(dòng)態(tài)生成的字符串
class Token:
def __init__(self, role):
self.role = role # Token實(shí)例規(guī)定帶有role,方便后續(xù)的權(quán)限管理
def encode_token(self, id_, login_at):
try:
# algorithm一般是HS256
header = {'algorithm': 'HS256', 'type': 'JWT'}
payload = {
# 發(fā)行時(shí)間
'iat': datetime.datetime.utcnow(),
# token簽發(fā)者
'iss': 'some_authority',
# jwt面向的角色
'sub': self.role,
# 私有聲明
'data': {
'id': id_,
'login_at': login_at
}
}
token = jwt.encode(payload, secret], headers=header)
return token
except Exception:
raise Exception(ApiExceptionEnum.InvalidInput.value, '無(wú)效token')
# decode是靜態(tài)方法,不需要綁定role
@staticmethod
def decode_token(token):
try:
payload = jwt.decode(token, secret, options={'verify_exp': False})
return payload
except jwt.ExpiredSignatureError:
raise Exception(ApiExceptionEnum.InvalidInput.value, 'token已過(guò)期')
except jwt.InvalidTokenError:
raise Exception(ApiExceptionEnum.InvalidInput.value, '無(wú)效token')
為了方便對(duì)所有API的管理,使用python的decorator實(shí)現(xiàn)。
# permission.py
from functools import wraps
from flask import request
# flask框架的request請(qǐng)求信息查詢(xún),其他框架只需要import相關(guān)request信息即可
from enum import ApiExceptionEnum, RoleEnum
from role import Role
from token import Token
def access_control(func):
@wraps(func)
def wrap_func(*args, **kwargs):
auth_header = request.headers.get('Authorization')
http_method = request.method
http_route = request.endpoint.split('.')
endpoint = http_route[0] + '.' + http_route[-1]
if not auth_header:
raise Exception(ApiExceptionEnum.InvalidInput.value, "請(qǐng)求頭錯(cuò)誤")
auth_attr = auth_header.split(' ')
if not auth_attr or auth_attr[0] != 'JWT' or len(auth_attr) != 2:
raise Exception(ApiExceptionEnum.InvalidInput.value, "token格式錯(cuò)誤")
jwt_token = auth_attr[1]
payload = Token.decode_token(jwt_token)
role = payload.get('sub')
id_ = payload.get('data').get('id')
if not Role[role][endpoint][http_method]:
raise Exception(ApiExceptionEnum.InvalidInput.value, "無(wú)權(quán)訪問(wèn)")
return func(id_, *args, **kwargs)
return wrap_func
這樣就可以使用鑒權(quán)和權(quán)限管理
# account.py
@blueprint.route('/sign_out', methods=['GET']) # flask的藍(lán)圖URI注冊(cè)
@access_control
def sign_out(worker_id):
"""
用戶(hù)登出(Worker)
:return:
"""
message = sign_out(worker_id)
return jsonify(message)
def sign_out(worker_id):
... # do something,關(guān)于業(yè)務(wù)邏輯的部分
return {'message': 'success'}
參考
http://www.itdecent.cn/p/db65cf48c111
http://www.itdecent.cn/p/b78744bd463b
https://zhuanlan.zhihu.com/p/28295641