微服务网关鉴权之sa-token

news2025/1/30 9:00:22

目录

前言

项目描述

使用技术

项目结构

要点

实现

前期准备

依赖准备

统一依赖版本

模块依赖 

配置文件准备

登录准备 

网关配置token解析拦截器

网关集成sa-token

配置sa-token接口鉴权

配置satoken权限、角色获取

通用模块配置用户拦截器

api模块配置feign拦截器

读后感


前言

本人在校大三,去年学习了微服务,但一直没有实战过。学习完过后感觉不过尔尔,现在实际动手操作了一下,困难重重,踩了不少的坑。终于感慨学习和实战,实乃天壤之别。

我一路摸爬滚打,只希望新上手微服务的读者能顺顺利利的掌握,而不是一路磕磕绊绊深一脚浅一脚的踩坑前进,于是我写这篇文章提供给读者一个参考。

。。。。

微服务拆分一般有两种模式:maven聚合和独立project

  • maven聚合:每个服务是一个独立的maven模块,所有的模块通过maven聚合到一个父工程中
  • 独立project:每个服务是一个独立的project项目,并且这些project内部还能拆分模块。一般把这些多个project放在同一个文件夹中

第一种方式较为简单,第二种适合大型项目,较为复杂。本文采用第一种:maven聚合的方式,

微服务一般有两种鉴权模式:即网关统一鉴权和每个服务独自鉴权

  • 网关统一鉴权:在网关处进行权限校验,如果权限具备则转发给目标服务,否则直接返回错误,其他服务只编写业务代码,而不关注权限。
  • 服务各自鉴权:网关直接转发请求到目标,目标服务完成自己的接口鉴权。该模式与单体架构类似

两种方式都有各自的优缺点。本文采用第一种方式是因为第二种方式与单体架构类似,咱已经掌握,就不必花费心思了。

项目描述

截止写这篇文章时,本项目还处于搭建后台管理的初步阶段。且模块较少,描述简单,更易理解

使用技术

语言:java(jdk17)

框架版本:

  • springboot:3.2.4;
  • springcloud:2023.0.3;
  • spring.cloud.alibaba:2023.0.0.0-RC1
  • sa-token:1.39.0(boot3)
  • nacos:2.4.3

项目结构

  • api:主要封装feign的接口,以及feign传递的参数模型还有feign的配置,该模块被其他需要发送feign调用的服务集成
  • auth:认证授权服务,目前仅实现了登录功能
  • common:通用工具模块,主要封装一些通用的工具类,以及依赖。该模块被其他模块和服务集成
  • gateway:网关服务,主要用于服务鉴权,路由转发,负载均衡
  • system:系统服务,本项目系统的业务功能。
  • script:脚本,比如数据库脚本
  • ui:前端项目
  • doc:文档,比如软件需求规格说明书,软件使用手册等(未实现)

要点

项目鉴权一般需要解决两个问题:接口鉴权和用户信息传递。

为什么要解决这两个问题?

接口鉴权:自然就是判断当前请求是否具有权限访问服务的接口。如果没有权限就拒接访问。

用户信息传递:服务需要获取当前请求是哪个用户,因为有些业务需要记录用户操作

怎么解决这两个问题?

对于服务各自鉴权模式来说就很简单,因为每个服务都集成了satoken,接口鉴权和用户信息获取跟单体模式一样简单(但是feign请求还是需要配置拦截器添加token请求头)。这里就不阐述了。

但是网关统一鉴模式要解决这两个问题就比较麻烦。其中实现大概要点如下:

  1. 在网关处配置satoken鉴权,实现转发服务之前校验权限
  2. 在网关处解析token,拿到用户ID,添加到请求头中。然后再转发给目标服务
  3. 在通用模块中配置MVC拦截器,判断请求头中是否具有用户id,如果有就存到线程变量中。
  4. 在api模块中配置feign拦截器,判断当前线程变量中是否具有用id,如果有就添加到请求头中

