django 简单通用的 RBAC 的权限控制方案
设计构思
基础构思
- 层级设计
- 层级设计基于各个系统的内部组织架构进行划分
- 这里简单演示使用 三层级的权限设计
- 具象化来说 (公司层级 - 部门层级 - 小组层级)
- 权限角色设计
- 角色隶属于各个层级, 相当于对角色进行分级 --- 即角色只能属于一个层级
- 具象化来说 - {受体}-{层级}-{权限}
- 公司层级 (A公司管理员, B公司分级管理管理, C公司只读审查员....)
- 部门层级 (A部门管理员, B部门客户管理员, C部门访客....)
- 小组层级 (A组管理员, B组项目管理员, C组实习生....)
- 具象化来说 - {受体}-{层级}-{权限}
- 这样设计的基本思想: 利用具体的受体的作用域以及本身层级从而不会无法跨域层级
- 具体来说
- A公司管理员 只能管A公司范围的数据, 无权插手 B,C 公司的范围 A公司管理员 可以对A公司范围的其他权限角色进行升降级或其他调整
- A 公司管理员 将 A公司的a部门的a1项目的xx实习生 提升到 a1项目管理员
- A 公司管理员 将 A公司的a部门的a1项目的xx实习生 转换成 A公司的c部门的c1项目 的 1项目只读审查员
- 具体来说
- 角色隶属于各个层级, 相当于对角色进行分级 --- 即角色只能属于一个层级
- 权限策略设计
- 权限内容分为: 接口级, 数据级
- 接口级: 定义 权限策略控制的 增删改查权限
- 数据级: 定义 策略影响的数据范围
- 权限内容分为: 接口级, 数据级
构思延伸
权限角色
基于以上的基础构思大体可以理解到一个权限角色应该具备的雏形
以 A公司管理员 为例
A公司 作为权限角色的受体, 提供权限角色具备的原始数据集 (即 A 公司的所有资源)
管理员作为权限角色的策略主体, 提供具体的权限策略 (即管理员具备的哪些增删改查的能力)
按照定义那么 A 公司管理员应该具备 A 公司旗下的所有资源的管理员级的权限
用户绑定
用户想具备权限则需要和权限角色绑定. 此时存在一个问题
用户如何绑定? 是否可以绑定多角色?
倘若可以绑定多角色.
理想情况下的场景:
羊驼用户想要绑定 A 公司的 a 部门访客 和 B 公司的 b 部门管理员
此时是没有问题的, 彼此的权限是不冲突的, 级A-a 和 B-b 彼此数据不存在重叠部分
不理想的情况:
1. 羊驼用户想要绑定 A 公司的 b 部门访客 和 A 公司的 b 部门管理员
此时存在数据重叠, 但是同层级可以使用最大权限并集进行处理, 即他任意一个角色有权限他就有权限
2. 羊驼用户想要绑定 A 公司的 b 部门访客 和 A 公司的 b 部门下的 bb 小组管理员
羊驼在部门层级的权限是访客. 他是只读的权限, 但是 bb 小组又是管理员, 拥有全部权限
此时不仅数据重叠,还存在策略冲突, 羊驼如果在访问 bb 的时候 此时存在两种设想:
1. 按层级优先来说, 羊驼就只能只读, 因为他高层级权限是覆盖下层权限的
2. 按照底层优先来说, 羊驼应该全量权限(管理员), 因为底层更细化
那问题来了.
你要怎么选? (选1, 更方便, 且更合理)
你选一个让用户记住并且避免用户自己配的时候不配错? (不现实, 在代码里面隐式处理, 不让用户察觉)
你在前端加限校验用户的配置? (yes)
你在后端每次就校验的时候都复杂的在这里? (按照层级往下拉)
很明显这时候多角色的设计是相当繁琐的, 一堆的问题要去解决,
基于权限的基本构思, 这些问题是绕不开的, 所以怎么让用户体验良好就是重点了
与其让用户自己配置完去记忆这些规则, 不如一开始就把这个问题避免处理方式则是上面括号内选择的方案
而为了用户体验, 只要采用单角色多策略即可显著提升, 这里的问题的避免在策略配置的时候就处理好
形成设计
根据以上的构思, 确定了单角色多策略的权限设计
即用户只能绑定一个角色, 角色本身只有一个层级, 层级本身就决定了他能配置的范围
举栗子:
a 部门管理员 --- 只能配 a 部门范围内的权限策略 比如 a 部门下的 aa, bb, cc 小组的权限
那 如果有个人他想要 A 公司的 a,b 两个部门的管理员怎么办
那就上升层级, 给他 A公司分级管理员 , 此角色的数据覆盖范围只纳管到 a,b 两个部门即可
这样我们就全面满足了所有的权限场景
还是举栗子:
A - a - (a1, a2, a3)
- b - (b1, b2)
- c - (c1)
B - m - (m1, m2)
- n - (n1)
最简单的单策略 - A 公司管理员 / a部门访客 /a3小组实习生
相同层级的多策略 - A公司分管(a, b) / a部门分管(a1, a2)
不同层级的多策略 - B公司小分管(n1, m2) / A 公司小分管 (a1, b2)
相当特殊的多策略 - 系统特殊小分管 (a1, n1)
由此可见, 这样的话你无论怎么配置只要有了天花板就可以拦住你的可配置范围.
同时这样也可以自由的配置层级下的所有资源
至于不受待见的
A 扭曲小分管(a, a2, b2) 这种, 策略存在覆盖的情况.
我们直接在配置的时候不做允许, 详情可以看策略设计的部分
从而让用户一开始就不会配错. 从而避免以后的麻烦
基于此, 我们只要这样设计权限策略即可
权限策略
权限策略为了方便用户的设计, 也定义层级
- 全域: 策略绑定的角色的层级全部数据 (globals)
- 一个角色最多只做只会有一条这样的数据, 因为这个数据已经覆盖了所有数据了
- 配置了此策略后. 不能配置任何其他策略
- 全表: 策略绑定的角色的层级下的 指定资源类型的全全部数据 (table)
- 一个角色最多可以有 n (table数量) 条这样的数据, 每条策略的 table 的唯一
- 同一个角色下的策略中对某一类资源最多只有一条策略
- 当 n = 所有table的总和时, 相当于全域策略
- 配置此策略后, 不能在配置此资源的单行策略
- 单行: 策略绑定的角色的层级下的(globals) 指定资源类型(table) 的某一个资源 (id)
- 无数量限制, 但是联合起来唯一
- 同一个角色下的策略中对某一个资源的id最多只有一条策略
再来看上面的 A 扭曲小分管(a, a2, b2) 按照我们的设计, 只会变成
正常小分管 A (a, b2) 或者 正常小分管 A (a2, b2) ,
此时基于场景用户进行自定义选择那种方式即可, 从而满足所有场景
数据权限
数据权限则就是所谓 A 公司, a 部门, a1 小组的具体的数据范围
在 orm 映射的过程中会把资源抽象成表, 属性抽象成字段, 每行记录则是具体的某一个资源
因此存在两种映射模式
1. 是对资源的属性中直接加入字段来表明他的所属, 如果资源属于多所属时用多对多映射等, 对资源表的侵略性较大且改动起来不方便
2. 额外的表存储隐射关系, 利用 django 中的 django_content_type 进行关联到具体的资源数据
django_content_type
先说下 这张表的特殊作用
django_content_type 这张表的结构较为简单
CREATE TABLE `django_content_type` ( `id` int(11) NOT NULL AUTO_INCREMENT, `app_label` varchar(100) NOT NULL, `model` varchar(100) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `django_content_type_app_label_model_76bd3d3b_uniq` (`app_label`,`model`) ) ENGINE=InnoDB AUTO_INCREMENT=54 DEFAULT CHARSET=utf8;
只有两个字段 app_label 和 model , 通过 app 和 model 就可以定位到唯一的表 (不同app内是可以存在model同名的)
而这张表的调用则可以通过 django 的内置模块取到
from django.contrib.contenttypes.models import ContentType
表内存的是字符串, 如图所示如果先想要取到 message 这个模型类
首先取到具体的这行记录. 基础的 orm 查询操作
c = ContentType.objects.get(app_label="account", model="message")
通过 .model_class() 即可取到 模型类可以再次进行其他的查询操作,
c.model_class().objects.filter(....)
等价于
Message.objects.filter(....)
模型类设计
模型源码
共 6 张表, 三张层级表, 一张角色表, 一张权限策略表. 一张层级数据范围表
from django.db import models from django.contrib.contenttypes.models import ContentType class Company(models.Model): code = models.CharField(verbose_name="公司", help_text="租户", max_length=50, unique=True) name = models.CharField(verbose_name="公司(中文名)", help_text="公司(中文名)", max_length=40, blank=True, null=True) # 取到某个表中的所有资源合集 def bind_list(self, app_label, model_name): c = ContentType.objects.get(app_label=app_label, model=model_name) # 存在 row 级别权限, 取到具体的 row 集和 if ('1',) in self.bind_obj.values_list("level"): return c.model_class().objects.filter( id__in=self.bind_obj.filter(model=model_name).values_list("object_id")) class Meta: verbose_name = "公司" verbose_name_plural = verbose_name class Department(models.Model): company = models.ForeignKey(verbose_name="公司", help_text="公司", to=Company, null=True, blank=True, on_delete=models.SET_NULL) code = models.CharField(verbose_name="部门", help_text="部门", max_length=60) name = models.CharField(verbose_name="部门(中文名)", help_text="部门(中文名)", max_length=60, blank=True, null=True) # 取到某个表中的所有资源合集 def bind_list(self, app_label, model_name): c = ContentType.objects.get(app_label=app_label, model=model_name) return c.model_class().objects.filter(id__in=self.bind_obj.filter(model=model_name).values("object_id")) class Meta: verbose_name = "部门" verbose_name_plural = verbose_name unique_together = ("company", "code") class Projcet(models.Model): company = models.ForeignKey(verbose_name="公司", help_text="公司", to=Company, null=True, blank=True, on_delete=models.SET_NULL) department = models.ForeignKey(verbose_name="部门", help_text="部门", to=Department, null=True, blank=True, on_delete=models.SET_NULL) code = models.CharField(verbose_name="项目", help_text="项目", max_length=60) name = models.CharField(verbose_name="项目(中文名)", help_text="项目(中文名)", max_length=60, blank=True, null=True) # 取到某个表中的所有资源合集 def bind_list(self, app_label, model_name): c = ContentType.objects.get(app_label=app_label, model=model_name) return c.model_class().objects.filter(id__in=self.bind_obj.filter(model=model_name).values("object_id")) class Meta: verbose_name = "项目" verbose_name_plural = verbose_name unique_together = ("company", "department", "code") # 权限角色 class PermissionRole(models.Model): LEVEL = ( (1, "公司级别"), (2, "部门级别"), (3, "项目级别") ) name = models.CharField(verbose_name="权限角色名", help_text="权限角色名", max_length=60) level = models.CharField(verbose_name="权限级别", help_text="权限级别", max_length=40, choices=LEVEL) company = models.ForeignKey(verbose_name="公司", help_text="公司", to=Company, null=True, blank=True, on_delete=models.SET_NULL) department = models.ForeignKey(verbose_name="部门", help_text="部门", to=Department, null=True, blank=True, on_delete=models.SET_NULL) project = models.ForeignKey(verbose_name="项目", help_text="项目", to=Projcet, null=True, blank=True, on_delete=models.SET_NULL) class Meta: verbose_name = "权限角色" verbose_name_plural = verbose_name unique_together = ("company", "department", "project", "name") # 权限策略 class PermissionAllowedMethods(models.Model): LEVEL = ( (1, "globals"), (2, "table"), (3, "row"), ) role = models.ForeignKey(verbose_name="权限角色", help_text="权限角色", to=PermissionRole, null=True, blank=True, on_delete=models.SET_NULL, related_name="methods") level = models.CharField(verbose_name="权限级别", help_text="权限级别", max_length=40, choices=LEVEL) app_label = models.CharField(verbose_name="app", help_text="app", max_length=40, null=True, blank=True) model = models.CharField(verbose_name="Model", help_text="Model", max_length=40, null=True, blank=True) object_id = models.IntegerField(verbose_name="对象ID", help_text="Model", null=True, blank=True) c = models.BooleanField(verbose_name="添加权限", help_text="添加权限", blank=True, default=False) u = models.BooleanField(verbose_name="修改权限", help_text="修改权限", blank=True, default=False) r = models.BooleanField(verbose_name="查看权限", help_text="查看权限", blank=True, default=False) d = models.BooleanField(verbose_name="删除权限", help_text="删除权限", blank=True, default=False) @property def allowed_methods(self): allowed_list = ["OPTIONS", "TRACE", "HEAD"] allowed_list.extend(["POST"] if self.c else []) allowed_list.extend(["PUT", "PATCH"] if self.u else []) allowed_list.extend(["DELETE"] if self.d else []) allowed_list.extend(["GET"] if self.r else []) return allowed_list @property def object_show(self): if self.model and self.object_id: c = ContentType.objects.get(app_label=self.app_label, model=self.model) res = c.model_class().objects.filter(id=self.object_id).first() return res.__str__() if res else "" class Meta: verbose_name = "权限策略" verbose_name_plural = verbose_name unique_together = ("role", "app_label", "model") # 层级数据范围 class PermissionDataScope(models.Model): LEVEL = ( (1, "公司级别"), (2, "部门级别"), (3, "项目级别") ) level = models.CharField(verbose_name="level", help_text="level", max_length=40, choices=LEVEL) company = models.ForeignKey(verbose_name="公司", help_text="公司", to=Company, null=True, blank=True, on_delete=models.SET_NULL, related_name="bind_obj") department = models.ForeignKey(verbose_name="部门", help_text="部门", to=Department, null=True, blank=True, on_delete=models.SET_NULL, related_name="bind_obj") project = models.ForeignKey(verbose_name="项目", help_text="项目", to=Projcet, null=True, blank=True, on_delete=models.SET_NULL, related_name="bind_obj") app_label = models.CharField(verbose_name="app", help_text="app", max_length=40) model = models.CharField(verbose_name="Model", max_length=40) object_id = models.IntegerField(verbose_name="对象ID") class Meta: verbose_name = "层级数据范围" verbose_name_plural = verbose_name unique_together = ("company", "department", "project", "model", "object_id")
层级模型
层级模型的三张表分别代表三个级别, 公司表, 部门表, 项目表
这部分比较简单没啥可说的, 就是彼此套一下而已
公司表
class Company(models.Model): code = models.CharField(verbose_name="公司", help_text="租户", max_length=50, unique=True) name = models.CharField(verbose_name="公司(中文名)", help_text="公司(中文名)", max_length=40, blank=True, null=True) # 取到某个表中的所有资源合集 def bind_list(self, app_label, model_name): c = ContentType.objects.get(app_label=app_label, model=model_name) # 存在 row 级别权限, 取到具体的 row 集和 if ('1',) in self.bind_obj.values_list("level"): return c.model_class().objects.filter(id__in=self.bind_obj.filter(model=model_name).values_list("object_id")) class Meta: verbose_name = "公司" verbose_name_plural = verbose_name
bind_list 则用到了 ContentType 相关的方法, 对应的是对数据级权限的范围缩圈处理
利用 ContentType 调到对应的模型类, 结合数据级策略的内容进行相应的查找
部门表
class Department(models.Model): company = models.ForeignKey(verbose_name="公司", help_text="公司", to=Company, null=True, blank=True, on_delete=models.SET_NULL) code = models.CharField(verbose_name="部门", help_text="部门", max_length=60) name = models.CharField(verbose_name="部门(中文名)", help_text="部门(中文名)", max_length=60, blank=True, null=True) # 取到某个表中的所有资源合集 def bind_list(self, app_label, model_name): c = ContentType.objects.get(app_label=app_label, model=model_name) return c.model_class().objects.filter(id__in=self.bind_obj.filter(model=model_name).values("object_id")) class Meta: verbose_name = "部门" verbose_name_plural = verbose_name unique_together = ("company", "code")
项目表
class Projcet(models.Model): company = models.ForeignKey(verbose_name="公司", help_text="公司", to=Company, null=True, blank=True, on_delete=models.SET_NULL) department = models.ForeignKey(verbose_name="部门", help_text="部门", to=Department, null=True, blank=True, on_delete=models.SET_NULL) code = models.CharField(verbose_name="项目", help_text="项目", max_length=60) name = models.CharField(verbose_name="项目(中文名)", help_text="项目(中文名)", max_length=60, blank=True, null=True) # 取到某个表中的所有资源合集 def bind_list(self, app_label, model_name): c = ContentType.objects.get(app_label=app_label, model=model_name) return c.model_class().objects.filter(id__in=self.bind_obj.filter(model=model_name).values("object_id")) class Meta: verbose_name = "项目" verbose_name_plural = verbose_name unique_together = ("company", "department", "code")
权限角色模型
class PermissionRole(models.Model): LEVEL = ( (1, "公司级别"), (2, "部门级别"), (3, "项目级别") ) name = models.CharField(verbose_name="权限角色名", help_text="权限角色名", max_length=60) level = models.CharField(verbose_name="权限级别", help_text="权限级别", max_length=40, choices=LEVEL) company = models.ForeignKey(verbose_name="公司", help_text="公司", to=Company, null=True, blank=True, on_delete=models.SET_NULL) department = models.ForeignKey(verbose_name="部门", help_text="部门", to=Department, null=True, blank=True, on_delete=models.SET_NULL) project = models.ForeignKey(verbose_name="项目", help_text="项目", to=Projcet, null=True, blank=True, on_delete=models.SET_NULL) class Meta: verbose_name = "权限角色" verbose_name_plural = verbose_name unique_together = ("company", "department", "project", "name")
通过 level 将权限角色分级, 对应层级绑定具体的层级表
level1 的 company 必填
level2 的 department 必填
level3 的 project 必填
层级数据范围模型
class PermissionDataScope(models.Model): LEVEL = ( (1, "公司级别"), (2, "部门级别"), (3, "项目级别") ) level = models.CharField(verbose_name="level", help_text="level", max_length=40, choices=LEVEL) company = models.ForeignKey(verbose_name="公司", help_text="公司", to=Company, null=True, blank=True, on_delete=models.SET_NULL, related_name="bind_obj") department = models.ForeignKey(verbose_name="部门", help_text="部门", to=Department, null=True, blank=True, on_delete=models.SET_NULL, related_name="bind_obj") project = models.ForeignKey(verbose_name="项目", help_text="项目", to=Projcet, null=True, blank=True, on_delete=models.SET_NULL, related_name="bind_obj") app_label = models.CharField(verbose_name="app", help_text="app", max_length=40) model = models.CharField(verbose_name="Model", max_length=40) object_id = models.IntegerField(verbose_name="对象ID") class Meta: verbose_name = "数据级权限" verbose_name_plural = verbose_name unique_together = ("company", "department", "project", "model", "object_id")
层级数据范围和权限角色本质上毫无关系. 层级数据范围表的目的是标识 层级的原始数据范围
如图所示还是用上面的实例, 这就是一份数据范围的示意图, 目的是将层级和他所有的资源进行划分使用
而这里的数据是为了权限策略中 (1, "globals") 和 (2, "table") 服务的, 作用于全数据的时候就要在这里取数据范围了
权限策略模型
class PermissionAllowedMethods(models.Model): LEVEL = ( (1, "globals"), (2, "table"), (3, "row"), ) role = models.ForeignKey(verbose_name="权限角色", help_text="权限角色", to=PermissionRole, null=True, blank=True, on_delete=models.SET_NULL, related_name="methods") level = models.CharField(verbose_name="权限级别", help_text="权限级别", max_length=40, choices=LEVEL) app_label = models.CharField(verbose_name="app", help_text="app", max_length=40, null=True, blank=True) model = models.CharField(verbose_name="Model", help_text="Model", max_length=40, null=True, blank=True) object_id = models.IntegerField(verbose_name="对象ID", help_text="Model", null=True, blank=True) c = models.BooleanField(verbose_name="添加权限", help_text="添加权限", blank=True, default=False) u = models.BooleanField(verbose_name="修改权限", help_text="修改权限", blank=True, default=False) r = models.BooleanField(verbose_name="查看权限", help_text="查看权限", blank=True, default=False) d = models.BooleanField(verbose_name="删除权限", help_text="删除权限", blank=True, default=False) @property def allowed_methods(self): allowed_list = ["OPTIONS", "TRACE", "HEAD"] allowed_list.extend(["POST"] if self.c else []) allowed_list.extend(["PUT", "PATCH"] if self.u else []) allowed_list.extend(["DELETE"] if self.d else []) allowed_list.extend(["GET"] if self.r else []) return allowed_list @property def object_show(self): if self.model and self.object_id: c = ContentType.objects.get(app_label=self.app_label, model=self.model) res = c.model_class().objects.filter(id=self.object_id).first() return res.__str__() if res else "" class Meta: verbose_name = "权限策略" verbose_name_plural = verbose_name unique_together = ("role", "app_label", "model", "object_id")
权限角色可以绑定多个策略, 策略本身分为3个等级
- level=1 全域
- app_label / model / object_id 为空无意义
- 同一角色只会存在一记录, 且无法再添加其他策略
- 表示当前角色绑定层级的全范围数据收到影响
- level=2 全表
- app_label / model 必填且与角色联合唯一, 且无法再添加此 model 下的单行策略
- object_id 为空无意义
- 对其它 model 的单行策略不影响
- 表示当前角色绑定层级的当前资源类型表范围的数据收到影响
- level=3 单行
- app_label / model / object_id 必填且与角色联合唯一
- 表示当前角色绑定层级的当前资源类型表中指定的 id 行收到影响
增啥改查 的具体方法则通过 allowed_methods 转化后直接对比即可
权限校验实现
模型类理清之后, 就是如何使用起来了
django 中处理权限的组件是有的, 但是这里可以考虑使用更大范围的中间件来处理
中间件中对请求的方法进行校验处理, queryset 中对数据的范围进行处理
重写 queryset
起一个公共钩子, 对 get_queryset 重写
# 数据鉴权, 用到此钩子的场景: 非公共资源的数据 def fix_get_queryset(self): """ 平台层级: 公司 / 部门 / 项目 平台绑定资源: 公司: 必须配置, 指定资源划分到公司 部门: 必须配置, 指定公司下资源划分到部门 项目: 必须配置, 基于公司下的部门内再次划分资源到项目 角色层级: level = 1, 公司 level = 2, 部门 level = 3, 项目 权限层级: level = 1, 全域 - 影响角色对应层级下的所有资源权限, 只存在唯一记录 level = 2, 全表 - 影响角色对应层级下指定 model 内所有资源权限, 对唯一 model 只存在唯一记录 level = 3, 单行 - 影响角色对应层级下指定 model 内某一个资源权限, 对唯一资源只存在唯一记录 整体的数据鉴权流程: 1. 判断用户是否超级用户 2. 判定用户角色层级 3. 判定用户权限策略层级 3.1 策略层级为 1(全域)/2(全表) 时 3.1.1 查询用户层级所有资源进行返回 3.2 策略层级为 3 时 3.2.1 查询角色绑定权限配置信息进行返回 :return: """ # 超级用户直接返回 if self.request.user.is_superuser: return self.queryset # 取到模型类名 object_name = self.queryset.model._meta.object_name # 当前登录用户权限角色 role = self.request.user.role # 权限信息 permission = PermissionAllowedMethods.objects.filter(role=role) # 预备操作的当前模型类 q = getattr(asset_models, object_name).objects # 当前登录用户权限角色配置的权限层级 permission_level = permission.values_list("level") # 全域 / 全表权限 - 返回表内当前租户所属层级所有资源 if ("1",) in permission_level or permission.filter(model=object_name, level="2"): # 取当前用户角色绑定公司在当前模型类中的绑定资源 if role.level == "1": ids = PermissionDataScope.objects \ .filter(level="1", model=object_name, company=role.Company) \ .values_list("object_id") return q.filter(id__in=[i[0] for i in ids]) # 部门层级 elif role.level == "2": # 取当前用户角色绑定部门在当前模型类中的绑定资源 ids = PermissionDataScope.objects \ .filter(level="2", model=object_name, department=role.department) \ .values_list("object_id") return q.filter(id__in=[i[0] for i in ids]) # 项目层级 elif role.level == "3": # 取当前用户角色绑定项目在当前模型类中的绑定资源 ids = PermissionDataScope.objects \ .filter(level="3", model=object_name, project=role.project) \ .values_list("object_id") return q.filter(id__in=[i[0] for i in ids]) # 行级 - 检测当前表内是否具备某些资源权限 elif ("3",) in permission_level: # 取当前用户角色绑定三级权限在当前模型类中的绑定资源 ids = permission.filter(model=object_name).values_list("object_id", flat=True) return q.filter(id__in=ids) return q.none()
中间件
原理类似, 只是多判断一层 request.metheds 是否在 allowed_methods 里面即可
本文来自博客园,作者:羊驼之歌,转载请注明原文链接:https://www.cnblogs.com/shijieli/p/15162524.html