OpenFeign高级特性
# OpenFeign高级特性
# 日志配置
在默认的情况下,我们通过OpenFeign去调用远程的接口的时候,它是没有任何的日志输出的。但是在我们日常开发过程当中,有可能我希望调试一下,我希望得到这个feign,它调用的到底是哪一个服务,他的url到底是什么,他的一些请求头,一些响应的数据,我都希望看一下。
这个时候,我们就可以通过去修改它,默认的日志级别,就可以显示出来。OpenFeign它提供了4种日志级别,默认的日志级别是不显示。
# 配置方式
我们可以通过配置一个Bean的方式来修改它默认的配置。
它的配置方式有两种,一种就是全局配置,我们可以针对所有的服务者,服务提供方来进行配置。另外一种就是局部配置,我可以只针对某一个服务来进行配置。
要进行全局配置的话,我们需要配置一个配置类。
局部配置除了通过配置类的方式,我们还可以通过配置文件的方式来完成局部配置。
# 日志级别
以下是 OpenFeign 的四种日志级别:
- NONE(默认值):不记录任何信息。这是默认设置,适用于生产环境以减少不必要的日志输出。
- BASIC:记录请求方法、URL、响应状态代码以及执行时间。适合需要了解基本通信情况但不需要详细数据流的情况。
- HEADERS:除了 BASIC 级别包含的信息外,还会记录请求和响应的头信息。这对于调试涉及复杂头部操作的问题非常有用。
- FULL:最详细的日志级别,记录包括请求和响应头、正文以及元数据在内的所有信息。非常适合用于深入调试和分析 API 调用过程中的问题。
# 全局配置
实现全局配置,需要定义一个Bean,通过该Bean实现修改默认配置的目的。
@Configuration
public class FeignLogConfig {
@Bean
public Logger.Level feignLoggerLevel() {
// 设置为 FULL 日志级别
return Logger.Level.FULL;
}
}
2
3
4
5
6
7
8
9
到此,修改OpenFeign的日志级别的工作就完成了。
但你此时去访问,可能仍不会输出Feign的对应日志。因为我们这个Feign它的这个调试日志,它是以Debug,级别来输出的。我们都知道spring boot它有一个默认的日志级别,是info级别。
那么info级别它是要大于debug的,所以说debug的这个日志它是不会输出。
一个问题:为什么设置成
DEBUG就能输出?
- 日志框架过滤机制:日志框架有一个全局的日志级别设置,只有当某个日志消息的级别高于或等于全局设置的日志级别时,该消息才会被记录下来。如果全局的日志级别被设置为
INFO或更高(如WARN,ERROR),那么所有低于此级别的日志(包括DEBUG和TRACE)都不会被记录。- 包级别的日志级别控制:除了全局的日志级别外,你还可以针对特定的包或者类设置日志级别。例如,在Spring Boot应用中,如果你希望看到关于某个特定Feign客户端的详细日志,你需要确保该客户端所在包的日志级别至少为
DEBUG。否则,即使Feign客户端本身配置为Logger.Level.FULL,如果包的日志级别不够详细,这些日志也不会显示。
那补充一个提问:
Java中的日志级别是哪几个,高低是什么样的。
- 主流日志级别(从高到低):
| ERROR | 错误事件,影响系统功能正常运行(如空指针、数据库连接失败)。必须被关注和处理。 |
|---|---|
| WARN | 警告事件,可能有问题但不影响当前运行(如配置缺失、使用了已废弃的 API)。 |
| INFO | 一般性信息,用于记录程序关键流程(如服务启动、用户登录、任务完成)。 |
| DEBUG | 调试信息,用于开发和调试阶段(如方法入参、中间变量、SQL 语句)。 |
| TRACE | 最详细的追踪信息,比 DEBUG 更细(如循环内部、逐行执行跟踪),通常只在诊断复杂问题时开启。 |
优先级顺序:ERROR
>WARN>INFO>DEBUG>TRACE
此时,你需要的就是修改日志级别,比如:
logging:
level:
com.your.package: DEBUG # 替换为你的Feign客户端所在包的实际路径
# 或者 只设定一个 类
# level:
# com.sugar.order.feign.FeignService: debug
2
3
4
5
6
下面是一些常见的日志级别以及它们的含义:
- TRACE:输出更详细的跟踪信息,通常用于开发和调试阶段。
- DEBUG:输出调试信息,通常用于开发和调试阶段。
- INFO:输出重要的运行信息,通常是应用程序的运行状态和关键事件。
- WARN:输出警告信息,表示可能的问题但不是严重错误。
- ERROR:输出错误信息,表示发生了错误或异常。
# 局部配置
下面是通过,Bean的方式实现局部配置。
如果你只想对几个特定的Feign客户端启用或调整日志级别,而不影响其他Feign客户端,则不需要为这些配置添加@Configuration注解。相反,你可以直接将配置类传递给特定的Feign客户端,通过@FeignClient中的configuration属性来指定。这是因为在这种情况下,你并不是要让整个应用程序上下文都知晓这些配置,而只是想让特定的Feign客户端使用它们。
public class SpecificFeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
@FeignClient(name = "specific-service", configuration = SpecificFeignConfig.class)
public interface SpecificFeignClient {
@GetMapping("/api/resource")
String getResource();
}
2
3
4
5
6
7
8
9
10
11
12
13
局部配置还需要在声明式接口配置configuration。
- 配置文件的方式
feign:
client:
config:
order-service: # 应用于 order-service
loggerLevel: full # 设置Feign的日志级别为full
2
3
4
5
那如果同时用了全局,和局部,谁会生效呢?
优先级:在Spring中,如果某个特定组件有更具体的配置(如通过@FeignClient的configuration属性指定),那么这个具体配置会覆盖全局配置。这样可以在不影响其他服务的情况下对某些服务进行特殊处理。
# 集成负载均衡
在 Spring Cloud 2020.0.0 及以上版本中,OpenFeign 默认集成了 Spring Cloud LoadBalancer,因此你不需要额外引入 spring-cloud-starter-netflix-ribbon 依赖。但是,为了确保所有必要的依赖项都存在,你仍然需要引入 spring-cloud-starter-loadbalancer 依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
2
3
4
5
6
7
8
之后,按照前文的操作,
- 在主类或配置类(消费端)中启用 OpenFeign,即添加@EnableFeignClients注解
- 定义一个接口,并使用
@FeignClient注解标记该接口为 Feign 客户端 - 在
application.yml文件中配置服务发现组件 - 在服务中注入并使用 Feign 客户端
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class YourService {
@Autowired
private UserServiceClient userServiceClient;
public User getUserById(Long id) {
return userServiceClient.getUser(id);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 超时控制
在Spring Cloud微服务架构中,大部分公司都是利用OpenFeign进行服务间的调用,而比较简单的业务使用默认配置是不会有多大问题的,但是如果是业务比较复杂,服务要进行比较繁杂的业务计算,那后台很有可能会出现Read Timeout这个异常,因此定制化配置超时时间就有必要了。
OpenFeign默认等待60秒钟,超过后报错
默认OpenFeign客户端等待60秒钟,但是服务端处理超过规定时间会导致Feign客户端返回报错。
为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制,默认60秒太长或者业务时间太短都不好
yml文件中开启配置:
connectTimeout # 连接超时时间
readTimeout # 请求处理超时时间
2
官网对应介绍-默认属性 (opens new window)
- https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-overriding-defaults
以下是2个时间的区别:
connectTimeout控制的是从客户端发起连接请求到成功建立TCP连接这段时间的最大等待值。它主要用于解决网络不可达的问题。readTimeout控制的是从成功建立连接后到接收到完整响应这段时间的最大等待值。它主要用于解决服务器响应缓慢的问题。
# 配置
spring:
cloud:
openfeign:
client:
config:
default:
#连接超时时间
connectTimeout: 3000
#读取超时时间
readTimeout: 3000
2
3
4
5
6
7
8
9
10
使用default表示全局配置,任何一处都是如此。当然也可以指定配置。
spring:
cloud:
openfeign:
client:
config:
default: # 表示全局配置
#连接超时时间
connectTimeout: 3000
#读取超时时间
readTimeout: 3000
serviceC:
#连接超时时间
connectTimeout: 2000
#读取超时时间
readTimeout: 2000
2
3
4
5
6
7
8
9
10
11
12
13
14
15
加上服务名后就表示只有该服务是如此。当然,全局配置和单一配置可以共存。此时,单一配置会覆盖全局配置,当然其他服务仍是default的值。
# 配置类配置
除了通过配置文件,你还可以通过创建一个自定义的配置类,并将其应用到特定的Feign客户端上来设置超时时间。这通常涉及到Feign.Builder的定制化。
首先,添加必要的依赖,确保你的项目包含对spring-cloud-starter-openfeign的支持。
配置类:
import feign.Request;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
public Request.Options options() {
// 设置连接超时时间为5秒,读取超时时间为5秒
return new Request.Options(5000, 5000);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
OpenFeign客户端类:
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "myFeignClient", configuration = FeignConfig.class)
public interface MyFeignClient {
@GetMapping("/api/resource")
String getResource();
}
2
3
4
5
6
7
8
9
# 重试机制
OpenFeign的重试,默认重试是关闭的,给了默认值。
开启Retryer功能
新增配置类FeignConfig并修改Retryer配置
package com.atguigu.cloud.config;
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @auther zzyy
* @create 2023-11-10 11:09
*/
@Configuration
public class FeignConfig
{
@Bean
public Retryer myRetryer()
{
//return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的
//最大请求次数为3(1+2),初始间隔时间为100ms,重试间最大间隔时间为1s
return new Retryer.Default(100,1,3);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 修改默认HttpClient
OpenFeign中http client 如果不做特殊配置,OpenFeign默认使用JDK自带的HttpURLConnection发送HTTP请求,
由于默认HttpURLConnection没有连接池、性能和效率比较低,如果采用默认,性能上不是最牛B的,所以加到最大。
修改对应的pom
<!-- httpclient5-->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
<!-- feign-hc5-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<version>13.1</version>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
Apache HttpClient5 配置开启说明
# Apache HttpClient5 配置开启
spring:
cloud:
openfeign:
httpclient:
hc5:
enabled: true
2
3
4
5
6
7
# 请求/响应压缩
对请求和响应进行GZIP压缩
Spring Cloud OpenFeign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。
通过下面的两个参数设置,就能开启请求与相应的压缩功能:
spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.response.enabled=true
细粒度化设置
对请求压缩做一些更细致的设置,比如下面的配置内容指定压缩的请求数据类型并设置了请求压缩的大小下限,
只有超过这个大小的请求才会进行压缩:
spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json #触发压缩数据类型
spring.cloud.openfeign.compression.request.min-request-size=2048 #最小触发压缩的大小
server:
port: 80
spring:
application:
name: cloud-consumer-openfeign-order
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
openfeign:
client:
config:
default:
#cloud-payment-service:
#连接超时时间
connectTimeout: 4000
#读取超时时间
readTimeout: 4000
httpclient:
hc5:
enabled: true
compression:
request:
enabled: true
min-request-size: 2048 #最小触发压缩的大小
mime-types: text/xml,application/xml,application/json #触发压缩数据类型
response:
enabled: true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 契约配置
什么叫契约配置呢?就是将我们之前OpenFeign接口当中的这些springMvc的注解,给它还原成Feign的原生的注解。
为什么要实现这个功能呢?为了向下兼容。
比方说我们以前spring cloud一的早期版本,它其实就是用的原生的Feign。那么随着next netflix停更,才替换成了OpenFeign。
假如说我现在有个项目,它就是用的spring clode早期的一个版本。我现在想做版本的一个升级,我想升级比较新的一个版本,我想用OpenFeign。但老项目只支持原生注解,此时——契约配置就可以登场了。
# 自定义拦截器
它也针对拦截器所做的一些扩展。
注意:这里的拦截指的是 服务端中——消费端调用提供端的这个过程。
spring MVC的拦截器它是在我们的客户端发送请求到服务端,起的作用,这是spring VC的拦截器。
那么OpenFeign的拦截器,它是当我们的这个消费端去调用我们的服务提供端的时候起的作用。
那它的作用有哪些呢?比如:在服务端去调用提供端的时候,每一次都去记录日志。我觉得你的日志我不满意,我自己去记录一些我自己的日志。还比方说我们服务端去调提供端的时候,我每一次请求,我都给你去带一些个参数。或者说我在请求头当中,我可以给你去设置一些参数。我们就可以做一些认证授权对不对。
# 实现自定义类
你需要创建一个类实现RequestInterceptor接口,并重写其apply方法。
我们也是通过这个方法来实现,身份信息的传递。为什么直接写到接口的实现中不行呢?
在这个方法中,你可以访问并修改即将发出的请求。
import feign.RequestInterceptor;
import feign.RequestTemplate;
public class CustomFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 在这里可以对请求进行修改
// 比如添加请求头
template.header("Custom-Header", "CustomValue");
// 或者根据需要添加其他逻辑
String method = template.method();
if ("GET".equalsIgnoreCase(method)) {
template.query("api-version", "1.0");
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
接下来,你需要将自定义的拦截器注册到Spring上下文中。这可以通过创建一个配置类并在其中定义拦截器作为bean来完成。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor customFeignInterceptor() {
return new CustomFeignInterceptor();
}
}
2
3
4
5
6
7
8
9
10
11
确保你的Feign客户端使用了这个配置类。你可以在声明Feign客户端时指定这个配置类:
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "example-client", url = "http://example.com", configuration = FeignConfig.class)
public interface ExampleClient {
@GetMapping("/api/resource")
String getResource();
}
2
3
4
5
6
7
8
9
上述的声明式客户端类,多了一个属性url。它起什么作用呢?
在@FeignClient注解中指定的url属性用于直接提供服务的基本URL。这个设置通常用于以下几种场景:
- 外部服务调用:当你需要调用不在Spring Cloud服务发现机制中的外部HTTP服务时,可以直接通过
url属性指定目标服务的完整地址。例如,如果你想要调用一个公开的REST API,而这个API并不由你的微服务架构管理或者不支持服务发现,这时就可以使用url属性。 - 开发和测试阶段:在开发或测试环境中,可能还没有配置好服务发现(如Eureka, Consul等),或者你希望快速地对某个服务进行本地调试,这时可以通过
url属性直接指向该服务的运行实例。 - 固定服务地址:对于某些特定的服务,如果其地址是固定的,并且不需要动态发现或负载均衡,也可以直接通过
url属性来指定。
# 认证功能
很多人知道 Feign 可以“像调用本地方法一样调用远程服务”。
但在真实微服务场景下——比如 OAuth2、JWT、Session、API Key 等体系中,如何让调用方带上“用户身份”或“服务凭证” 就变得非常关键。它该如何实现呢?有哪些注意事项呢?
在微服务架构中:
- 前端调用 网关(Gateway);
- 网关经过认证(OAuth2 / JWT / SSO),拿到用户凭证;
- 网关将请求转发到下游服务;
- 下游服务可能还会再调用别的服务(通过 Feign)。
这时的问题是:
Feign 调用是“后端服务之间”的调用, 怎么把最初的认证信息(如 Token)继续传下去?
# Feign 身份传递的主流实现方式
| 方案 | 核心思路 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| ① 请求头透传(Header 传递) | 从当前请求取出 Token,放进 Feign 请求头中 | JWT、OAuth2 | 简单直接 | 需要手动配置拦截器 |
| ② 服务间凭证(Service Token) | 每个服务有独立凭证,用于服务级认证 | 内部系统通信 | 安全性高 | 不能区分具体用户 |
| ③ 使用 Spring Security OAuth2 Client | Feign 集成 OAuth2 客户端自动携带 Token | OAuth2 授权场景 | 标准规范 | 配置复杂 |
| ④ Session 共享 / Redis 存储 Token | 所有服务从共享存储读取认证上下文 | 老系统或单点登录 | 无需改请求头 | 不适合无状态微服务 |
| ⑤ 网关签名 / HMAC 校验 | 网关为请求签名,服务间校验签名 | 高安全要求系统 | 防篡改 | 成本高 |
# Header 传递
其中,最常见也最推荐的方式是,放在Header中,这是 95% 项目中采用的方式。
原理简单清晰:
在发出 Feign 请求前,从当前 HTTP 请求中取出认证头(Authorization / Cookie / custom header),然后添加到 Feign 请求中。
实现步骤
- 创建一个 Feign 拦截器(RequestInterceptor)
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
@Component
public class FeignAuthInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 获取当前请求上下文
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
// 获取 Authorization 头
String token = request.getHeader("Authorization");
if (token != null) {
// 将 Token 透传给下游服务
template.header("Authorization", token);
}
// 也可以透传其他自定义头(如 X-User-Id、X-Request-Id)
String userId = request.getHeader("X-User-Id");
if (userId != null) {
template.header("X-User-Id", userId);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 下游服务验证身份
在下游服务(比如 UserService)中,仍然由 Spring Security 或自定义过滤器验证 Token:
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
String token = request.getHeader("Authorization");
if (token != null && JwtUtils.verify(token)) {
// 验证通过,设置认证上下文
}
filterChain.doFilter(request, response);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
几个你可能会问的问题:
- 为什么下游验证身份时用
OncePerRequestFilter,而不是像 Feign 一样实现RequestInterceptor? OncePerRequestFilter是 OpenFeign 的接口吗?
答案结论:
OncePerRequestFilter不是 Feign 的接口, 它属于 Spring Web / Spring Security, 用于 处理进入当前服务的 HTTP 请求。而
RequestInterceptor是 Feign 的接口, 用于 处理发出的 HTTP 请求。
也就是说,两者职责方向相反:
| 拦截点 | 所属框架 | 拦截的请求方向 | 用途 |
|---|---|---|---|
RequestInterceptor | OpenFeign | 出站请求(Outbound) | 给发出的 Feign 请求加 Header(例如 Token) |
OncePerRequestFilter | Spring Web / Spring Security | 入站请求(Inbound) | 处理收到的 HTTP 请求,验证 Token、鉴权等 |
继续追问:
那获取OpenFeign的header信息,只有继承OncePerRequestFilter类的这个方法吗?
不一定。
OncePerRequestFilter 只是其中一种(最常用、最底层、最通用)方式。实际上,你可以在多个层次拿到 Header 信息,只是适用场景不同。
比如:
# 传递的Header信息在哪
Feign 调用带来的 Header 到底在哪儿?
当 A 服务 用 Feign 调用 B 服务时,B 服务接收到的是一个标准的 HTTP 请求:
也就是说,所有 Feign 传过去的 Header 最终都存在于:
HttpServletRequest.getHeader("xxx")
下游服务中获取 Header 的几种方式
| 层次 | 实现方式 | 获取方式 | 适用场景 |
|---|---|---|---|
| ① Filter 层(最底层) | 继承 OncePerRequestFilter | request.getHeader("Authorization") | 安全框架、全局鉴权、日志追踪 |
| ② Controller 层 | 通过注解 | @RequestHeader("Authorization") String token | 单个接口需要拿 Token、TraceId |
| ③ AOP 层 | 切面中注入 HttpServletRequest | @Autowired HttpServletRequest | 日志、审计、灰度路由 |
| ④ 拦截器层(HandlerInterceptor) | 实现 HandlerInterceptor | request.getHeader("X-Service-Auth") | Spring MVC 层面通用逻辑 |
| ⑤ 参数解析器 | 自定义 HandlerMethodArgumentResolver | 自动注入到参数对象 | 高度封装(比如注入 LoginUser 对象) |
比如,直接在controller中获取OpenFeign加到header中的信息:
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/check")
public String checkOrder(
@RequestHeader(value = "X-Service-Auth", required = false) String serviceAuth,
@RequestHeader(value = "Authorization", required = false) String authorization
) {
return String.format("Received headers: X-Service-Auth=%s, Authorization=%s",
serviceAuth, authorization);
}
}
// 其他的方式
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/check")
public String checkOrder(HttpServletRequest request) {
String serviceAuth = request.getHeader("X-Service-Auth");
String authorization = request.getHeader("Authorization");
String traceId = request.getHeader("traceId"); // 如果有链路追踪头
return String.format(
"X-Service-Auth=%s, Authorization=%s, traceId=%s",
serviceAuth, authorization, traceId
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
说明:
required = false可以避免请求头不存在时报错;- 如果 Feign 成功传递 Header,这里会直接接收到;
- 可以直接用作业务逻辑(比如验证服务来源、用户身份等)。
# 进阶方案:服务间凭证 + 用户身份双层认证
在较大规模系统中,为了避免伪造调用(比如有人伪造服务请求),
会同时带上两层身份:
| Header | 内容 | 用途 |
|---|---|---|
Authorization | 用户 Token(JWT / OAuth2) | 表示用户是谁 |
X-Service-Auth | 服务 Token(比如用 client_id / secret 生成) | 表示服务是谁 |
这样下游服务可以验证两个维度:
- 用户是否合法;
- 调用方服务是否被授权访问该接口。
你可能会问的一些问题:
- 在进阶方案中,
Authorization和X-Service-Auth都是加到 Header 中吗? - 它们是怎么加的?也是通过
RequestInterceptor吗? - 那服务者信息是怎么写进去的?
答案结论:
是的,两个都在 HTTP Header 中传递, 都是通过 Feign 的 RequestInterceptor 加进去的。
区别只是它们的来源不同:
Authorization(用户身份)来自“当前请求上下文”;X-Service-Auth(服务身份)来自“本服务自己的配置或签名逻辑”。
# 一个例子
@Component
public class FeignAuthInterceptor implements RequestInterceptor {
@Value("${service.auth.token}") // 每个服务自己的固定凭证
private String serviceToken;
@Override
public void apply(RequestTemplate template) {
// 1. 透传用户身份(从当前 HTTP 请求中取出)
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
String userToken = attributes.getRequest().getHeader("Authorization");
if (userToken != null) {
template.header("Authorization", userToken);
}
}
// 2. 添加服务身份凭证(从配置或签名生成)
if (serviceToken != null) {
template.header("X-Service-Auth", serviceToken);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
在下游服务(比如 UserService)中,用 OncePerRequestFilter 验证两层身份:
@Component
public class DualAuthFilter extends OncePerRequestFilter {
@Value("${service.auth.token}")
private String expectedServiceToken;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 验证服务层凭证
String serviceToken = request.getHeader("X-Service-Auth");
if (!expectedServiceToken.equals(serviceToken)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
// 验证用户层 Token
String userToken = request.getHeader("Authorization");
if (userToken != null && JwtUtils.verify(userToken)) {
// 设置认证上下文(Spring Security)
}
filterChain.doFilter(request, response);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29