鉴权和权限管理的探索

传统鉴权方案是通过Session ID完成的,现在一般使用Token鉴权。

1.认证与鉴权

  • 分布式session

用户认证信息存储在共享存储中,通常以用户会话作为key来实现简单分布式哈希映射。优点是高可用且可扩展,缺点是共享存储需要安全链接等复杂的保护机制。

  • OAuth2 Token方案

相比于Session,token方案一般会包含更丰富的用户信息,token一般放在HTTP请求头中。有点是服务器无状态,token验证不需要访问数据库所以性能更好,支持多端(Web和移动端等)

2.基本流程

最简单的用户系统的功能包括注册、登录和鉴权三个方面

  • 注册,用户发送用户名和密码,服务器存储;
image
  • 登录,用户发送用户名和密码,服务器鉴定是否成功,token方式成功后返回token;
image
  • 鉴权,用户将token发送给服务器,服务器鉴定token是否legal。
image

这个过程可能的问题包括

  1. 密码泄漏
  2. 生成token的secret key/salt泄漏
  3. token泄漏/伪造

3.JWT

JWT(JSON Web Token)是一种典型的token,由header+payload+signature组成。

  • header,包括加密方式和token类型
{
    "alg": "HS256",
    "typ": "JWT"
}
  • paload,包括用户信息
{
    "id": 1,
    "name": "Tom",
    "role": "admin"
}
  • signature,由header和payload的Base64值+secret key生成的字符串,再对该字符串使用header规定的散列方式(一般是HS256)取散列值后得到的字符串
HMACSHA256(
    base64UrlEncode(header) + "." + base64UrlEncode(payload),
    secret
)

因为HS256是可逆的,所以严格意义上来讲JWT不是保密的。
安全策略:

  1. 用户注册的时候,根据密码+随机生成的盐生成新的加密密码存储于数据库,同时每个用户随机的盐也存放在数据库;
  2. JWT唯一的安全信息是secret key,使用时间戳 + 用户名的MD5值作为secret key。

前面提到的安全问题就都解决了,获取的密码是每个用户唯一salt加密后的值,token不存储在服务器或者数据库,secret key是计算而得的值。

为了提升服务器效率,可以用redis缓存每个用户的salt,但不要缓存token,会引发数据库token泄漏的风险。

4.权限管理

4.1资源与状态转换

配合JWT的资源访问模式一般是REST,representational state transfer,这种模式重点是资源状态转换
资源是网上实体,包括文本、图片、音频、视频、服务等等;状态转换是HTTP协议里四种基本状态的操作:GET(浏览资源browse), POST(新建资源create), PUT(更新资源update), DELETE(删除资源delete)。
在权限管理的话题下,即讨论用户关于转换资源状态的管理。
用户的资源一般分为三类:

  • 私人资源Personal Source
    某个用户私有资源,只有用户本人能操作,例如订单信息、收货地址等
  • 角色资源Roles Source
    角色可以包含多个用户,同角色用户享有规定好的权限
  • 公共资源Public Source
    无差别用户,任意角色都能访问并操作资源

4.2权限

权限就是资源与操作的组合,所以任何一种资源的权限有且只有四种,浏览、新建、更新、删除资源。
角色和用户是多对多的关系:一个角色对应多个用户,一个用户对应多个角色;
角色与权限是多对一的关系:一个权限对应一个角色,一个角色对应多个权限;
权限与用户是多对多的关系:一个用户对应多个权限,一个权限对应多个用户。

4.3表设计

image
  • source表
    permissions字段:1个资源有4个权限——CRUD;
  • permission表
    name字段:source某条记录的唯一标识identity;
    action字段:对资源的操作,只能是CRUD之一;
    relation字段:标记资源类型,私人、角色或者公共;
    roles字段:拥有该权限的角色;

4.4策略

image
  • SessionAuthPolicy
    检测用户是否已经登录,用户登录是进行下面检测的前提。
  • SourcePolicy
    检测访问的资源是否存在,主要检测Source表的记录
  • PermissionPolicy
    检测该用户所属的角色,是否有对所访问资源进行对应操作的权限。
  • OwnerPolicy
    如果所访问的资源属于私人资源,则检测当前用户是否该资源的拥有者。

5.实践

通过JWT进行鉴权,在API的views层进行权限管理。以WORKER登出作为例子说明如何实现鉴权和权限管理。

通过enum管理roles,方便后续代码修改,而不是用硬编码的magic number实现

# enum.py

from enum import Enum

# 命名最好以Enum结尾,避免在应用中和其他package关于role的namespace冲突,同时可读性更强
class RoleEnum(Enum):  
    """
    用户角色
    """
    ADMIN = 1
    COMPANY = 2
    WORKER = 3
    
class ApiExceptionEnum(Enum):
    """
    接口异常
    """
    InvalidInput = 10000

建立roles的权限规则,即role对API的endpoint的CRUD的权限

# role.py

from enum import RoleEnum

Role = {
    RoleEnum.WORKER.value: {
        "worker.sign_out": {"GET": True}  
        # WORKER的role在worker.sign_out的endpoint有GET的权限
    }
}

建立好权限规则后,接着要实现鉴权和权限管理。
通过JWT实现鉴权

# token.py

import datetime
import jwt  # JWT package,需要其提供的encode和decode的方法,也可以DIY实现

from enum import ApiExceptionEnum

secret = "some_secret_string"  # JWT signature的secret,可以是动态生成的字符串

class Token:
    def __init__(self, role):
        self.role = role  # Token实例规定带有role,方便后续的权限管理

    def encode_token(self, id_, login_at):
        try:
            # algorithm一般是HS256
            header = {'algorithm': 'HS256', 'type': 'JWT'}
            payload = {
                # 发行时间
                'iat': datetime.datetime.utcnow(),
                # token签发者
                '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, '无效token')

    # decode是静态方法,不需要绑定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已过期')
        except jwt.InvalidTokenError:
            raise Exception(ApiExceptionEnum.InvalidInput.value, '无效token')

为了方便对所有API的管理,使用python的decorator实现。

# permission.py

from functools import wraps
from flask import request  
# flask框架的request请求信息查询,其他框架只需要import相关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, "请求头错误")

        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格式错误")

        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, "无权访问")

        return func(id_, *args, **kwargs)

    return wrap_func

这样就可以使用鉴权和权限管理

# account.py

@blueprint.route('/sign_out', methods=['GET'])  # flask的蓝图URI注册
@access_control
def sign_out(worker_id):
    """
    用户登出(Worker)
    :return:
    """
    message = sign_out(worker_id)

    return jsonify(message)

def sign_out(worker_id):
    ... # do something,关于业务逻辑的部分
    return {'message': 'success'}

参考
https://www.jianshu.com/p/db65cf48c111
https://www.jianshu.com/p/b78744bd463b
https://zhuanlan.zhihu.com/p/28295641

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343