(本文沿着思路来写,不会一开始就给出最后结论。)

很多项目需要对用户操作进行鉴权。它们的需求可以归纳为下面几点:
1、基于角色的权限控制,一个用户可能被授予多个角色;
2、用户对同一条记录的不同操作(如查看和修改)需要分别授权;
3、记录之间可能存在父子关系,子记录的权限自动从父记录继承,不需要明确授权。

所以,可以看出这个权限模型存在四个基本要素:用户、角色、操作、记录。
用户:实际进行操作的单位。
角色:用户进行操作时的身份。
操作:用户执行的与记录有关的动作。
记录:被操作的对象。

因为鉴权只针对角色而非用户,所以一个鉴权应该可以描述为:判断某角色是否对某记录拥有某操作的权限

可以看出,每个权限都是一个“角色—操作—记录”的三要素关联关系。如果我们要设计一张表保存所有授权,那么这张表至少应该包含这三个字段。

那么问题来了:随着记录的增加,这张表的记录数将呈级数增长,特别是记录之间存在父子关系的情况下,为父记录授权就意味着同样要为所有子记录授权,删除一条授权也会造成大量的查询和更新,这张表的维护将成为噩梦,这样的表设计没有实用性。

造成这种情况的根本原因是,记录是经常变化的,而鉴权规则很少改变,二者之间存在脱节。

在这里我们不得不重新思考授权的本质:授权本来就不是针对某条具体的记录的。以博客系统为例,“作者有权删除其创建的文章”这个规则中,角色是“作者”,操作是“删除”,而记录呢?“作者创建的文章”并不是一条具体的记录。所以说,授权是对规则的描述,它针对的不是具体的记录,而是更抽象的东西。

这种更抽象的东西,我们暂把它叫做“资源”。那么鉴权的三要素,应该称作“角色—操作—资源”。资源是对记录的抽象,就如角色是对用户的抽象一样。这样,一条权限就变成了完全抽象的:它既不针对具体的某个用户,也不针对具体的某条记录,它完全是对规则的描述。当一个用户对一条记录的操作需要鉴权时,需要进行映射,将用户映射到角色,将记录映射到资源,然后再搜索是否寻在允许的授权。

因为“用户—角色”是多对多的关系,“记录—资源”也是多对多的关系(比如一篇博客文章可能是“我的文章”,也可能是“别人的文章”),所以“用户—操作—记录”先要被映射到“角色[]—操作—资源[]”([]表示有多个),然后再从匹配关系的组合中搜索是否存在允许的授权。如果存在则表示鉴权通过,否则鉴权不通过。

至此为止,权限表设计已经从“角色—操作—记录”改为“角色—操作—资源”,它符合“授权的本质”,所以不会有大量的级联查询和更新。但它仍然存在两个问题:1)因为存在角色与资源的多对多组合,所以每次鉴权需要进行大量的判断。2)我们没有办法实现从记录到资源的映射,因为两者并没有直接关联。为什么这么说呢?以博客系统为例,一篇文章在作者面前可以被映射为“我的文章”,在管理员面前可以被映射为“普通文章”,也许还会存在其他的映射,这是无法确定的。我们需要仔细考察这里面的关系。

经过仔细考察,我发现这两个问题其实是有关联的。我们需要重新审视资源的概念:资源不是独立存在的,而是与角色关联的,不同的角色需要面对不同的资源。比如博客系统中,管理员面对用户、角色等资源,而普通用户则面对文章、博客等资源。所以记录到资源的映射与角色有着密切关系,比如一篇文章在博客作者面前要么是“我的文章”,要么是“别人的文章”,而在管理员面前要么是“普通文章”,要么是“其他文章”。这是很合理的:一条记录在不同的角色面前以不同的角度呈现。

所以通过角色,我们可以将“用户—操作—记录”映射到“角色—操作—资源”的步骤改为:首先完成用户到角色的映射,然后对每种角色,列出记录到资源的映射,然后搜索是否存在允许的授权。这样能够明显减少判断的次数。

我们还要考虑记录的继承关系。这个逻辑不复杂:当一条记录搜索不到授权时,还要获取其父记录并搜索父记录的授权。直到所有的父记录都找不到授权,才能返回授权不通过。

然后我们要考虑同步:对同一个“角色(不是用户,因为这样同步范围更大)—操作—记录(不是资源,因为这样搜索更精准)”的鉴权需要进行同步锁,这样可以避免重复搜索浪费资源。

最后我们要考虑如何缓存。通常为了提高鉴权效率,所有已经判断的授权都要缓存起来可以避免重复搜索。缓存是对记录(而非资源)授权的缓存,即缓存“用户/角色—操作—记录”,这样可以避免重复搜索父记录授权。

当授权变更时,缓存如何维护?这个问题也要考虑。当添加一条授权时,我们不需要关心,因为经过一段时间的搜索,相应的缓存记录就会自动补充起来;当一条“角色—操作—资源”授权被删除时,需要删除:1)所有对应的“角色—操作—记录”缓存;2)找到角色对应的用户,删除所有对应的“用户—操作—记录”缓存。这个过程效率当然可能不高,但考虑到删除授权本身不会很频繁,所以应该能够接受。

至此为止,表设计和鉴权的逻辑过程我们都清晰了,然后是如何实现。鉴权过程的实现关键在于映射,特别是从记录到资源的映射。这个映射是不可能光靠数据库配置来完成的,必须要有代码逻辑,比如博客系统的文章映射到“我的文章”,就需要判断该用户是不是文章的作者。这样的逻辑我们可以抽象为一个接口:

/**
 * 将记录映射到资源的接口。不同类型的记录应该有不同的实现类
 
*/
public interface ResourceMapper {

    /**
     * 根据角色将记录映射到资源
     * 
@param recordId 记录ID
     * 
@param roleId   角色ID
     * 
@return 资源ID
     
*/
    Long getResourceId(Long recordId, Long roleId);

    /**
     * 搜索父记录,如果不存在则返回 null
     * 
@param recordId  记录ID
     * 
@return 父记录ID,如果不存在则返回 null
     
*/
    Long getParent(Long recordId);
}

至于其他的部分,本文就不赘述了。我要赶紧去实现一个看看。

最后总结一下这个鉴权系统的主要部分:
1、数据库设计:角色表(ID,名称),鉴权表(角色,操作,资源),资源表(ID,名称,角色【如果为空则表示对所有角色可见】)
2、鉴权的核心逻辑:映射 + 搜索
3、鉴权的外围逻辑:缓存
4、需要用户实现的逻辑:映射接口 ResourceMapper