微服务网关鉴权之sa-token
目录
前言
项目描述
使用技术
项目结构
要点
实现
前期准备
依赖准备
统一依赖版本
模块依赖
配置文件准备
登录准备
网关配置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请求头)。这里就不阐述了。
但是网关统一鉴模式要解决这两个问题就比较麻烦。其中实现大概要点如下:
- 在网关处配置satoken鉴权,实现转发服务之前校验权限
- 在网关处解析token,拿到用户ID,添加到请求头中。然后再转发给目标服务
- 在通用模块中配置MVC拦截器,判断请求头中是否具有用户id,如果有就存到线程变量中。
- 在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-gatewaydata:redis:host: localhostport: 6379cloud:nacos:server-addr: localhost:8848config:import-check:enabled: falsegateway:routes:- id: sc-system #id最好与服务名称相同uri: lb://sc-system #路由路径predicates: #路由规则- Path=/system/** #路径匹配规则,如果匹配该规则,则会被路由到指定的服务- id: sc-authuri: lb://sc-authpredicates:- 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-authcloud:nacos:server-addr: localhost:8848config:import-check:enabled: falsedata:redis:host: localhostport: 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-systemprofiles:active: devdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://${my-config.mysql.host}:3306/${my-config.mysql.database-name}?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=falseusername: ${my-config.mysql.user-name}password: ${my-config.mysql.password}data:redis:host: ${my-config.redis.host}port: 6379password:lettuce:pool:# 连接池最大连接数max-active: 200# 连接池最大阻塞等待时间(使用负值表示没有限制)max-wait: -1ms# 连接池中的最大空闲连接max-idle: 10# 连接池中的最小空闲连接min-idle: 0cloud:nacos:discovery:server-addr: ${my-config.cloud.server-addr}config:import-check:enabled: falsemybatis: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服务获取用户IdR<String> r = sysUserFeignService.login(loginModel);
// 设置登录凭证StpUtil.login(r.getData());
// 返回tokenreturn 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 {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取requestServerHttpRequest request = exchange.getRequest();
// 获取tokenList<String> strings = request.getHeaders().get("token");if (strings == null) {return chain.filter(exchange);}// 获取用户IDString 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);}@Overridepublic int getOrder() {return 0;}
}
网关集成sa-token
查看sa-token官网,发现sa-token有针对网关鉴权的实现:点我跳转查看文档
配置sa-token接口鉴权
在网关模块编写代码
@Configuration
public class SaTokenConfigure {// 注册 Sa-Token全局过滤器@Beanpublic 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;/*** 返回一个账号所拥有的权限码集合*/@Overridepublic 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);}}/*** 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)*/@Overridepublic 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@ConditionalOnMissingBeanpublic 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 {@Overridepublic 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 {@Overridepublic 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}*/@Beanpublic RequestInterceptor userInfoInterceptor() {return new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {String user = UserContext.getUser();if (StrUtil.isNotBlank(user)) {
// 如果有则添加到请求头template.header("user-id", user);}}};}
}
你有没有发现这里用到了之前写的用户线程变量工具类,如果我们当时嫌麻烦没有编写这个工具类,现在就要抓瞎了,这里可获取不到网关转发时请求的请求头。
读后感
读到这里,你可能有一点明悟了他的流程:
网关鉴权,然后解析token,将用户信息添加到请求头上,转发给目标服务,目标服务集成了common,里面有个拦截器将请求头的用户信息存入到线程变量中;目标服务就能轻松的获取用户信息;如果目标服务需要远程调用其他服务呢?那就集成api模块,里面有个feign拦截器,判断线程变量中是否具有用户信息,如果有就添加到请求头中,然后发出这个请求,而对方服务同样集成了common模块,于是又能轻松的将请求头的用户信息存入线程变量了。。。
快要过年了,笔者在这里提前祝大家新年快乐,希望大家在2025年顺风顺水,万事如意!!!
相关文章:

微服务网关鉴权之sa-token
目录 前言 项目描述 使用技术 项目结构 要点 实现 前期准备 依赖准备 统一依赖版本 模块依赖 配置文件准备 登录准备 网关配置token解析拦截器 网关集成sa-token 配置sa-token接口鉴权 配置satoken权限、角色获取 通用模块配置用户拦截器 api模块配置feign…...

23【进制的理解】
很多人可能听过计算机的最底层是2进制执行,但是原理并不知道,我们今天先不讨论那么复杂的问题,先讨论什么是进制 1910,10并不是1个字符,而是2个字符,也就是说在10进制里面没有“10”这个字符,1…...

