鑒權(quán)和權(quán)限管理的探索

傳統(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ǔ);
image
  • 登錄,用戶(hù)發(fā)送用戶(hù)名和密碼,服務(wù)器鑒定是否成功,token方式成功后返回token;
image
  • 鑒權(quán),用戶(hù)將token發(fā)送給服務(wù)器,服務(wù)器鑒定token是否legal。
image

這個(gè)過(guò)程可能的問(wèn)題包括

  1. 密碼泄漏
  2. 生成token的secret key/salt泄漏
  3. 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不是保密的。
安全策略:

  1. 用戶(hù)注冊(cè)的時(shí)候,根據(jù)密碼+隨機(jī)生成的鹽生成新的加密密碼存儲(chǔ)于數(shù)據(jù)庫(kù),同時(shí)每個(gè)用戶(hù)隨機(jī)的鹽也存放在數(shù)據(jù)庫(kù);
  2. 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ì)

image
  • 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策略

image
  • 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容