您现在的位置是:首页 >技术教程 >Spring-Cloud-Gateway 整合 Sa-Token 全局过滤器之路由匹配网站首页技术教程
Spring-Cloud-Gateway 整合 Sa-Token 全局过滤器之路由匹配
Spring-Cloud-Gateway 整合 Sa-Token 全局过滤器之路由匹配
Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。
Sa-Token 旨在以简单、优雅的方式完成系统的权限认证部分。其中在 Springboot/Webflux 中直接对需要鉴权的接口添加注解即可完成鉴权配置,不过当我们在需要全局拦截的情景下,比如网关代理,就必须手动配置需要鉴权的路由了,类似 Spring Filter,但 Sa-Token 将简便得多。
Sa-Token 官方文档上的路由拦截鉴权部分,看似很详细,实则介绍的并不到位,笔者在看了一遍后并不能完全精准的配置路由,于是对 Sa-Token 的路由匹配进行了一系列的实践尝试,得到了一套比较完备的实践方案。
以下基于 SpringCloud Gateway 整合 Sa-Token 时注册的全局过滤器
注册过滤器
首先,需要建立一个全局过滤器,由于 SpringCloud Gateway 是响应式的,所以使用的是 SaReactorFilter。
@Configuration
public class SaTokenConfigure {
@Resource
Gson gson;
// 注册 Sa-Token全局过滤器
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter();
}
}
拦截范围
这是这个过滤器生效的范围,对于网关应用来说通常就是全部范围
return new SaReactorFilter()
// 拦截全部
.addInclude("/**");
赦免范围
之前过滤器拦截了所有的路由,对于资源型、脚本形等的路由绝大部分时候不需要拦截,因此我们对它们进行特别放行
.addExclude("/**.ico")
.addExclude("/**.svg")
.addExclude("/**.html")
.addExclude("/**.css")
.addExclude("/**.js")
需要放行哪些文件看情况决定,像我做的项目绝大部分都是纯后端,后端只暴露交互接口,我就可以不设置排除项。
错误处理
后端都会有全局错误处理,网关自然也不能落下,很不幸的是常见的 @ControllerAdvice
和@RestControllerAdvice
在这里不起作用,是因为 SaToken 全局过滤器里面已经自带了全局错误处理,可以通过setError
进行简单配置
.setError(e -> {
return gson.toJson(Result.bad(e.getMessage()));
});
返回的内容将被转为字符串返回给前端,我为了与后端保持一致的返回格式,定义了一个返回类,并将其转为了Json字符串。如果你不希望鉴权失败时返回给前端字符串而是Json对象,可能就需要重写它的全局错误处理。
路由匹配
最核心的部分,路由匹配,需要通过setAuth
进入路由鉴权设置,通过match
来匹配指定形式的路由
.setAuth(obj -> {
SaRouter.match("/**")
.check(StpUtil::checkLogin);
/** 或者这样
SaRouter.match("/**", StpUtil::checkLogin);
*/
})
以上行为对所有的路由都开启了登录检验,一个match相当于 if 中的一个条件,如果后面再链接更多的match,则必须要所有的match都匹配上,才算匹配成功。
全开能保证不会遗留,但在全开的情况下,设置排除项将会比较麻烦。因为,当一个请求达到网关时,进入Auth域之后,会从上往下依次比对,假设/user/login
需要放行,你不能下面一行直接加一句SaRouter.notAMtch("/user/login", StpUtil::checkLogin);
,因为在上面一行它已经没有通过校验被抛出异常了,你需要的是
.setAuth(obj -> {
SaRouter.match("/**", "/user/login", StpUtil::checkLogin);
})
或者这样:
.setAuth(obj -> {
SaRouter.match("/**")
.notMatch("/user/login)
.check(StpUtil::checkLogin);
})
这样有2个弊端:
- 需要排除的接口往往很多,都挤在一个对象上太臃肿
- 没法做细致的匹配规则,比如Rest接口通常一个路由有不同的请求方式,第1种就没办法配置
适当的匹配粒度
更好的做法是按一定的粒度划分,按模块,按路由甚至按指定方法的路由匹配一个规则,是的,你可以直接写一个个精确路由地址的 SaRouter 规则:
// order-service
SaRouter.match("/order/client/cancelOrder")
.check(r->StpUtil.checkLogin())
.check(r->StpUtil.checkPermission("1"));
SaRouter.match("/order/updateOrderState")
.check(r->StpUtil.checkLogin())
.check(r->StpUtil.checkPermission("0"));
SaRouter.match("/order/finishOrder")
.check(r->StpUtil.checkLogin())
.check(r->StpUtil.checkPermission("0"));
SaRouter.match("/order/page/order")
.check(r->StpUtil.checkLogin());
Rest接口规则
区别于上面路由名意图的明确,Rest接口往往共有一个路由,通过不同的请求方法进行区分,此时就需要借助 match(SaHttpMethod)
来匹配指定的请求方式
SaRouter.match(SaHttpMethod.GET)
.match("/message/msg")
.check(r->StpUtil.checkLogin());
上面这段代码就是对Rest接口/message/msg
的GET请求进行了登录检验,那如果还有其它的方式,需要检验,但检验的不太一样,那我们要像如下几乎雷同的写4个独立的匹配规则吗?
SaRouter.match(SaHttpMethod.GET)
.match("/message/msg")
.check(r->StpUtil.checkLogin());
SaRouter.match(SaHttpMethod.POST)
.match("/message/msg")
.check(r->StpUtil.checkRole(1));
SaRouter.match(SaHttpMethod.PUT)
.match("/message/msg")
.check(r->StpUtil.checkRole(0));
SaRouter.match(SaHttpMethod.DELETE)
.match("/message/msg")
.check(r->StpUtil.checkPermission(1));
当然不!SaRouter.free
可以开辟一段独立的匹配域,利用它我们可以巧妙的将一定范围内的规则聚合在一起:
SaRouter.match("/sv2/api/")
.free(r->{
SaRouter.match(SaHttpMethod.POST)
.check(StpUtil::checkLogin);
SaRouter.match(SaHttpMethod.PUT)
.check(StpUtil::checkLogin);
});
上面这段代码将/sv2/api/
这个Rest接口不同的独立规则聚合到了一起,这个形式更体现模块化的特性,可以提高代码的可维护性,毕竟它有一定的组织,而不是完全的零散。
总结: 接口匹配应当模块化,粒度要合适,不应过于集中,也不宜过于零散