基于RESTful 实现简易的权限管控系统 # 目标 - 实现基于 RESTful API 路径的细粒度权限控制,**确保系统安全性**。 - 简化权限管理:仅使用 User、Group、Permission 模型,提供简单易用的权限管理。 - 有一定的扩展性:可对 Permission 种类进行扩展,可将多个 Permission 进行分组,**便于权限管理**。 # 方案示例 ## 实体 User(用户): - User 代表系统中的用户身份,可以是: - 基础的带用户名、密码、邮箱、姓名的账户 - API 令牌账户 Permission(权限): - Permission 由 RESTful 实体派生出来,例如 DBInstance 是一种资源实体,它的定义如下: ``` class DBInstance(models.Model): # 如果不需要实际存储数据,可以把它设为 abstract,参考下例: owner = models.TextField() name = models.TextField() created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: abstract = True verbose_name = "DBInstancePermission" verbose_name_plural = "DBInstancePermissions" permissions = ( ("can_export_info", "Can Export DBInstance"), ("can_backup", "Can Backup DBInstance"), ("can_restore", "Can Restore DBInstance"), ) ``` 系统初始化时,将自动创建**基础权限**和**自定义权限**: - 基础权限(自动生成): - dbinstance.can_add - dbinstance.can_view - dbinstance.can_delete - dbinstance.can_change - 自定义权限(在 Meta 类中定义): - dbinstance.can_export_info - dbinstance.can_backup - dbinstance.can_restore ...... - 每一种权限(Permission)都可以单独绑定到一个用户上,也可以划分到一个 Group 中。 Group(用户组): - Group 是权限的集合,用于将多个权限分配给一组用户。 它们之间的关系 ## 实现 **Permission的扩展** - 没有实体的权限是奇怪的(虽然也可以为权限单独定义抽象类),Permission 通常与实体(Model)关联,由实体派生。 - 在定义实体的同时,可以定义实体相关的操作,系统将自动生成对应的 Permission。随着系统的实体不断完善,Permission 的种类会逐步扩展。 Django中的Permission示例 **RESTful:得天独厚的优势** RESTful 架构通过 URL 映射资源,并使用 HTTP 方法定义操作,这为权限系统的设计提供了天然的优势。 举例如下: - /dbinstance:映射为 database 的实体类,在实践中是database的列表或集合 - /dbinstance GET:获取 database 列表的内容。 - /dbinstance PUT:创建一个 database 实例。 - /dbinstance POST/DELETE:对列表进行修改(一般来说,列表由子项生成,这些权限是无效权限) - /dbinstance/$DBID:映射为具体单个实体,也就是一个数据库的实例 - GET/POST/DELETE:对应对具体的实例进行查看信息,修改信息,删除实例。 - /dbinstance/$DBID/backups:这个数据库实例对应的备份。 - PUT/GET:对应对具体的数据库备份进行创建,查看备份列表。 - /dbinstance/$DBID/backups/$BACKUPID:单个数据库备份的实体。 - DELETE/GET:对应对具体的数据库备份进行删除,查看单个备份的信息。 实现URL与权限的映射表 假设有资源-权限映射表如下: ``` url_permission_list = { "dbinstance/id-foo/backups": { "permission_map": { "dbinstance.can_view": { # 这个实体下可附加的N个权限 # 这是基础权限 "GET": True, "POST": False, "PUT": False, "DELETE": False, }, "dbinstance.can_backup": { "GET": False, "POST": False, "PUT": True, "DELETE": False, }, } } } ``` 以上面的权限表举例,U1 用户拥有 permission: - dbinstance.can_backup - dbinstance.can_view U2 用户拥有: - dbinstance.can_view 当用户访问资源 dbinstance/id-foo/backups 时,系统将调用 user.has_perm(permission) 方法,检查用户是否具有访问该资源所需的权限。 注意,user.has_perm 的实现是重中之重。它必须能正确地检查当前用户 Group 中包含的权限,和单独授权的权限。 权限检查伪代码: ``` permisson_map = url_permission_list[request.resolved.url_name] for permission, method_map in permission_map: if request.user.has_perm(permission): if request.method in method_map and method_map[request.method]: # 权限和方法都匹配,正常下一步 response = self.get_response(request) return response # 所有的权限都不匹配,返回403 ``` 在上面的假设下: - U1 用户,调用 PUT dbinstance/id-foo/backups 时,通过 - U1 用户,调用 PUT dbinstance/id-bar/backups 时,不通过,因为 id-bar 资源没有绑定 dbinstance.can_backup 权限。 - U2 用户,调用 PUT dbinstance/id-foo/backups 时,不通过,因为虽然 id-foo 绑定了 dbinstance.can_backup 权限,但 U2 用户没有 dbinstance.can_backup 权限。 - 从这个例子来看,这个体系中 Permission 带有 AWS 权限系统中 Policy 的功能。这个简化当然会带来功能上的损失。 ## 使用中间件在访问资源时进行权限检查 在每次 HTTP 访问一个 URL 时,在 RESTful 体系对应访问一个资源及对它施加操作。这是一个检查权限的好时机,一般会实现中间件进行统一检查。 对应的 django-python 代码如下: ``` class PermissionMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): resolved = resolve(request.path_info) # 通过上下文,定位到资源名 url_name = resolved.url_name # 根据 url_name 获取对应的权限列表,如果不在权限控制列表的,默认放通 if url_name in url_permission_list: permission_map = url_permission_list[url_name]["permisson_map"] # 根据 method 中 True 的数量进行排序,先检查权限大的Permission sorted_permissions = sorted( permission_map.items(), key=lambda item: sum(item[1].values()), reverse=True, ) # 遍历权限列表,检查权限和方法匹配 for permission, method_map in sorted_permissions: if request.user.has_perm(permission): if request.method in method_map and method_map[request.method]: # 权限和方法都匹配,立即返回 response = self.get_response(request) return response # 权限被拒绝,重定向回上一个页面 referer = request.META.get("HTTP_REFERER") # 如果url参数中包含ajax,就不跳转,直接返回 HttpResponseForbidden 403 is_ajax = request.is_ajax() if not is_ajax and referer: parsed_url = urlparse(referer) query_params = parse_qs(parsed_url.query) # 给出新的错误提示 query_params["error_msg"] = ( f"Permission denied or method:{request.method} not allowed" ) query_string = urlencode( query_params, doseq=True ) # doseq=True 处理多值的query参数 # 获取不包含参数的部分 url_path = parsed_url.path redirect_url = f"{url_path}?{query_string}" logging.info( f"PermissionMiddleware rejected, redirect referer:{url_path}, query:{query_string}, url:{redirect_url}" ) return redirect(redirect_url) else: logging.info(f"PermissionMiddleware rejected, {request.path}") return HttpResponseForbidden( f"Permission denied or method:{request.method} not allowed" ) response = self.get_response(request) return response ``` # 优化 - 超级用户权限处理: - user.has_perm 接口中,需要对超级用户进行特殊处理。在 User 表中,新增字段标记用户 is_admin。对标记为 admin 的用户,user.has_perm 一律返回 True。 - 实体类与实体权限区分处理: - 首先明确两个概念,实体类和单个实体。实体类数量是有限的,比如 dbinstance。单个实体(dbinstance/id-foo)则不然,它理论上可以创建无穷多个。 - 在实践中,url_permission_list 会存储在数据库中,直接使用上面的伪代码会非常的低效。因为在实作中,实体类的鉴权(通常是列表、首页...)非常频繁,而针对单个实体的规则则非常多。 - 建议对这两类权限做区分处理,对实体类的鉴权,可直接使用 bitmap 进行缓存优化,走快速检验的逻辑。对单个实体的权限则利用数据库表 + 缓存进行鉴权。 - 避免使用注解/装饰器: ``` 在 Django 中,提供了装饰器(注解)对权限进行判定: @permission_required('app_name.can_export_info', raise_exception=True) def my_view(request): # 视图逻辑 return HttpResponse('You have the required permission.' ``` 这看起来很方便,实际上,它将权限的管理散落在多处,为后续的系统扩展和安全审计留下隐患。 来自 大脸猪 写于 2025-03-06 00:52 -- 更新于2025-04-03 16:26 -- 2 条评论