实战!用 Spring Gateway 配合 Sa-Token 实现微服务无感鉴权

实战!用 Spring Gateway 配合 Sa-Token 实现微服务无感鉴权

前言

众所周知,Spring Cloud Gateway 是一个基于 Spring WebFlux 技术构建的高性能微服务网关,通过 Spring Cloud Gateway,我们可以实现对微服务的负载均衡,服务治理等功能;Sa-Token 则是一款轻量级的 Java 权限认证框架,通过 Sa-Token 我们可以非常简便的实现服务的鉴权功能。

在业务实践中,我们可以直接在网关对需要鉴权的路由进行访问鉴权,阻止未登录或无权限用户访问指定 API/页面。Sa-Token 的文档也描述了这种网关统一鉴权的解决方案,但这依然不能解决一些问题:

  1. 下游微服务依然需要依赖 Sa-Token(或者通过中间件)获取用户信息,没有做到无感鉴权;
  2. 由于上述原因,导致下游微服务与 Sa-Token 耦合度过高,并且由于需要重复获取一次用户信息(在网关已经获取了一次),造成了额外的数据访问。

因此,本文提供了一种无感鉴权的方案,通过直接向下游微服务请求注入用户 ID 的方式,做到了无感鉴权,使鉴权服务对下游微服务保持透明。

本文全程使用 Java 17 + Spring Boot 3 作为示例,对于传统 Java 8 + Spring Boot 2 项目,除部分依赖需使用 Spring Boot 2 适配版本,整体代码变化不大。

无感鉴权的实现

引入依赖

首先,创建一个标准 Spring Boot 3 项目,并引入 Spring Cloud Gateway 和 Sa-Token 的相关依赖:

scss 复制代码
plugins {
  // 引入 Java 插件
  java
  // 引入 Spring Boot 插件
  id("org.springframework.boot") version "3.1.2"
  // 引入 Spring 依赖管理插件
  id("io.spring.dependency-management") version "1.1.2"
}
​
java {
    // 设置 Java 源代码版本为 Java 17
    sourceCompatibility = JavaVersion.VERSION_17
}
​
repositories {
  // 引入 Maven 中央库
  mavenCentral()
}
​
// 设置 Spring Cloud 版本
extra["springCloudVersion"] = "2022.0.4"
​
dependencies {
    // 引入 Spring Cloud Gateway 的 Spring Boot starter 依赖
    implementation("org.springframework.cloud:spring-cloud-starter-gateway")
​
    // 重要:引入 Sa-Token 的 Spring Boot 3 Webflux 依赖(而不是 Spring Boot 2 Webflux)
    implementation("cn.dev33:sa-token-reactor-spring-boot3-starter:1.35.0.RC")
    // 引入 Sa-Token 的 redis 支持依赖
    implementation("cn.dev33:sa-token-redis:1.35.0.RC")
    // 引入连接池
    implementation("org.apache.commons:commons-pool2")
}
​
dependencyManagement {
  // 导入 Maven Bom
  imports {
    mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
  }
}

创建路由

创建 RoutesConfiguration 类,并将其注册为 Configuration 类,创建路由逻辑,例如:

kotlin 复制代码
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
@Configuration
public class RoutesConfiguration {
​
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("service-user", r -> r.path("/api/users/**")
                        .uri("lb://service-user"))
                .route("service-auth", r -> r.path("/api/authorization/**")
                        .uri("lb://service-auth"))
                .route("frontend", r -> r.path("/**")
                        .uri(frontendUrl))
                .build();
    }
​
}

实现鉴权接口

创建 StpInterfaceImpl 类,实现 StpInterface 类并将其注册为 Component:

typescript 复制代码
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;
​
import java.util.List;
​
@Component
public class StpInterfaceImpl implements StpInterface {
​
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        return List.of(); // TODO: 返回此 loginId 拥有的权限列表
    }
​
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        return List.of(); // TODO: 返回此 loginId 拥有的角色列表
    }
​
}

注册全局过滤器

创建 SaTokenConfigure 类,并将其注册为 Configuration 类,添加路由鉴权逻辑,例如:

