实战!用 Spring Gateway 配合 Sa-Token 实现微服务无感鉴权 - V2EX
爱意满满的作品展示区。
HikariLan

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

  •  
  •   HikariLan
    shaokeyibb Aug 8, 2023 3870 views
    This topic created in 1012 days ago, the information mentioned may be changed or developed.

    实战!用 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 的相关依赖:

    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 类,创建路由逻辑,例如:

    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:

    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 类,添加路由鉴权逻辑,例如:

    @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 ,并将其添加到请求头中:

    import cn.dev33.saoken.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

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

    12 replies    2024-04-07 18:08:41 +08:00
    ljsh093
        1
    ljsh093  
       Aug 8, 2023   2
    update doc dromara/Sa-Token@2ef8a82 (github.com)

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

    这还用个啥?
    qinfengge
        2
    qinfengge  
       Aug 9, 2023
    唉,国内开源真是死路一条吗
    papalong
        3
    papalong  
       Aug 9, 2023
    Desdemor
        4
    Desdemor  
       Aug 9, 2023
    @papalong 第一次见这么离谱的
    wanniwa
        5
    wanniwa  
       Aug 9, 2023
    Sa-Token 确实这样很败好感,本来朋友推荐的,我用着也蛮好,昨天看文档的时候发现强制要求收藏
    zagfai
        6
    zagfai  
       Aug 9, 2023
    偷笑一下 Java 系 出来的人才:)
    lifespy
        7
    lifespy  
       Aug 9, 2023
    你的这个思路,我们前年开始就是这么勇的,实践确实很不错。
    感谢分享
    nekoneko
        8
    nekoneko  
       Aug 9, 2023
    Lip's mama opened the door for Lip.
    FormatToday
        9
    FormatToday  
       Aug 10, 2023
    人家还会删评论哦,我在那个 commit 下写了句”成功劝退至少一个用户“,然后被删了。
    邮箱提醒还在呢
    ![]( )
    russ44
        10
    russ44  
       Aug 10, 2023
    从强制关注公众号到强制要 Star
    s1461a
        11
    s1461a  
       Nov 24, 2023
    屏蔽掉那个 http 请求就行
    allgy
        12
    allgy  
       Apr 7, 2024
    真特么离谱
    About     Help     Advertise     Blog     API     FAQ     Solana     3002 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 54ms UTC 02:45 PVG 10:45 LAX 19:45 JFK 22:45
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86