实战!用 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 能重新考虑该功能的设立,还国内一个良好的开源环境。

相关推荐
ExiFengs1 小时前
实际项目Java1.8流处理, Optional常见用法
java·开发语言·spring
捂月1 小时前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
瓜牛_gn1 小时前
依赖注入注解
java·后端·spring
一元咖啡2 小时前
SpringCloud Gateway转发请求到同一个服务的不同端口
spring·spring cloud·gateway
天天扭码3 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
FIN技术铺3 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端
小码的头发丝、4 小时前
Spring Boot 注解
java·spring boot
午觉千万别睡过4 小时前
RuoYI分页不准确问题解决
spring boot
java亮小白19974 小时前
Spring循环依赖如何解决的?
java·后端·spring