jemalloc 5.3.0的tsd模块的源码分析
一、背景 在主流的内存库里,jemalloc作为android 5.0-android 10.0的默认分配器肯定占用了非常重要的一席之地。jemalloc的低版本和高版本之间的差异特别大,低版本的诸多网上整理的总结,无论是在概念上和还是在结构体命名上在新版本中很多都…...

【Convex Optimization Stanford】Lec3 Function
【Convex Optimization Stanford】Lec3 Function 前言凸函数的定义对凸函数在一条线上的限制增值扩充? 一阶条件二阶条件一些一阶/二阶条件的例子象集和sublevel set关于函数凸性的扩展(Jesen Inequality)保持函数凸性的操作非负加权和 & 仿射函数的…...

深入 Rollup:从入门到精通(三)Rollup CLI命令行实战
准备阶段:初始化项目 初始化项目,这里使用的是pnpm,也可以使用yarn或者npm # npm npm init -y # yarn yarn init -y # pnpm pnpm init安装rollup # npm npm install rollup -D # yarn yarn add rollup -D # pnpm pnpm install rollup -D在…...

wangEditor富文本编辑器,Laravel上传图片配置和使用
文章目录 前言步骤1. 构造好前端模版2. 搭建后端存储3. 调试 前言 由于最近写项目需要使用富文本编辑器,使用的是VUE3.0版本所以很多不兼容,实际测试以后推荐使用wangEditor 步骤 构造好前端模版搭建后端存储调试 1. 构造好前端模版 安装模版 模版安…...

