微服务网关鉴权之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…...
STM32 TIM输入捕获 测量频率
输入捕获简介: IC(Input Capture)输入捕获 输入捕获模式下,当通道输入引脚出现指定电平跳变时,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数 每个高级定时器…...
python | OpenCV小记(一):cv2.imread(f) 读取图像操作(待更新)
python | OpenCV小记(一):cv2.imread(f)读取图像操作 1. 为什么 [:, :, 0] 提取的是第一个通道(B 通道)?OpenCV 的通道存储格式索引操作 [:, :, 0] 的解释常见误解 1. 为什么 [:, :,…...
解析静态链接
文章目录 静态链接空间与地址分配相似段合并虚拟地址分配符号地址确定符号解析与重定位链接器优化重复代码消除函数链接级别静态库静态链接优缺点静态链接 一组目标文件经过链接器链接后形成的文件即可执行文件,如果没有动态库的加入,那么这个可执行文件被加载后无需再进行重…...
Ollama 运行从 ModelScope 下载的 GGUF 格式的模型
本文系统环境 Windows 10 Ollama 0.5.7 Ollama 是什么? Ollama 可以让你快速集成和部署本地 AI 模型。它支持各种不同的 AI 模型,并允许用户通过简单的 API 进行调用 Ollama 的安装 Ollama 官网 有其下载及安装方法,非常简便 但如果希…...
【2025年最新版】Java JDK安装、环境配置教程 (图文非常详细)
文章目录 【2025年最新版】Java JDK安装、环境配置教程 (图文非常详细)1. JDK介绍2. 下载 JDK3. 安装 JDK4. 配置环境变量5. 验证安装6. 创建并测试简单的 Java 程序6.1 创建 Java 程序:6.2 编译和运行程序:6.3 在显示或更改文件的…...
探索性测试与自动化测试的结合
随着软件开发周期的不断缩短和质量要求的不断提高,测试行业正在经历一场深刻的变革。自动化测试因其高效性和可重复性成为测试团队必不可少的工具,而探索性测试(Exploratory Testing, ET)则因其灵活性和创意性在面对复杂、动态变化…...
我是如何写作的?
以前是如何写作的 从小学三年级开始学写作文,看的作文书,老师布置作文题目,内容我都是自己写的。那时会积累一些好词,听到什么好词就记住了。并没有去观察什么,也没有好好花心思在写作上。总觉得我写的作文与真正好的…...
智慧园区管理系统为企业提供高效运作与风险控制的智能化解决方案
内容概要 快鲸智慧园区管理系统,作为一款备受欢迎的智能化管理解决方案,致力于为企业提供高效的运作效率与风险控制优化。具体来说,这套系统非常适用于工业园、产业园、物流园、写字楼及公寓等多种园区和商办场所。它通过数字化与智能化的手…...
INCOSE需求编写指南-附录 B: 首字母缩略词和缩写
附录 Appendix B: 首字母缩略词和缩写ACRONYMS AND ABBREVIATIONS AD 难易程度的进阶 Advancement Degree of Difficulty AI 人工智能 Artificial Intelligence CM 配置管理 Configuration Management ConOps 运作理念 Concept of Operations COTS 商业现货 Comme…...
VS2008 - debug版 - 由于应用程序配置不正确,应用程序未能启动。重新安装应用程序可能会纠正这个问题。
文章目录 VS2008 - debug版 - 由于应用程序配置不正确,应用程序未能启动。重新安装应用程序可能会纠正这个问题。概述笔记VS2008安装环境VS2008测试程序设置默认报错的情况措施1措施2备注 - exe清单文件的问题是否使用静态库?_BIND_TO_CURRENT_VCLIBS_VERSION的出处…...
Docker容器数据恢复
Docker容器数据恢复 1 创建mongo数据库时未挂载数据到宿主机2 查找数据卷位置3 将容器在宿主机上的数据复制到指定目录下4 修改docker-compose并挂载数据(注意端口)5 重新运行新容器 以mongodb8.0.3为例。 1 创建mongo数据库时未挂载数据到宿主机 versi…...
翼星求生服务器搭建【Icarus Dedicated Server For Linux】
一、前言 本次搭建的服务器为Steam平台一款名为Icarus的沙盒、生存、建造游戏,由于官方只提供了Windows版本服务器导致很多热爱Linux的小伙伴无法释怀,众所周知Linux才是专业服务器的唯一准则。虽然Github上已经有大佬制作了容器版本但是容终究不够完美,毕竟容器无法与原生L…...
如何在data.table中处理缺失值
📊💻【R语言进阶】轻松搞定缺失值,让数据清洗更高效! 👋 大家好呀!今天我要和大家分享一个超实用的R语言技巧——如何在data.table中处理缺失值,并且提供了一个自定义函数calculate_missing_va…...
react中如何获取dom元素
实现代码 const inputRef useRef(null) inputRef.current.focus()...
引入@Inject的依赖包
maven引入Inject的依赖包 在 Maven 项目中引入 Inject 注解所需的依赖包同样取决于你打算使用的依赖注入框架。以下是一些常见框架及其 Maven 依赖配置的示例: 1. Google Guice 如果你打算使用 Google Guice,你需要在 pom.xml 文件中添加 Guice 的依赖…...
Deep Seek R1本地化部署
目录 说明 一、下载ollama 二、在ollama官网下载模型 三、使用 后记 说明 操作系统:win10 使用工具:ollama 一、下载ollama 从官网下载ollama: ollama默认安装在C盘,具体位置为C:\Users\用户名\AppData\Local\Programs\O…...
RDMA 工作原理 | 支持 RDMA 的网络协议
注:本文为 “RDMA” 相关文章合辑。 英文引文机翻未校。 图片清晰度受引文所限。 Introduction to Remote Direct Memory Access (RDMA) Written by: Dotan Barak on March 31, 2014.on February 13, 2015. What is RDMA? 什么是 RDMA? Direct me…...
再见了流氓软件~~
聊一聊 最近一直在测试软件,需要装各种软件和工具配合测试,导致现在电脑都快装满了,需要把不用的软件卸载。电脑自带的卸载只能一个一个卸载,不但麻烦还卸载不干净。 相信很多人也有这方面的需要,电脑装了很多软件&a…...
165. 比较版本号
两个注意的点: 分割字符串的时候,要用split("\\.")而不能用split("."),因为前者表示“对.使用斜杠转义,\\表示一个斜杠”,而后者表示匹配任意单个字符,例如version2 "1.2.3&quo…...
一文大白话讲清楚webpack进阶——9——ModuleFederation实战
文章目录 一文大白话讲清楚webpack进阶——9——ModuleFederation实战1. 啥是ModuleFederation2. 创建容器应用3. 创建远程应用4. 启动远程应用5. 使用远程应用的组件 一文大白话讲清楚webpack进阶——9——ModuleFederation实战 1. 啥是ModuleFederation 先看这篇文章&#…...
【llm对话系统】LLM 大模型Prompt 怎么写?
如果说 LLM 是一个强大的工具,那么 Prompt 就是使用这个工具的“说明书”。一份好的 Prompt 可以引导 LLM 生成更准确、更相关、更符合你期望的输出。 今天,我们就来聊聊 LLM Prompt 的编写技巧,掌握这把解锁 LLM 潜能的钥匙! 一…...
INCOSE需求编写指南-附录 C: 需求模式
附录 Appendix C: 需求模式 Requirement Patterns C.1 需求模式简介 Introduction to Requirement Patterns 需求模式(样板或模板)的概念最初于 1998 年在英国的未来水面战斗人员 (FSC) 国防项目中应用(Dick 和 Llorens,2012 年…...
WGCLOUD使用介绍 - 如何监控ActiveMQ和RabbitMQ
根据WGCLOUD官网的信息,目前没有针对ActiveMQ和RabbitMQ这两个组件专门做适配 不过可以使用WGCLOUD已经具备的通用监测模块:进程监测、端口监测或者日志监测、接口监测 来对这两个组件进行监控...
【VASP】AIMD计算总结
【VASP】AIMD计算总结 vasp 计算文件INCAR 参数介绍后处理 二维材料与异质结的构造除了筛选优势还应该判断是否稳定,所以我在这分享一篇基于vasp6.2计算的AIMD 示例: https://www.vasp.at/wiki/index.php/Liquid_Si_-_Standard_MD vasp 计算文件 POSCA…...
春节旅游高峰,人力资源如何巧妙应对?
春节旅游高峰,人力资源如何巧妙应对? 春节等假期一到,各大旅游景区便人潮汹涌,游客如织。面对这种旅游高峰,工作人员往往要连续超负荷运转,身心俱疲。特别是在那些热门景区和网红打卡地,人…...
zsh安装插件
0 zsh不仅在外观上比较美观,而且其具有强大的插件,如果不使用那就亏大了。 官方插件库 https://github.com/ohmyzsh/ohmyzsh/wiki/Plugins 官方插件库并不一定有所有的插件,比如zsh-autosuggestions插件就不再列表里,下面演示zs…...
continuous batching、chunked-prefill相关概念
batching VS. continuous batching batching是所有requests的output都生成完毕之后,才能开始处理下一个batch。一般要做input padding,要等待凑够batch才运行(也有超时bar)。 continuous batching是每完成1个request,就…...
python算法和数据结构刷题[2]:链表、队列、栈
链表 链表的节点定义: class Node():def __init__(self,item,nextNone):self.itemitemself.nextNone 删除节点: 删除节点前的节点的next指针指向删除节点的后一个节点 添加节点: 单链表 class Node():"""单链表的结点&quo…...
认知神经科学0-----关于心智的生物学(2011年第三版)
译者序 人类的科学事业所面临的挑战之一-就是认识意识与物质或心灵(智慧)与大脑的关系。从古希腊哲学先贤或更早的时代开始,人类对这一-古 老问题就有了大量的探讨或臆测;但仅仅是在近代和现代,人们才真正在科学的意义上探索心智与大脑的关系。脑…...
想品客老师的第九天:原型和继承
原型与继承前置看这里 原型 原型都了解了,但是不是所有对象都有对象原型 let obj1 {}console.log(obj1)let obj2 Object.create(null, {name: {value: 荷叶饭}})console.log(obj2) obj2为什么没有对象原型?obj2是完全的数据字典对象,没有…...
指针(C语言)从0到1掌握指针,为后续学习c++打下基础
目录 一,指针 二,内存地址和指针 1,什么是内存地址 2,指针在不同系统下所占内存 三,指针的声明和初始化以及类型 1,指针的声明 2,指针 的初始化 1, 初始化方式优点及适用场景 4,指针的声明初始化类型…...
php接口连接数据库
框架:https://www.thinkphp.cn/doc 创建网站 域名自己写 创建文件夹,“test”拉取框架,地址栏输入 composer create-project topthink/think5.1.* tp5 会自动创建一个tp5文件夹 根目录选择刚刚创建拉框架的文件夹 以test为示例 “D:\test\…...
Qt中json的使用
目录 一、json相关类和接口 1.QJsonDocument 2.QJsonObject 3.QJsonArray 4.QJsonValue 二、json写文件 1.写文件基本流程 2.代码示例 三、json读文件 1.读文件基本流程 2.代码示例 json是一种轻量级的数据交换格式,在Qt中使用json数据可以通过Qt提供的Q…...
OpenAI-Edge-TTS:本地化 OpenAI 兼容的文本转语音 API,免费高效!
文本转语音(TTS)技术已经成为人工智能领域的重要一环,无论是语音助手、教育内容生成,还是音频文章创作,TTS 工具都能显著提高效率。今天要为大家介绍的是 OpenAI-Edge-TTS,一款基于 Microsoft Edge 在线文本…...
物业系统改革引领行业智能化管理与提升服务质量的新征程
内容概要 在当今迅速变化的社会中,物业系统改革正在悄然推动行业的智能化管理进程。物业管理作为一个古老而传统的领域,面临着诸多挑战,包括效率低下、业主需求难以满足等。数字化转型为这一现象注入了新活力,帮助物业公司通过先…...
【LLM】Deepseek本地部署学习
文章目录 1. 访问ollama官网安装平台2. 选择配置3. 下载和运行 1. 访问ollama官网安装平台 https://ollama.com/ 2. 选择配置 参考以下配置要求 3. 下载和运行 ollama run deepseek-r1:7b...
Vscode编辑器下 Markdown无法显示图片
1.问题 在vscode 编辑器中无法预览 markdon 文件中的图片 2.解决方案 大部分出现这种情况是因为新版本的vscode会阻拦有风险的资源显示,将安全等级调低即可。 方式一: 1.打开任意 MD 文件,ctrl,调出设置 2. 输入 markdown.ch…...
Java实现.env文件读取敏感数据
文章目录 1.common-env-starter模块1.目录结构2.DotenvEnvironmentPostProcessor.java 在${xxx}解析之前执行,提前读取配置3.EnvProperties.java 这里的path只是为了代码提示4.EnvAutoConfiguration.java Env模块自动配置类5.spring.factories 自动配置和注册Enviro…...
高效学习方法分享
高效学习方法分享 引言 在信息高速发展的今天,学习已经成为每个人不可或缺的一部分。你是否曾感到学习的疲惫,信息的爆炸让你无从下手?今天,我们将探讨几种高效的学习方法,帮助你从中找到适合自己的学习之道。关于学…...
分库分表 相关问题
问题:分库后,就有多个数据源需要,dbproxy 对机器做代理,一般需要lvs/f5 等手段来实现流量的负载均衡,跨机房可能需要dns分发,例如 mycat 阿里的主键。 就这个问题通过一问一答的方式解答 什么是 dbproxy&…...
【Linux系统】进程间通信:实现命名管道通信
认识命名管道通信 命名管道通信的结构图示: 图中的 Server 和 Client 是不同的进程, Server 负责发送数据, Client 则是接收数据,进程之间通过命名管道进行数据通信 准备工作: 创建以下文件 Server.hpp #服务器类的…...
IT服务管理平台(ITSM):构建高效运维体系的基石
IT服务管理平台(ITSM):构建高效运维体系的基石 在数字化转型浪潮的推动下,企业对IT服务的依赖日益加深,如何高效管理和优化IT服务成为企业面临的重要课题。IT服务管理平台(ITSM)应运而生,以其系统化的管理方法和工具,助力企业实现IT服务的规范化、高效化和智能化。本…...
SSM开发(八) MyBatis解决方法重载
目录 一、Mybatis能否支持方法重载? 二、解决 MyBatis 方法重载问题的几种方法 解决方法一: (注解方式) 将重载方法命名为不同的方法名 解决方法二:采用@SelectProvider注解 解决方法三:使用 MyBatis 的 标签和动态 SQL 来构建不同参数的 SQL 查询 三、总结 一、Myb…...
AIGC时代的Vue或React前端开发
在AIGC(人工智能生成内容)时代,Vue开发正经历着深刻的变革。以下是对AIGC时代Vue开发的详细分析: 一、AIGC技术对Vue开发的影响 代码生成与自动化 AIGC技术使得开发者能够借助智能工具快速生成和优化Vue代码。例如,通…...
【实践案例】使用Dify构建文章生成工作流【在线搜索+封面图片生成+内容标题生成】
文章目录 概述开始节点图片封面生成关键词实时搜索主题参考生成文章详情和生成文章标题测试完整工作流运行测试结果 概述 使用Dify构建文章生成工作流,使用工具包括:使用 Tavily 执行的搜索查询,使用Flux生成封面图片,使用Stable…...
使用 Context API 管理临时状态,避免 Redux/Zustand 的持久化陷阱
在开发 React Native 应用时,我们经常需要管理全局状态,比如用户信息、主题设置、网络状态等。而对于某些临时状态,例如 数据同步进行中的状态 (isSyncing),我们应该选择什么方式来管理它? 在项目开发过程中ÿ…...
【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】1.26 统计圣殿:从描述统计到推断检验
1.26 统计圣殿:从描述统计到推断检验 目录 #mermaid-svg-3nz11PRr47fVfGWZ {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3nz11PRr47fVfGWZ .error-icon{fill:#552222;}#mermaid-svg-3nz11PRr47fVfGWZ…...
C# 添加、替换、提取、或删除Excel中的图片
在Excel中插入与数据相关的图片,能将关键数据或信息以更直观的方式呈现出来,使文档更加美观。此外,对于已有图片,你有事可能需要更新图片以确保信息的准确性,或者将Excel 中的图片单独保存,用于资料归档、备…...
商密测评题库详解:商用密码应用安全性评估从业人员考核题库详细解析(9)
1. 申请商用密码测评机构需提交材料考点 根据《商用密码应用安全性测评机构管理办法(试行)》,申请成为商用密码应用安全性测评机构的单位应当提交的材料不包括( )。 A. 从事与普通密码相关工作情况的说明 B. 开展测评工作所需的软硬件及其他服务保障设施配备情况 C. 管…...