gateway(二)信息传递的三种情况

1、通过过滤器在网关内获取用户信息(网关内传递)

通过下面的代码,就可以实现网关转发请求到微服务的时候携带用户信息

package com.hmall.gateway.filters;

import com.hmall.common.exception.UnauthorizedException;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.utils.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
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;

@Component
@RequiredArgsConstructor
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.获取用户信息
        ServerHttpRequest request = exchange.getRequest();
        //  2.判断是否需要做登录拦截
        if(isExclude(request.getPath().toString()))    return chain.filter(exchange);  //  放行
        //  3.获取token
        String token = null;
        List<String> headers = request.getHeaders().get("authorization");
        if(headers!=null && !headers.isEmpty()) {
            token = headers.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();
        }
        //  5.传递用户信息
        String userInfo = userId.toString();
        ServerWebExchange serverWebExchange = exchange.mutate()
                .request(builder -> builder.header("user-info", userInfo))
                .build();
        //  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;
    }
}

2、微服务解析网关传过来的用户信息(网关和微服务之间传递)

解析有很多种方法,一种是最原始的,也就是在controller形参处加上@RequestHeader注解来获取请求头内的用户信息,但是这样就太麻烦了,因为每个controller都要这么做。
第二种方法就是基于SpringMVC的拦截器,如果每个拦截器都写也挺麻烦的,所以最终我们把拦截器写到了common模块里,在这里面获取请求头里面的信息,并保存到ThreadLocal,这样后续的所有业务都能从ThreadLocal里获取到用户信息。(简而言之,就是定义一个通过用的拦截器,用于获取网关传递过来的用户信息)。

在把拦截器定义在common模块下我们也碰到了很多问题,就是我们把拦截器的配置放在了common模块下,这样就导致了我们其它微服务是扫描不到的,没有办法生效,在这里,我们用到了SpringBoot自动装配的原理(Spring通过扫描指定的包路径,自动发现标注了特定注解 如@Component,@Service,@Repository的类,并将其注册为Spring容器中的bean):我们将定义的配置类放到了resource/META-INF下的spring.factories文件下,这样的话就能实现自动装配。

但是这又带来另外一个问题,就是我们希望这个配置类在微服务里生效,在网关里不要生效,因此我们又用了一个条件注解@ConditionalOnClass(DispatcherServlet.class) 来去判断当前项目下有没有DispatcherServlet,网关里没有,这样就不会生效了

用来获取网关传过来的用户:

package com.hmall.common.interceptors;

import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class UserInfoInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {   //  controller之前执行
        //  1.获取用户信息
        String userInfo = request.getHeader("user-info");//  和网关保持一致
        //  2.判断是否获取了用户,如果有,存入ThreadLocal
        if(StrUtil.isNotBlank(userInfo))    {
            UserContext.setUser(Long.valueOf(userInfo));    //  保存到ThreadLocal
        }
        //  3.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {  //  controller完成之后
        //  清理用户
        UserContext.removeUser();
    }
}

拦截器生效的配置类

package com.hmall.common.config;

import com.hmall.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)    //  判断一个类是否存在,当DispatcherServlet.class存在时,配置生效
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {     //  拦截器的注册器
        registry.addInterceptor(new UserInfoInterceptor());
    }
}

3、微服务之间传递

package com.hmall.api.config;

import com.hmall.common.utils.UserContext;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;

public class DefaultFeignConfig {   //  千万不要在这个类上面加@Configuration注解,因为这个配置类会在其它启动类上的注解引用
    @Bean
    public Logger.Level feignLogLevel() {
        return Logger.Level.BASIC;
    }
    @Bean
    public RequestInterceptor userInfoInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                Long userId = UserContext.getUser();
                //  为什么上面这行代码可以从UserContext里取用户信息,因为之前的服务中的信息是网关给的,这个时候请求头里是带了用户信息的
                if(userId != null)  {
                    requestTemplate.header("user-info", userId.toString());
                }
            }
        };
    }
}
posted @ 2024-05-03 15:33  惊朝  阅读(194)  评论(0编辑  收藏  举报