前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实战!用 Spring Gateway 配合 Sa-Token 实现微服务无感鉴权

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

作者头像
HikariLan贺兰星辰
发布2023-10-18 15:38:08
2.2K1
发布2023-10-18 15:38:08
举报
文章被收录于专栏:HikariLan's BlogHikariLan's Blog

由 ChatGPT 生成的文章摘要

博主在本文中介绍了如何使用Spring Gateway和Sa-Token实现无感鉴权的方案。他首先提到了Spring Cloud Gateway和Sa-Token的基本概念和功能。然后,博主指出了直接在网关中实现鉴权的局限性,并提出了一种无感鉴权的解决方案。在这个方案中,博主通过在请求中注入用户ID的方式实现了无感鉴权,从而避免了下游微服务依赖Sa-Token的问题。接着,博主详细介绍了如何引入依赖、创建路由、实现鉴权接口和注册全局过滤器的步骤。最后,博主展示了如何为Webflux请求添加过滤器,获取用户登录ID并在请求头中注入,并呈现了下游微服务如何获取到用户ID的方法。博主还表达了对Sa-Token近期文档更新中强制要求用户star并授权的不满,认为这种行为是欺诈和对国内开源环境的打击,并希望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 的相关依赖:

代码语言:javascript
复制
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 类,创建路由逻辑,例如:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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 类,添加路由鉴权逻辑,例如:

代码语言:javascript
复制
@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,并将其添加到请求头中:

代码语言:javascript
复制
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

代码语言:javascript
复制
@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 能重新考虑该功能的设立,还国内一个良好的开源环境。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-8-08 1,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 实战!用 Spring Gateway 配合 Sa-Token 实现微服务无感鉴权
    • 前言
      • 无感鉴权的实现
        • 引入依赖
        • 创建路由
        • 实现鉴权接口
        • 注册全局过滤器
        • 添加过滤器,实现无感鉴权
        • 下游微服务获取用户 ID
      • 最后
      相关产品与服务
      负载均衡
      负载均衡(Cloud Load Balancer,CLB)提供安全快捷的流量分发服务,访问流量经由 CLB 可以自动分配到云中的多台后端服务器上,扩展系统的服务能力并消除单点故障。负载均衡支持亿级连接和千万级并发,可轻松应对大流量访问,满足业务需求。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档