鉴权和权限管理的探索

传统鉴权方案是通过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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容