传统鉴权方案是通过Session ID完成的,现在一般使用Token鉴权。
1.认证与鉴权
- 分布式session
用户认证信息存储在共享存储中,通常以用户会话作为key来实现简单分布式哈希映射。优点是高可用且可扩展,缺点是共享存储需要安全链接等复杂的保护机制。
- OAuth2 Token方案
相比于Session,token方案一般会包含更丰富的用户信息,token一般放在HTTP请求头中。有点是服务器无状态,token验证不需要访问数据库所以性能更好,支持多端(Web和移动端等)
2.基本流程
最简单的用户系统的功能包括注册、登录和鉴权三个方面
- 注册,用户发送用户名和密码,服务器存储;
- 登录,用户发送用户名和密码,服务器鉴定是否成功,token方式成功后返回token;
- 鉴权,用户将token发送给服务器,服务器鉴定token是否legal。
这个过程可能的问题包括
- 密码泄漏
- 生成token的secret key/salt泄漏
- 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不是保密的。
安全策略:
- 用户注册的时候,根据密码+随机生成的盐生成新的加密密码存储于数据库,同时每个用户随机的盐也存放在数据库;
- 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表设计
- source表
permissions字段:1个资源有4个权限——CRUD; - permission表
name字段:source某条记录的唯一标识identity;
action字段:对资源的操作,只能是CRUD之一;
relation字段:标记资源类型,私人、角色或者公共;
roles字段:拥有该权限的角色;
4.4策略
- 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