实现微服务间调用传递用户信息

目录

1、用户登录流程

2、网关转发请求到微服务传递用户信息

3、微服务之间传递用户信息


1、用户登录流程

网关:网关就是络的口。数据在网络间传输,从一个网络传输到另一网络时就需要经过网关来做数据的路由和转发以及数据安全的校验。也就是一切请求都需要先经过网关

用户登录流程:用户请求用户微服务进行登录,然后自定义网关过滤器判断该请求是否需要做登录校验(一般不用,用户登录请求不拦截),然后用户提交登陆数据表单,用户微服务进行用户登录数据校验,

如果账号密码正确,用户微服务则生成一个临时token返回,前端拿到响应把token设置在session中

网关部分过滤不需要请求代码

用户登录校验代码如下:

private final PasswordEncoder passwordEncoder;

    private final JwtTool jwtTool;

    private final JwtProperties jwtProperties;

    @Override
    public UserLoginVO login(LoginFormDTO loginDTO) {
        // 1.数据校验
        String username = loginDTO.getUsername();
        String password = loginDTO.getPassword();
        // 2.根据用户名或手机号查询
        User user = lambdaQuery().eq(User::getUsername, username).one();
        Assert.notNull(user, "用户名错误");
        // 3.校验是否禁用
        if (user.getStatus() == UserStatus.FROZEN) {
            throw new ForbiddenException("用户被冻结");
        }
        // 4.校验密码
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BadRequestException("用户名或密码错误");
        }
        // 5.生成TOKEN
        String token = jwtTool.createToken(user.getId(), jwtProperties.getTokenTTL());
        // 6.封装VO返回
        UserLoginVO vo = new UserLoginVO();
        vo.setUserId(user.getId());
        vo.setUsername(user.getUsername());
        vo.setBalance(user.getBalance());
        vo.setToken(token);
        return vo;
    }

前端代码如下:

2、网关转发请求到微服务传递用户信息

如图所示:

  1. 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。

  2. WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为Filter)。

  3. 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为prepost两部分,分别会在请求路由到微服务之前之后被执行。

  4. 只有所有Filterpre逻辑都依次顺序执行通过后,请求才会被路由到微服务。

  5. 微服务返回结果后,再倒序执行Filterpost逻辑。

  6. 最终把响应结果返回。

如图中所示,最终请求转发是有一个名为NettyRoutingFilter的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter之前,这就符合我们的需求了!

可以看到NetttyRoutingFilter的顺序是int的最大值,因此只需要将自定义网关的顺序大小比NettyRoutingFilter小即可

实现Ordered接口,并重写getOrder方法,制定顺序

@RequiredArgsConstructor
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

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

因此,接下来我们要做的事情有:

  • 改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务

  • 编写微服务拦截器,拦截请求获取用户信息,保存到ThreadLocal后放行

完整的自定义网关代码:

import com.hmall.common.exception.UnauthorizedException;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.utils.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

@RequiredArgsConstructor
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {



    private final AuthProperties authProperties;

    private final JwtTool jwtTool;

    private final AntPathMatcher antPathMatcher=new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1、获取request
        ServerHttpRequest request = exchange.getRequest();
        // 2、判断是否需要做登录拦截
        if(isExclude(request.getPath().toString())){
            //放行
            return chain.filter(exchange);
        }
        // 3、获取token
        String token=null;
        HttpHeaders headers = request.getHeaders();
        // JWT令牌默认保存在key为authentication的请求头中
        List<String> authorization = headers.get("authorization");
        if(authorization !=null && !authorization.isEmpty()){
            token=authorization.get(0);
        }
        // 4、解析并校验token
        Long userId=null;
        try {
            userId=jwtTool.parseToken(token);
        } catch (UnauthorizedException e) {
            // 拦截 并返回401
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
             // 结束响应
            return response.setComplete();
        }
        String userInfo = userId.toString();
        // http请求在转发到微服务之后就失效,由微服务调用其他微服务时需要重新在请求头中加入用户信息
        // 5、传递用户信息
        ServerWebExchange serverWebExchange = exchange.mutate().request(builder -> {
            builder.header("user-info", userInfo);
        }).build(); // 构建并返回修改后的 ServerWebExchange 实例

        System.out.println("userId = " + userId);
        // 6、放行
        return chain.filter(serverWebExchange);
    }

    private boolean isExclude(String path) {
        for(String pathPattern : authProperties.getExcludePaths()){
            if(antPathMatcher.match(pathPattern,path)){
                return true;
            }
        }
        return false;
    }

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

}

微服务拦截器:

  • 实现 HandlerInterceptor 接口其中的 preHandle() 方法,在 Controller 之前执行

  • 实现 afterCompletion() 方法在 Controller 执行完之后执行,清除 ThreadLocal 中的用户信息

  • 注册拦截器:定义 SpringMVC 的配置类,实现 WebMvcConfigurer 中的 addInterceptors()