是不是感觉简单?其实不然,真实操作的时候有较多的坑!!!(;´༎ຶД༎ຶ`)

实现

前期准备

依赖准备

统一依赖版本

父模块主要用于统一依赖版本,其pom.xml如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>
    <modules>
        <module>sc-common</module>
        <module>sc-system</module>
        <module>sc-gateway</module>
        <module>sc-auth</module>
        <module>sc-api</module>
    </modules>
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>3.2.4</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
    <groupId>com.schoolcolud</groupId>
    <artifactId>SchoolCloud</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.cloud.version>2023.0.3</spring.cloud.version>
        <spring.cloud.alibaba.version>2023.0.0.0-RC1</spring.cloud.alibaba.version>
        <mybatis.version>3.0.3</mybatis.version>
        <mysql.version>8.0.31</mysql.version>
        <hutool.version>5.8.25</hutool.version>
        <satoken.version>1.39.0</satoken.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring.cloud.alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <repositories>
        <repository>
            <id>public</id>
            <name>阿里云公共仓库</name>
<!--            <url>https://repo.maven.apache.org/maven2</url>-->
            <url>https://maven.aliyun.com/repository/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
        </repository>
    </repositories>
</project>
模块依赖 

common模块依赖

  <dependencies>
        <!--        redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>
<!--        服务注册与发现-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--        负载均衡-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
<!--        配置中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
    </dependencies>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

api模块依赖

    <dependencies>
        <!--        远程调用-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.schoolcolud.common</groupId>
            <artifactId>sc-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

 auth模块依赖

<dependencies>
        <dependency>
            <groupId>com.schoolcolud.common</groupId>
            <artifactId>sc-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--        验证码-->
        <dependency>
            <groupId>com.github.whvcse</groupId>
            <artifactId>easy-captcha</artifactId>
            <version>1.6.2</version>
        </dependency>
        <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot3-starter</artifactId>
            <version>${satoken.version}</version>
        </dependency>
        <!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-redis-jackson</artifactId>
            <version>1.39.0</version>
        </dependency>
        <dependency>
            <groupId>com.schoolcolud.api</groupId>
            <artifactId>sc-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

 gateway模块依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- sa-token 权限认证, 在线文档:https://sa-token.cc/ -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
            <version>1.39.0</version>
        </dependency>
        <!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-redis-jackson</artifactId>
            <version>1.39.0</version>
        </dependency>

        <dependency>
            <groupId>com.schoolcolud.api</groupId>
            <artifactId>sc-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>com.schoolcolud.common</groupId>
            <artifactId>sc-common</artifactId>
            <version>1.0-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

system模块依赖

<dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.version}</version>
        </dependency>
<!--        测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-launcher</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.schoolcolud.common</groupId>
            <artifactId>sc-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>
<!--        热启动-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
配置文件准备

gateway配置文件

server:
  port: 8080
spring:
  application:
    name: sc-gateway
  data:
    redis:
      host: localhost
      port: 6379
  cloud:
    nacos:
      server-addr: localhost:8848
      config:
        import-check:
          enabled: false
    gateway:
      routes:
        - id: sc-system #id最好与服务名称相同
          uri: lb://sc-system #路由路径
          predicates: #路由规则
            - Path=/system/** #路径匹配规则,如果匹配该规则,则会被路由到指定的服务
        - id: sc-auth
          uri: lb://sc-auth
          predicates:
            - Path=/auth/**
sa-token:
  # token名称 (同时也是cookie名称)
  token-name: token
  # token有效期,单位s 默认30天, -1代表永不过期
  timeout: 2592000
  # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
  activity-timeout: -1
  # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
  allow-concurrent-login: false
  # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
  is-share: false
  # token风格
  token-style: uuid

 auth配置文件

spring:
  application:
    name: sc-auth
  cloud:
    nacos:
      server-addr: localhost:8848
      config:
        import-check:
          enabled: false
  data:
    redis:
      host: localhost
      port: 6379
server:
  port: 8100

  # sa-token配置
sa-token:
  # token名称 (同时也是cookie名称)
  token-name: token
  # token有效期,单位s 默认30天, -1代表永不过期
  timeout: 2592000
  # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
  activity-timeout: -1
  # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
  allow-concurrent-login: false
  # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
  is-share: false
  # token风格
  token-style: uuid

system配置文件

server:
  port: 8081
spring:
  application:
    name: sc-system
  profiles:
    active: dev
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${my-config.mysql.host}:3306/${my-config.mysql.database-name}?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false
    username: ${my-config.mysql.user-name}
    password: ${my-config.mysql.password}
  data:
    redis:
      host: ${my-config.redis.host}
      port: 6379
      password:
      lettuce:
        pool:
          # 连接池最大连接数
          max-active: 200
          # 连接池最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
          # 连接池中的最大空闲连接
          max-idle: 10
          # 连接池中的最小空闲连接
          min-idle: 0
  cloud:
    nacos:
      discovery:
        server-addr: ${my-config.cloud.server-addr}
      config:
        import-check:
          enabled: false


mybatis:
  mapper-locations: classpath:mapper/*.xml

登录准备 

前面说过,auth是统一处理认证授权的模块,自然登录就在此处,但是由于用户表归为其他服务(比如后台用户表归为system服务),这里就不好集成了,于是采用feign远程调用system服务的方式登录。即:前端登录请求--->gateway--->auth---->system。那么为什么不直接从网关到system呢?考虑到后面我会实现前台用户表(多账户认证sa-token也有对应的措施:点我跳转查看文档),并且社交登录也能作为扩展,那么单独一个auth出来是有必要的,主要是也不费事哈哈。。。

package com.schoolcolud.auth.controller;

import cn.dev33.satoken.stp.StpUtil;
import com.schoolcloud.common.model.R;
import com.schoolcolud.api.client.SystemFeignService;
import com.schoolcolud.api.dto.LoginModel;
import com.wf.captcha.SpecCaptcha;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("auth")
@RequiredArgsConstructor
@Slf4j
public class LoginController {
    private final String KEY_PREFIX = "login-key:";
    private final RedisTemplate<String, String> redisTemplate;

    private final SystemFeignService sysUserFeignService;

    @PostMapping("/admin/login")
    public R sysUserLogin(@RequestBody LoginModel loginModel) {
        //验证验证码的正确性
        String path = KEY_PREFIX + loginModel.getCaptchaKey();
        String s = redisTemplate.opsForValue().get(path);
        if (s == null || !s.equals(loginModel.getCaptchaValue())) {
            return R.err("验证码错误!");
        }
//        远程调用system服务获取用户Id
        R<String> r = sysUserFeignService.login(loginModel);
//        设置登录凭证
        StpUtil.login(r.getData());
//        返回token
        return R.ok(StpUtil.getTokenValue());
    }

    /**获取验证码
     * @return {@link R}<{@link Map}<{@link String}, {@link String}>>
     */
    @RequestMapping("/captcha")
    public R<Map<String, String>> loginCaptcha() {
        SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 5);
        String verCode = specCaptcha.text().toLowerCase();
        String key = UUID.randomUUID().toString();
        String path = KEY_PREFIX + key;
        // 存入redis并设置过期时间为2分钟
        redisTemplate.opsForValue().set(path, verCode, 10, TimeUnit.MINUTES);
        HashMap<String, String> map = new HashMap<>();
        map.put("captchaKey", key);
        map.put("captchaImage", specCaptcha.toBase64());
        log.info("验证码key为:code:{},验证码值为:value:{}", key, verCode);
        // 将key和base64返回给前端
        return R.ok(map);
    }

    @GetMapping("/test")
    public void test() {
        sysUserFeignService.test();
    }
}

其中feign的接口在api模块下

@FeignClient("sc-system")
public interface SystemFeignService {

    @PostMapping("system/user/login")
    R<String> login(@RequestBody LoginModel user) throws LoginException;

    @GetMapping("system/user/test")
    R<String> test();

    @GetMapping("system/permission/code/user")
    public R<List<String>> getUserPermissionCode(@RequestParam String userId);

    @GetMapping("system/role/code/user")
    public R<List<String>> getUserRoleCode(@RequestParam String userId);
}

 至于system就简单了,查询到就返回用户ID,没查询到则直接抛异常

    @PostMapping("/login")
    public R<String> login(@RequestBody SysUser user) throws LoginException {
        String userId = service.login(user.getUserName(), user.getPassword());
        return R.ok(userId);
    }

网关配置token解析拦截器

为什么网关要配置token解析拦截器?

我们项目中,前端传来的请求头携带的token,而非用户id,但是我们的服务并不集成satoken,也就是并不具备从token中解析出用户信息的能力。

说到这里你可能有疑问:为什么不在每个服务中集成sa-token?这样直接通过工具类StpUtil就能获取到用户信息了啊?

但是如果每个服务都集成satoken的话,咱们还要费劲心思在网关鉴权干嘛呢?直接在每个服务中自己校验不就好了?那这样就走的是第二种方案了。。。。

言归正传,既然目标服务不具备解析token的能力,咱们就需要直接把用户信息连同请求传过去就好了。

没错!就是将用户信息直接添加到请求头上!咱们在网关模块中编写一个拦截器,并解析tolen,将用户信息添加到请求头即可。

package com.schoolcloud.gateway.filters;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;

import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

@Configuration
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//        获取request
        ServerHttpRequest request = exchange.getRequest();
//        获取token
        List<String> strings = request.getHeaders().get("token");
        if (strings == null) {
            return chain.filter(exchange);
        }
        //        获取用户ID
        String token = strings.get(0);
        String userId;
        if (StrUtil.isEmpty(token)) {
            return chain.filter(exchange);
        }
        userId = StpUtil.getLoginIdAsString();
//        传递用户信息
        ServerWebExchange build = exchange.mutate().request(builder -> builder.header("user-id", userId)).build();
//        放行
        return chain.filter(build);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

网关集成sa-token

查看sa-token官网,发现sa-token有针对网关鉴权的实现:点我跳转查看文档

配置sa-token接口鉴权

在网关模块编写代码

@Configuration
public class SaTokenConfigure {
    // 注册 Sa-Token全局过滤器
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
                // 拦截地址
                .addInclude("/**")    /* 拦截全部path */
                // 开放地址
                .addExclude("/auth/**")//登录接口
                // 鉴权方法:每次访问进入
                .setAuth(obj -> {
                    // 权限认证 -- 不同模块, 校验不同权限
                    SaRouter.match("/**", r -> {
                        //   如果是超级管理员,直接放行
                        if (StpUtil.hasRole("admin")) {
                            return;
                        }
//                        如果是非超级管理员,则进行后续判断
                        SaRouter.match("/system/**", r1 -> StpUtil.checkPermission("system"));
                    });

                    // 更多匹配 ...  */
                })
                // 异常处理方法:每次setAuth函数出现异常时进入
                .setError(e -> {
                    return SaResult.error(e.getMessage());
                })
                ;
    }
}

额。。。你可能会发现,我在这里有个嵌套的SaRouter.match。因为我想要实现:如果是超级管理员,就无需查询权限,直接放行所有接口,如果不是超级管理员则进行常规权限检查。但是return只会跳出当前的SaRouter.match方法,后续的SaRouter.match也会继续执行,还是会把超级管理员拦截下来(因为我的数据库超级管理员没有设置任何权限数据),除非每个SaRouter.match方法内部都写上判断是否是超级管理员的代码,那这样就太繁琐了。不过好在satoken支持嵌套SaRouter.match,于是我这里采用这种写法。

配置satoken权限、角色获取

在网关处怎么获取角色权限信息呢?sa-token官网有描述:

 个人感觉在网关集成ORM框架,配置数据库的方式是欠缺的,比如:系统有两个用户表,一个前台用户表,后台用户表,这两个表分别是两个服务(后台管理服务和前台系统服务)的数据库表。在微服务中每个服务的数据库都是独立的,网关需要连接两个数据库,这还是其次,毕竟能够实现,但是这个两个数据库是属于某两个独立的服务,不应该还被其他服务也就是网关集成。

其他方式,除非一开始把所有用户的权限角色信息存入redis,否则无论如何都要走数据库。

既然其他服务能够查询权限角色信息。那我们在网关处发起远程调用就行了啊。

package com.schoolcloud.gateway.config;

import cn.dev33.satoken.stp.StpInterface;
import com.schoolcloud.common.model.R;
import com.schoolcolud.api.client.SystemFeignService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

@Component    // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
@Slf4j
@RequiredArgsConstructor
public class StpInterfaceImpl implements StpInterface {

    /**
     *远程调用
     */
    private final SystemFeignService systemFeignService;

    /**
     * 返回一个账号所拥有的权限码集合
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        CompletableFuture<R<List<String>>> r = CompletableFuture.supplyAsync(()->{
//            远程调用,发送feign请求获取用户权限
                     return systemFeignService.getUserPermissionCode(String.valueOf(loginId));
                   });
        try {
            return r.get().getData();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        CompletableFuture<R<List<String>>> r = CompletableFuture.supplyAsync(()->{
//            远程调用,发送feign请求获取用户角色列表
            return systemFeignService.getUserRoleCode(String.valueOf(loginId));
        });
        try {
            return r.get().getData();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }
}

你可能会发现CompletableFuture<R<List<String>>> r = CompletableFuture.supplyAsync(()->{这段代码,没错!因为feign是阻塞式,而网关springgateway是响应式的,直接调用feign接口,直接完蛋(说多了都是泪呜呜。。。),我们需要通过异步方式调用。

假设这样直接调用

我们发送一个请求,会发现报错:block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3

而控制台则不会出现相关错误。 

很不幸的是在这之前还要编写一个配置类

@Configuration
public class GatewayConfig {

    /**Spring Cloud Gateway是基于WebFlux的,是ReactiveWeb,所以HttpMessageConverters不会自动注入。
     * 用于解决网关发送feign请求
     * @param converters
     * @return {@link HttpMessageConverters}
     */
    @Bean
    @ConditionalOnMissingBean
    public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
        return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
    }
}

因为feign需要解析数据,需要这个转换器,而springgateway默认不会配置,我们需要手动创建

否则就会报错

通用模块配置用户拦截器

为什么通用模块要配置用户拦截器?

前面说过,网关解析token然后将用户信息放到请求头上,然后再转发给目标服务,目标服务能够直接从请求头中获取用户信息了,但是这样比较麻烦,首先需要获取请求request,然后再从request请求头中获取用户ID,如果每个服务都这样写,太繁琐了亿点。于是我们在通用模块中配置一个MVC拦截器,如果有用户信息,那就存到线程变量中。

为了方便存取,我们甚至还要写一个工具类:UserContext(哈哈,是不是很熟悉?看过黑马虎老师的读者应该知道了本文的思路来源了吧?)

package com.schoolcloud.common.util;

public class UserContext {
    private static final ThreadLocal<String> tl = new ThreadLocal<>();

    public static void setUser(String userId) {
        tl.set(userId);
    }

    public static String getUser() {
        return tl.get();
    }

    public static void removeUser() {
        tl.remove();
    }
}

为什么采用线程变量?因为一个服务肯定是在一个机器上的,用户请求到服务后,web服务器(比如tomcat)就会为这个请求单独创建一个线程,而线程变量ThreadLocal就能在这个请求线程中随时分享。 

拦截器

package com.schoolcloud.common.interceptors;

import cn.hutool.core.util.StrUtil;
import com.schoolcloud.common.util.UserContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * 如果请求头中有“user-id",那么将其存入threadLocal
 *
 * @author hongmizfb
 * @date 2025/01/24
 */
public class UserInfoInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String userId = request.getHeader("user-id");
        if (StrUtil.isNotBlank(userId)) {
//            如果请求头中有用户信息,就存入线程变量中
            UserContext.setUser(userId);
        }
        return true;
    }
}

编写了拦截器,就要注册这个拦截器。我们编写一个配置类,将刚才的拦截器注册

package com.schoolcloud.common.config;

import com.schoolcloud.common.interceptors.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor());
    }
}

又又很不幸的告诉你,这个配置不会生效。

由于这个代码编写在common模块中,其他服务继承了这个模块,springboot也不会扫描到这个配置,除非common模块在该服务的子包中,但那样不规范,毕竟common应当是一个独立的、公共的模块。

通常两种解决办法:目标服务通过@ComponentScan注解指定扫描包,以及利用springboot提供的自动配置文件实现。

@ComponentScan方式肯定不可取,这样的话每个引入common模块的服务都要编写,忒麻烦。

那就利用springboot提供的自动配置实现,由于我的springboot版本是3.2.4,就采用META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports方式(名称一定不能错了!!!!

如果你是springboot2.7版本以前,那就采用META-INF/spring.factories方式 

api模块配置feign拦截器

为什么要配置feign拦截器?

前面我们说过,网关解析token,并携带用户信息转发给目标服务,那么目标服务就能获取用户信息了,但如果这个服务又需要远程调用其他服务来完成业务,但是痛点是对方也要获取用户信息。这个请求不是从网关转发的,自然不会携带用户信息!

怎么办?难不成在转发前手动添加请求头?那也太逊了吧!

没错编写一个feign拦截器即可!

package com.schoolcolud.api.config;

import cn.hutool.core.util.StrUtil;
import com.schoolcloud.common.util.UserContext;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;

public class DefaultFeignConfig {
    /**
     * 远程调用时携带用户信息(feign请求)
     *
     * @return {@link RequestInterceptor}
     */
    @Bean
    public RequestInterceptor userInfoInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                String user = UserContext.getUser();
                if (StrUtil.isNotBlank(user)) {
//                    如果有则添加到请求头
                    template.header("user-id", user);
                }
            }
        };
    }
}

你有没有发现这里用到了之前写的用户线程变量工具类,如果我们当时嫌麻烦没有编写这个工具类,现在就要抓瞎了,这里可获取不到网关转发时请求的请求头。

读后感

读到这里,你可能有一点明悟了他的流程:

网关鉴权,然后解析token,将用户信息添加到请求头上,转发给目标服务,目标服务集成了common,里面有个拦截器将请求头的用户信息存入到线程变量中;目标服务就能轻松的获取用户信息;如果目标服务需要远程调用其他服务呢?那就集成api模块,里面有个feign拦截器,判断线程变量中是否具有用户信息,如果有就添加到请求头中,然后发出这个请求,而对方服务同样集成了common模块,于是又能轻松的将请求头的用户信息存入线程变量了。。。

快要过年了,笔者在这里提前祝大家新年快乐,希望大家在2025年顺风顺水,万事如意!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2286352.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华为小米vivo向上,苹果荣耀OPPO向下

日前&#xff0c;Counterpoint发布的手机销量月度报告显示&#xff0c;中国智能手机销量在2024年第四季度同比下降3.2%&#xff0c;成为2024年唯一出现同比下滑的季度。而对于各大智能手机品牌来说&#xff0c;他们的市场份额和格局也在悄然发生变化。 华为逆势向上 在2024年第…

国产编辑器EverEdit - 输出窗口

1 输出窗口 1.1 应用场景 输出窗口可以显示用户执行某些操作的结果&#xff0c;主要包括&#xff1a; 查找类&#xff1a;查找全部&#xff0c;筛选等待操作&#xff0c;可以把查找结果打印到输出窗口中&#xff1b; 程序类&#xff1a;在执行外部程序时(如&#xff1a;命令窗…

获取snmp oid的小方法1(随手记)

snmpwalk遍历设备的mib # snmpwalk -v <SNMP version> -c <community-id> <IP> . snmpwalk -v 2c -c test 192.168.100.201 .根据获取的值&#xff0c;找到某一个想要的值的oid # SNMPv2-MIB::sysName.0 STRING: test1 [rootzabbix01 fonts]# snmpwalk -v…

望获实时Linux系统:2024回顾与2025展望

2024年回顾 功能安全认证 2024年4月&#xff0c;望获操作系统V2获ISO26262:2018功能安全产品认证&#xff08;ASIL B等级&#xff09;&#xff0c;达到国际功能安全标准。 EtherCAT实时性增强 2024年5月&#xff0c;发布通信实时增强组件&#xff0c;EtherCAT总线通信抖…

2025_1_29 C语言学习中关于指针

1. 指针 指针就是存储的变量的地址&#xff0c;指针变量就是指针的变量。 1.1 空指针 当定义一个指针没有明确指向内容时&#xff0c;就可以将他设置为空指针 int* p NULL;这样对空指针的操作就会使程序崩溃而不会导致出现未定义行为&#xff0c;因为程序崩溃是宏观的&…

SQL注入漏洞之高阶手法 宽字节注入以及编码解释 以及堆叠注入原理说明

目录 宽字节注入 编码区分 原理 函数 转译符号解释 注意 绕过方式详解 堆叠【Stack】注入攻击 注入语句 宽字节注入 在说宽字节注入之前 我们需要知道编码相关的知识点&#xff0c;这个有助于搞定什么是宽字节注入 分清楚是ascii码是什么宽字节注入代码里面加入了adds…

ADC 精度 第一部分:精度与分辨率是否不同?

在与使用模数转换器&#xff08;ADC&#xff09;的系统设计师交谈时&#xff0c;我经常听到的一个最常见问题是&#xff1a; “你们的16位ADC也是16位准确的吗&#xff1f;” 这个问题的答案在于对分辨率和精度这两个概念的基本理解存在差异。尽管这是两个完全不同的概念&…

生成模型:扩散模型(DDPM, DDIM, 条件生成)

扩散模型的理论较为复杂&#xff0c;论文公式与开源代码都难以理解。现有的教程大多侧重推导公式。为此&#xff0c;本文通过精简代码&#xff08;约300行&#xff09;&#xff0c;更多以代码运行角度讲解扩散模型。 本代码包括扩散模型的主流技术复现&#xff1a; 1.DDPM (De…

【hot100】刷题记录(7)-除自身数组以外的乘积

题目描述&#xff1a; 给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&#x…

鸢尾花书01---基本介绍和Jupyterlab的上手

文章目录 1.致谢和推荐2.py和.ipynb区别3.Jupyterlab的上手3.1入口3.2页面展示3.3相关键介绍3.4代码的运行3.5重命名3.6latex和markdown说明 1.致谢和推荐 这个系列是关于一套书籍&#xff0c;结合了python和数学&#xff0c;机器学习等等相关的理论&#xff0c;总结的7本书籍…

可扩展架构:如何打造一个善变的柔性系统?

系统的构成:模块 + 关系 我们天天和系统打交道,但你有没想过系统到底是什么?在我看来,系统内部是有明确结构 的,它可以简化表达为: 系统 = 模块 + 关系 在这里,模块是系统的基本组成部分,它泛指子系统、应用、服务或功能模块。关系指模块 之间的依赖关系,简单…

C++并发:C++内存模型和原子操作

C11引入了新的线程感知内存模型。内存模型精确定义了基础构建单元应当如何被运转。 1 内存模型基础 内存模型牵涉两个方面&#xff1a;基本结构和并发。 基本结构关系到整个程序在内存中的布局。 1.1 对象和内存区域 C的数据包括&#xff1a; 内建基本类型&#xff1a;int&…

宝塔mysql数据库容量限制_宝塔数据库mysql-bin.000001占用磁盘空间过大

磁盘空间占用过多&#xff0c;排查后发现网站/www/wwwroot只占用7G&#xff0c;/www/server占用却高达8G&#xff0c;再深入排查发现/www/server/data目录下的mysql-bin.000001和mysql-bin.000002两个日志文件占去了1.5G空间。 百度后学到以下知识&#xff0c;做个记录。 mysql…

2859.计算K置位下标对应元素的和

示例 1&#xff1a;输入&#xff1a;nums [5,10,1,5,2], k 1 输出&#xff1a;13 解释&#xff1a;下标的二进制表示是&#xff1a; 0 0002 1 0012 2 0102 3 0112 4 1002 下标 1、2 和 4 在其二进制表示中都存在 k 1 个置位。 因此&#xff0c;答案为 nums[1] nums[…

8. 网络编程

网络的基本概念 TCP/IP协议概述 OSI和TCP/IP模型 socket&#xff08;套接字&#xff09; 创建socket 字节序 字节序转换函数 通用地址结构 因特网地址结构 IPV4地址族和字符地址间的转换(点分十进制->网络字节序) 填写IPV4地址族结构案例 掌握TCP协议网络基础编程 相关函数 …

关于opencv环境搭建问题:由于找不到opencv_worldXXX.dll,无法执行代码,重新安装程序可能会解决此问题

方法一&#xff1a;利用复制黏贴方法 打开opencv文件夹目录找到\opencv\build\x64\vc15\bin 复制该目录下所有文件&#xff0c;找到C:\Windows\System32文件夹&#xff08;注意一定是C盘&#xff09;黏贴至该文件夹重新打开VS。 方法二&#xff1a;直接配置环境 打开opencv文…

Git Bash 配置 zsh

博客食用更佳 博客链接 安装 zsh 安装 Zsh 安装 Oh-my-zsh github仓库 sh -c "$(curl -fsSL https://install.ohmyz.sh/)"让 zsh 成为 git bash 默认终端 vi ~/.bashrc写入&#xff1a; if [ -t 1 ]; thenexec zsh fisource ~/.bashrc再重启即可。 更换主题 …

DeepSeek-R1 本地部署模型流程

DeepSeek-R1 本地部署模型流程 ***************************************************** 环境准备 操作系统&#xff1a;Windows11 内存&#xff1a;32GB RAM 存储&#xff1a;预留 300GB 可用空间 显存: 16G 网络: 100M带宽 ********************************************…

C++ unordered_map和unordered_set的使用,哈希表的实现

文章目录 unordered_map&#xff0c;unorder_set和map &#xff0c;set的差异哈希表的实现概念直接定址法哈希冲突哈希冲突举个例子 负载因子将关键字转为整数哈希函数除法散列法/除留余数法 哈希冲突的解决方法开放定址法线性探测二次探测 开放定址法代码实现 哈希表的代码 un…

C#通过3E帧SLMP/MC协议读写三菱FX5U/Q系列PLC数据案例

C#通过3E帧SLMP/MC协议读写三菱FX5U/Q系列PLC数据案例&#xff0c;仅做数据读写报文测试。附带自己整理的SLMP/MC通讯协议表。 SLMP以太网读写PLC数据20191206/.vs/WindowsFormsApp7/v15/.suo , 73216 SLMP以太网读写PLC数据20191206/SLMP与MC协议3E帧通讯协议表.xlsx , 10382…