scala 复制代码
@Configuration
public class SaTokenConfigure {
    // 注册 Sa-Token全局过滤器 
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
            // 拦截地址 
            .addInclude("/**")    /* 拦截全部path */
            // 开放地址 
            .addExclude("/favicon.ico")
            // 鉴权方法:每次访问进入 
            .setAuth(obj -> {
                // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 
                SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin());
                
                // 权限认证 -- 不同模块, 校验不同权限 
                SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
                SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
                SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
                SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
                
                // 更多匹配 ...  */
            })
            // 异常处理方法:每次setAuth函数出现异常时进入 
            .setError(e -> {
                return SaResult.error(e.getMessage());
            })
            ;
    }
}

添加过滤器,实现无感鉴权

为 Webflux 请求添加过滤器,从 Sa-Token 获取用户登录 ID,并将其添加到请求头中:

java 复制代码
import cn.dev33.satoken.stp.StpUtil;
import io.hikarilan.nerabbs.common.BizConstants;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
​
@Component
public class AuthorizeFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest newRequest = exchange
                .getRequest()
                .mutate()
                .header("X-User-ID", StpUtil.getLoginId(-1L).toString())
                .build();
        ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
​
        return chain.filter(newExchange);
    }
}
​

以上代码拦截了 HTTP 请求,获取了 Sa-Token 存储的当前请求用户登录 ID,并将其注入到 X-User-ID 请求头中。如果用户未登录则返回 -1

下游微服务获取用户 ID

最后,任何下游微服务只需要获取 X-User-ID 请求头便可得知用户登录 ID(或者未登录,得到 -1

less 复制代码
@GetMapping
@ResponseBody
public UserBasicInfoVo getUserBasicInfoFromHeader(@RequestHeader("X-User-ID") long userID) {
    if (userID == -1)
        throw new UnauthorizedException();
​
    return userInfoService.getUserBasicInfoByID(userID);
}

如此一来,我们便做到了无感鉴权以及对 Sa-Token 鉴权服务的解耦。

最后

最后发点自己的小牢骚,我曾经是很看好 Sa-Token 这款框架的,因为他用起来的心智负担确实比 Spring Security 低很多,很容易就能搭建一套鉴权系统出来。但是前几天发生的一个事情却让我近乎想要拉黑这个软件,乃至不再想写这篇文章。而这一切的罪魁祸首就是 Sa-Token 最近对其文档的更新:

update doc · dromara/Sa-Token@2ef8a82 (github.com)

在本次修改中,Sa-Token 强制要求用户必须前往其 Gitee 仓库对该软件 star,且授权 Sa-Token 的远程服务器获取 Gitee 的 OAuth 权限以检测用户是否真正点击了 star。

我认为这种行为无异于是耍流氓,是赤裸裸的欺诈,对国内开源环境的又一重挫。

希望 Sa-Token 能重新考虑该功能的设立,还国内一个良好的开源环境。

相关推荐
码猿技术专栏几秒前
Spring Integration 轻松实现服务间消息传递,真香!
java·spring boot
非ban必选7 分钟前
spring-ai-openai调用Xinference1.4.1报错
java·python·spring
zru_960220 分钟前
springboot 项目怎样开启https服务
数据库·spring boot·https
风象南1 小时前
SpringBoot中3种优雅停机的实现方式
java·spring boot·后端
明天过后ww3 小时前
MySQL SQL 优化的10个关键方向
java·数据库·spring
Kale又菜又爱玩9 小时前
Sentinel全面解析与实战教程
java·spring·微服务·sentinel·springboot·springcloud
hxung9 小时前
springboot项目中常用的工具类和api
数据库·spring boot·后端
唐人街都是苦瓜脸9 小时前
SpringBoot的简单介绍
java·spring boot·后端
菜鸟起航ing10 小时前
【Java面试系列】Spring Boot微服务架构下的分布式事务处理与Seata框架实现原理详解 - 3-5年Java开发必备知识
java·spring boot·微服务·seata·分布式事务
xq51486310 小时前
Spring Boot 自动装配原理
java·spring boot·后端