您现在的位置是:首页 >技术教程 >Spring-Cloud-Gateway 整合 Sa-Token 全局过滤器之路由匹配网站首页技术教程

Spring-Cloud-Gateway 整合 Sa-Token 全局过滤器之路由匹配

Evanpatchouli℃ 2024-07-23 12:01:02
简介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接口不同的独立规则聚合到了一起,这个形式更体现模块化的特性,可以提高代码的可维护性,毕竟它有一定的组织,而不是完全的零散。


总结: 接口匹配应当模块化,粒度要合适,不应过于集中,也不宜过于零散

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。