chrome源码剖析—进程通信
Chrome 浏览器采用多进程架构(multi-process architecture),这种架构使得每个浏览器标签、扩展、插件、GPU 渲染等都在独立的进程中运行。为了确保不同进程之间的高效通信,Chrome 使用 进程间通信(IPC, Inter-Process …...

JJJ:linux时间子系统相关术语
文章目录 墙上时间内核管理的各种时间无时钟滴答模式(tickless mode 或 no-tick mode)简要介绍具体实现动态时钟滴答 Dynamic Ticks完全无时钟滴答(Full Tickless) nohz sleep单触发模式 oneshot mode 墙上时间 真实世界的真实时…...

0 基础学运维:解锁 K8s 云计算运维工程师成长密码
前言:作为一个过来人,我曾站在技术的门槛之外,连电脑运行内存和内存空间都傻傻分不清,完完全全的零基础。但如今,我已成长为一名资深的k8s云计算运维工程师。回顾这段历程,我深知踏上这条技术之路的艰辛与不…...

大一计算机的自学总结:位运算的应用及位图
前言 不仅异或运算有很多骚操作,位运算本身也有很多骚操作。(尤其后几个题,太逆天了) 一、2 的幂 class Solution { public:bool isPowerOfTwo(int n) {return n>0&&n(n&-n);} }; 根据二进制表示数的原理&#…...

计算机毕业设计Django+Tensorflow音乐推荐系统 机器学习 深度学习 音乐可视化 音乐爬虫 知识图谱 混合神经网络推荐算法 大数据毕设
温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…...

AI 图片涌入百度图库
在这个信息爆炸的时代,我们习惯了通过搜索引擎来获取各种想要的信息和图片。然而,现在打开搜索引擎看到的却是许多真假难辨的信息——AI图片,这部分数据正以惊人的速度涌入百度图库,让小编不禁想问:未来打开百度图库不…...

可爱狗狗的404动画页面HTML源码
源码介绍 可爱狗狗的404动画页面HTML源码,源码由HTMLCSSJS组成,记事本打开源码文件可以进行内容文字之类的修改,双击html文件可以本地运行效果 效果预览 源码获取 可爱狗狗的404动画页面HTML源码...

【微服务与分布式实践】探索 Dubbo
核心组件 服务注册与发现原理 服务提供者启动时,会将其服务信息(如服务名、版本、所在节点的网络地址等)注册到注册中心。服务消费者则可以从注册中心发现可用的服务提供者列表,并与之通信。注册中心会存储服务的信息,…...

OpenCSG月度更新2025.1
1月的OpenCSG取得了一些亮眼的成绩 在2025年1月,OpenCSG在产品和社区方面继续取得了显著进展。产品方面,推出了AutoHub浏览器自动化助手,帮助用户提升浏览体验;CSGHub企业版功能全面升级,现已开放试用申请,…...

C++封装红黑树实现mymap和myset和模拟实现详解
文章目录 map和set的封装map和set的底层 map和set的模拟实现insertiterator实现的思路operatoroperator- -operator[ ] map和set的封装 介绍map和set的底层实现 map和set的底层 一份模版实例化出key的rb_tree和pair<k,v>的rb_tree rb_tree的Key和Value不是我们之前传统意…...

二次封装的方法
二次封装 我们开发中经常需要封装一些第三方组件,那么父组件应该怎么传值,怎么调用封装好的组件原有的属性、插槽、方法,一个个调用虽然可行,但十分麻烦,我们一起来看更简便的方法。 二次封装组件,属性怎…...

消息队列篇--通信协议篇--网络通信模型(OSI7层参考模型,TCP/IP分层模型)
一、OSI参考模型(Open Systems Interconnection Model) OSI参考模型是一个用于描述和标准化网络通信功能的七层框架。它由国际标准化组织(ISO)提出,旨在为不同的网络设备和协议提供一个通用的语言和结构,以…...

Python实现U盘数据自动拷贝
功能:当电脑上有U盘插入时,自动复制U盘内的所有内容 主要特点: 1、使用PyQt5创建图形界面,但默认隐藏 2、通过CtrlAltU组合键可以显示/隐藏界面 3、自动添加到Windows启动项 4、监控USB设备插入 5、按修改时间排序复制文件 6、静…...

汇编的使用总结
一、汇编的组成 1、汇编指令(指令集) 数据处理指令: 数据搬移指令 数据移位指令 位运算指令 算术运算指令 比较指令 跳转指令 内存读写指令 状态寄存器传送指令 异常产生指令等 2、伪指令 不是汇编指令,但是可以起到指令的作用,伪…...

DeepSeek理解概率的能力
问题: 下一个问题是概率问题。乘车时有一个人带刀子的概率是百分之一,两个人同时带刀子的概率是万分之一。有人认为如果他乘车时带上刀子,那么还有其他人带刀子的概率就是万分之一,他乘车就会安全得多。他的想法对吗?…...

AI 浪潮席卷中国年,开启科技新春新纪元
在这博主提前祝大家蛇年快乐呀!!! 随着人工智能(AI)技术的飞速发展,其影响力已经渗透到社会生活的方方面面。在中国传统节日 —— 春节期间,AI 技术也展现出了巨大的潜力,为中国年带…...

AI时代的网络安全:传统技术的落寞与新机遇
AI时代的网络安全:传统技术的落寞与新机遇 在AI技术飞速发展的浪潮中,网络安全领域正经历着前所未有的变革。一方面,传统网络安全技术在面对新型攻击手段时逐渐显露出局限性;另一方面,AI为网络安全带来了新的机遇&…...

可以称之为“yyds”的物联网开源框架有哪几个?
有了物联网的发展,我们的生活似乎也变得更加“鲜活”、有趣、便捷,包具有科技感的。在物联网(IoT)领域中,也有许多优秀的开源框架支持设备连接、数据处理、云服务等,成为被用户们广泛认可的存在。以下给大家…...

线程局部存储tls的原理和使用
一、背景 tls即Thread Local Storage,也就是线程局部存储,可在进程内,多线程按照各个线程分开进行存储。对于一些与线程上下文相关的变量,可放到tls中,减少多线程之间的数据同步的开销。 有人可能会问,我…...

RK3588平台开发系列讲解(ARM篇)ARM64底层中断处理
文章目录 一、异常级别二、异常分类2.1、同步异常2.2、异步异常三、中断向量表沉淀、分享、成长,让自己和他人都能有所收获!😄 一、异常级别 ARM64处理器确实定义了4个异常级别(Exception Levels, EL),分别是EL0到EL3。这些级别用于管理处理器的特权级别和权限,级别越高…...

CAN总线
1. 数据帧(Data Frame) 数据帧是 CAN 总线中最常用的帧类型,用于传输实际的数据。其结构如下: 起始位(Start of Frame, SOF):标志帧的开始。标识符(Identifier)&#x…...

qwen2.5-vl:阿里开源超强多模态大模型(包含使用方法、微调方法介绍)
1.简介 在 Qwen2-VL 发布后的五个月里,众多开发者基于该视觉语言模型开发了新的模型,并向 Qwen 团队提供了极具价值的反馈。在此期间,Qwen 团队始终致力于打造更具实用性的视觉语言模型。今天,Qwen 家族的最新成员——Qwen2.5-VL…...

python实现dbscan
python实现dbscan 原理 DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是一个比较有代表性的基于密度的聚类算法。它将簇定义为密度相连的点的最大集合,能够把具有足够高密度的区域划分为簇,并可在噪声的空间数据库中发现任意形…...

学习数据结构(3)顺序表
1.动态顺序表的实现 (1)初始化 (2)扩容 (3)头部插入 (4)尾部插入 (5)头部删除 (这里注意要保证有效数据个数不为0) (6&a…...