业务-接口鉴权设计与实现

1. 需求分析

难点在于,请求一个接口时,怎么确定当前请求的接口所对应的权限记录

request对象如下图:

所以可以根据request的path进行判断。

数据库中存储了接口的信息,如下图

permission表中的记录,会关联到具体的接口

如图resource_type=0表示该权限记录对应的限制资源时接口,resource_id就是关联到interface表的外键

token中有用户的Uid以及用户的角色,角色用List<Integer>表示,一个用户有多个角色,存储的是role表记录的主键id

所以鉴权时,可以直接从token中获取角色信息,也就能获取到所有能够访问的接口(role和permission关联)信息。

此时,在判断当前请求的接口在不在这个有限集合中,就能得到鉴权结果,那么要怎么才能知道当前请求的接口是哪一个接口呢?

根据一个Request对象我们能得到的信息只有请求的method、uri等信息,所以初步考虑,根据请求uri的path进行判断。

潜在问题:请求url可能会被网关过滤器进行修改后再路由,所以要将可能修改url的filter放在tokenFilter的前面,保证TokenFilter拿到的path是最终要请求具体服务的Path。

此外,还要考虑到以下问题

  • Path中的@PathVariable参数对Path匹配会带来影响
  • 需要支持前缀匹配,匹配到path前面指定一部分,则表示匹配成功
  • Restful风格的path设计,同一个url可能应不同方法的请求接口,所以请求方法也要考虑进去,采用method+uri确定接口的范式
  • 功能接口组,对于管理员用户来说授权一个功能给某用户,意味着授权一组接口,与上面一条可能重复,实现方式不同

针对以上问题,提出以下设计:

  • 对于PathVariable参数内嵌在path中,在存入interface表记录时,一律用*替代@PathVariable参数,*表示匹配任意一个单词
  • 前缀匹配的问题,采用**表示匹配Path中的一个或多个单词

如果interface表中一条记录采用了前缀匹配的uri,那么这条记录对应的可能就不止一个接口了。

2. 缓存设计

2.1 redis缓存

TokenFilter拿到roleId之后,需要去缓存中查询当前请求的权限,这里使用缓存,一是为了提高性能,二是为了解耦,权限管理的接口并不在网关中,使用缓存解耦可以减少服务间调用。这种情况下,权限管理服务需要保证权限记录的缓存数据库读写一致性的问题。

使用String类型数据类型来存储

redisKey 为 passport:role:${roleId}

value值为jsonArray,如下所示:

1
2
3
4
5
6
7
[
"POST:/passport/check/raise",
"POST:/passport/login",
"POST:/passport/check/code",
"POST:/passport/reset",
"POST:/passport/check/weixin"
]
2.2 本地二级缓存

每次鉴权都需要访问缓存获取对应角色的ant表达式jason字串,然后解析成列表,所以可以将解析好的列表在本地进行缓存,生产环境中,角色对应的接口权限是一个读多写少的数据,所以使用本地缓存可以有效的提高效率。

需要考虑多级缓存一致性的问题:当redis中的缓存内容修改时,在redis中发布一个消息,网关服务监听到该消息,就将本地缓存中对应的roleId删除

由于redis消息是不可靠的通知机制,所以此处可以引入消息队列

此外还可以启动一个定时任务,定期清除本地缓存,但是这样一旦发生不一致的情况可能会有一个窗口期维持这种不一致。

3. 实现

对于ant表达式,使用Spring自带的AntPathMatcher工具类进行匹配。