不过,需要注意的是,这个配置类默认是不会生效的,因为它所在的包是com.hmall.common.config,与其它微服务的扫描包不一致,无法被扫描到,因此无法生效。

基于SpringBoot的自动装配原理,我们要将其添加到resources目录下的META-INF/spring.factories文件中:

注意❗网关模块不能使用 / 引用 SpringMVC 相关的类,Spring Cloud Gateway 的底层用的不是 SpringMVC,而是响应式的 webflux

网关项目也引入了 common 模块,但是 common 模块中定义了 SpringMVC 的拦截器,微服务项目需要该拦截器,网关不需要该拦截器,如何次拦截器配置类在有些情况(微服务模块)生效,有些情况(网关)不生效?

解决方法:

让 SpringMVC 的配置类根据条件来加载,即判断是否有 SpringMVC(网关和其他微服务项目的差别就是是否有 SpringMVC,可以根据是否有 SpringMVC 中的核心 API DispatcherServlet)

gateway不是基于springMvc的,所以该MvcConfig不应该生效。通过使用@ConditionalOnClass(DispatcherServlet.class),表示仅对包含了springMvc的核心类(DispatcherServlet)的微服务生效

3、微服务之间传递用户信息

前端发起的请求都会经过网关再到微服务,由于我们之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。

但有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务。比如下单业务,流程如下:

下单的过程中,需要调用商品服务扣减库存,调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是,订单服务调用购物车时并没有传递用户信息,购物车服务无法知道当前用户是谁!

由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头

微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求。我们如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢?

这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor

public interface RequestInterceptor {

  /**
   * Called for every request. 
   * Add data using methods on the supplied {@link RequestTemplate}.
   */
  void apply(RequestTemplate template);
}

们只需要实现这个接口,然后实现apply方法,利用RequestTemplate类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。

由于FeignClient全部都是在hm-api模块,因此我们在hm-api模块的com.hmall.api.config.DefaultFeignConfig中编写这个拦截器:

com.hmall.api.config.DefaultFeignConfig中添加一个Bean:

import com.hmall.api.fallback.*;
import com.hmall.common.utils.UserContext;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;


public class DefaultFeignConfig {

    @Bean
    public Logger.Level DefaultLoggerLevel() {
        return Logger.Level.FULL;
    }


    @Bean
    public RequestInterceptor userInfoRequestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                // 获取登录用户信息
                Long userInfo = UserContext.getUser();
                if (userInfo == null) {
                    // 没有登录用户信息,不添加header
                    return;
                }
                // 添加header
                requestTemplate.header("user-info", userInfo.toString());
            }
        };
    }

    @Bean
    public ItemClientFallback itemClientFallback() {
        return new ItemClientFallback();
    }

    @Bean
    public CartClientFallback cartClientFallback(){
        return new CartClientFallback();
    }

    @Bean
    public UserClientFallback userClientFallback() {
        return new UserClientFallback();
    }

    @Bean
    public TradeClientFallback tradeClientFallback(){
        return new TradeClientFallback();
    }

    @Bean
    public PayClientFallback payClientFallback(){
        return new PayClientFallback();
    }


}

好了,现在微服务之间通过OpenFeign调用时也会传递登录用户信

微服务架构中,如果使用Feign来进行服务的通信,并且希望在调用链路中传递MDC(Mapped Diagnostic Context),可以按照以下步骤进行配置: 1. 配置Feign的RequestInterceptor:通过实现Feign的RequestInterceptor接口,可以在请求被发送之前拦截请求并修改请求的头部信息。在这个拦截器中,你可以将MDC的数据添加到请求头中。 ```java public class MDCFeignRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { // 获取MDC的数据并添加到请求头 template.header("X-MDC-Data", MDC.getCopyOfContextMap().toString()); } } ``` 2. 注册MDCFeignRequestInterceptor:将上述的拦截器注册到Feign客户端中。 ```java @Configuration public class FeignConfig { @Bean public MDCFeignRequestInterceptor mdcFeignRequestInterceptor() { return new MDCFeignRequestInterceptor(); } @Bean public feign.RequestInterceptor requestInterceptor() { return mdcFeignRequestInterceptor(); } } ``` 3. 在服务调用的地方使用@FeignClient注解指定feign.RequestInterceptor的Bean。 ```java @FeignClient(name = "service-name", configuration = FeignConfig.class) public interface MyFeignClient { // Feign接口定义 } ``` 通过以上配置,MDC的数据就会被传递到Feign请求的头部中。在服务提供方接收到请求后,可以从请求头中获取MDC的数据,并将其设置到MDC中,以保证在服务提供方的日志中能够正确地记录MDC的相关信息。 当然,在使用上述方法传递MDC时,需要确保各个微服务的日志框架都支持MDC,并且在日志输出时正确地处理MDC的数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值