Spring Cloud Gateway 集成Sa-Token

6,768 阅读5分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。

引言

Hello 大家好,这里是Anyin。

在我之前的文章中,不知道大家有没有发现我的代码都是放在Anyin Cloud这个项目的(欢迎大家点个星星)。这个项目我积累了一些我自己平时在工作当中小工具和最佳实践,随着时间的推进这个项目已经慢慢成为一个我个人搭建项目的脚手架,能够快速搭建起来一个完善的基于Spring Cloud技术栈的微服务基础架构。

之前在掘金上看到一个权限认证的框架Sa-Token,简单的了解下,发现确实容易上手,而且功能丰富。今天就让我们来把它集成到 Anyin Cloud项目吧。

需求梳理

在把Sa-Token集成到我们的项目之前,我们需要先梳理下需求,不能为了集成而集成。

  1. Anyin Cloud项目需要一个认证鉴权的框架,经过选型,确定使用Sa-Token
  2. Anyin Cloud项目是一个微服务项目,所以我们统一的认证需要放在认证服务Auth,而统一的鉴权是放在网关Gateway
  3. AuthGateway都是高频访问的服务,需要足够轻量,所以我们设计这两个服务都不依赖数据库,并且不会过多依赖其他服务,Auth服务和Gateway服务的数据通信通过Redis
  4. Gateway服务需要把当前登录用户的标识传递到下游。

Sa-Token集成

集成认证Auth服务

首先,我们先来处理Auth服务。

添加pom.xml依赖,因为我们需要通过Redis来进行数据通讯,所以需要依赖对应的Redis组件。

        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-dao-redis-jackson</artifactId>
        </dependency>

Auth服务编写Login方法。按我们之前的需求,Auth需要足够轻量,所以它不会去依赖数据库,在登录的时候需要用户信息,通过用户服务upms远程调用从而获取用户信息。

@Component
@Slf4j
public class UsernameLoginHandler implements LoginHandler {
    @Autowired
    private SysUserFeignApi sysUserFeignApi;
    @Override
    public LoginTypeEnum extension() {
        return LoginTypeEnum.USERNAME;
    }
    @Override
    public LoginUserDTO login(String... content) {
        // TODO param check
        String username = content[0];
        String password = content[1];
        SysUserResp sysUser = sysUserFeignApi.infoByUsername(username).getData();
        if(sysUser == null){
            throw AuthExCodeEnum.USER_NOT_REGISTER.getException();
        }
        // TODO add salt
        if(!sysUser.getPassword().equals(SecureUtil.md5(password))){
            throw AuthExCodeEnum.USER_PASSWORD_ERROR.getException();
        }
        if(UserStatusEnum.DISABLE.getCode().equals(sysUser.getStatus())){
            throw AuthExCodeEnum.USER_IS_DISABLE.getException();
        }
        sysUser.setPassword(null);

        StpUtil.login(sysUser.getId());
        SaTokenInfo token = StpUtil.getTokenInfo();

        LoginUserDTO user = new LoginUserDTO();
        user.setSysUser(sysUser);
        user.setToken(token);
        return user;
    }
}

根据Sa-Token建议的使用方式,在我们对用户密码进行校验正确之后,通过StpUtil.login来进行框架内部的登录操作,这个操作其实是在Redis上记录对应的信息。在Redis上会记录2个信息:

  • 用户ID,satoken:login:session:开头,它的值还会包含一些其他的信息
  • 登录的Token,satoken:login:token:开头,它的值就是用户ID

在登录之后,我们还需要获取token返回给前端,所以这里使用StpUtil.getTokenInfo()获取token信息,最后组装用户信息和token信息返回给前端。

image.png

代码编写好,我们需要对登录接口做下测试。

image.png

好了,登录的认证我们已经处理好了,简不简单?香不香 ?

集成鉴权Gateway服务

我们接着处理鉴权Gateway服务。老规矩,先添加依赖。

这里要特别注意了,因为我们的网关是Spring Cloud Gateway,底层是WebFlux实现,它是基于Reactor模型编程的;而之前的Auth服务是正常的SpringMVC服务,基于Servlet模型编程的。

所以我们这里引入的是sa-token-reactor-spring-boot-starter

        <!-- sa-token -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-dao-redis-jackson</artifactId>
        </dependency>

接着,我们继续编写Gateway服务的过滤器,在过滤器中我们主要做以下3个事情:

  1. 判断哪些路由需要进行鉴权,哪些不需要。
  2. 如果需要鉴权,则判断是否登录,未登录则直接返回异常信息。
  3. 如果登录,则透传用户ID到下游服务

对于判断哪些路由需要鉴权,我们可以在动态路由中配置路由的元数据,从而判断当前路由是否鉴权。代码如下:

        Route route = (Route)exchange.getAttributes().get(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        Integer needAuth =(Integer) route.getMetadata().get(GatewayConstants.SYS_ROUTE_AUTH_KEY);
        // 无需认证的路由
        if(!NEED_AUTH.equals(needAuth)){
            return chain.filter(exchange);
        }

当路由需要进行鉴权的时候,我们再使用Sa-Token提供的isLogin方法进行判断,如果未登录则响应异常信息。如下:

        // 判断是否登录
        if(!StpUtil.isLogin()){
            return this.response(exchange, CommonExCodeEnum.USER_NOT_LOGIN.getException());
        }

如果当前请求用户已经登录,则使用StpUtil.getLoginId方法获取当前用户ID,然后透传到下游服务,如下:

    private ServerWebExchange setHeaderLoginId(ServerWebExchange exchange, String loginId){
        ServerHttpRequest request = exchange.getRequest().mutate().header(CommonConstants.USER_ID, loginId).build();
        return exchange.mutate().request(request).build();
    }

最后,很关键的一步,我们还需要注册全局的过滤器,除了我们自己编写的过滤器,还有Sa-Token的过滤器。如果细心的同学可以发现StpUtil工具类提供的方法都是很简单,得益于它需要注册一个全局的过滤器SaReactorFilter,通过该过滤器它把大量的上下文信息和对应的逻辑都在内部处理掉。所以,我们需要注册2个过滤器,如下:

    @Bean
    public GatewayAuthFilter gatewayAuthFilter(){
        return new GatewayAuthFilter();
    }
    @Bean
    public SaReactorFilter saReactorFilter(){
        return new SaReactorFilter();
    }

其实SaReactorFilter过滤器提供了很多的方法和参数,用来处理各种业务场景,但是因为我这边可能需要对过滤器进行更加定制化的逻辑处理,所以未使用它内部的一些方法。

测试

完成了以上2部分代码,我们需要对其进行测试下,看看框架是否好事。

首先,测试下不传递token的场景,是否会报未登录的异常。

image.png

传递token的场景下,能够正常返回信息

image.png

最后

感谢Sa-Token作者提供了这么一个牛皮的框架。其文档地址 Sa-Token

以上,就是Spring Cloud Gateway集成Sa-Token的步骤,如果有什么问题,欢迎指正。
后面会深入Sa-Token源码,了解更多的使用方法和设计思想,敬请期待。

相关源码:Anyin Cloud