目录

Gateway网关路由与登录鉴权

Gateway:网关路由与登录鉴权

在微服务架构中,用户登录和身份校验的处理方式确实与单体应用有所不同。在单体架构中,一旦用户通过身份验证,其会话信息可以在整个应用范围内共享,所有模块都能访问到用户信息。然而,在微服务架构下,每个服务独立部署且通常运行在不同的进程中,因此需要一种机制来确保用户的身份信息能够在各个微服务之间安全、高效地传递和验证。

目录


网关实现路由

问题:每个微服务都有不同的地址或端口,入口不同,请求不同数据时要访问不同的入口,需要维护多个入口地址,前端无法调用nacos,无法实时更新服务列表。

解决方案:采用微服务网关,数据在网络间传输,从一个网络传输到另一网络时就需要经过网关来做数据的 路由 和转发。

在微服务中新建一个网关模块,作为网关微服务

引入依赖(需要加入nacos依赖,让nacos管理)

 <dependencies>
        <!--common-->
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>hm-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!--网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--nacos discovery-->
        <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>
    </dependencies>

在 application.yaml 文件,配置路由

  gateway:
      routes:
        - id: item # 路由规则id自定义唯一
          uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
          predicates: # 路由断言判断当前请求是否符合当前规则符合则路由到目标服务
            - Path=/items/**,/search/** # 这里是以请求路径作为判断规则

经测试成功

网关登录鉴权

问题:单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。但是,在微服务中,每个微服务都独立部署,一般只有用户微服务能校验登录信息,事实上,这是不安全的。

解决方案:既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做。

鉴权思路

登录是基于JWT来实现的,校验JWT的算法复杂,而且需要用到秘钥。我们不可能让个微服务都需要知道JWT的秘钥,不安全。也不可能每个微服务重复编写登录校验代码、权限校验代码,麻烦。

所以,我们在网关和用户服务保存秘钥,开发登录校验功能。

登录校验流程图

https://i-blog.csdnimg.cn/direct/4c55f5a61253493fac4f1f0c364221d0.png

我们可以看到:前端——网关——后端服务

我们在网关层去实现过滤请求。

网关过滤器详解

Gateway 内部工作的基本原理:

https://i-blog.csdnimg.cn/direct/c57c372e34004d0ba859406a8fe3634d.png

如图所示:

  1. 客户端请求进入网关后由 HandlerMapping 对请求做判断,找到与当前请求匹配的路由规则( Route ),然后将请求交给 WebHandler 去处理
  2. WebHandler 则会加载当前路由下需要执行的过滤器链( Filter chain ),然后按照顺序逐一执行过滤器(后面称为 Filter
  3. 图中 Filter 被虚线分为左右两部分,是因为 Filter 内部的逻辑分为 prepost 两部分,分别会在请求路由到微服务 之前之后 被执行
  4. 只有所有 Filterpre 逻辑都依次顺序执行通过后,请求才会被路由到微服务
  5. 微服务返回结果后,再倒序执行 Filterpost 逻辑
  6. 最终把响应结果返回

反正就是:我们需要在 NettyRoutingFilter过滤器之前,在发起Request时,即pre时,定义一个过滤器,进行网关登录校验。

实现网关过滤器

我们采用全局过滤器,作用范围是所有路由,即 GlobalFilter。

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;

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

    private final AuthProperties authProperties;

    private final JwtTool jwtTool;

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    /**
     * 过滤器
     * @param exchange
     * @param chain
     * @return
     */
    @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) {
            // 如果无效,拦截
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        // 5.获取用户信息
        String userInfo = userId.toString();
        ServerWebExchange swe = exchange.mutate().request(builder -> builder.header("user-info",userInfo ))
                .build();
        // 6.放行
        return chain.filter(swe);
    }

    /**
     * 判断路径是否需要拦截
     * @param antPath
     * @return
     */
    private boolean isExclude(String antPath) {
        for (String pathPattern : authProperties.getExcludePaths()) {
            if(antPathMatcher.match(pathPattern, antPath)){
                return true;
            }
        }
        return false;
    }

    // 优先级
    @Override
    public int getOrder() {
        return 0;
    }
}

因为我采用了JWT令牌进行校验,所以引入了JWT工具类,进行令牌的获取与解析。

至于 AuthProperties 是配置了不需要鉴权就能访问的路径。

经测试,网关已经可以完成登录校验并获取登录用户身份信息。


问题:当网关将请求转发到微服务时,微服务如何获取用户身份,我们不可能每个微服务都写一个拦截器去得到用户身份信息。

解决方案:将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息(上述代码第五步已经完成了)。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal。

拦截器流程图

https://i-blog.csdnimg.cn/direct/f362ae5358334459b051f071e81c6c84.png

提供一个用于保存登录用户的ThreadLocal工具UserContext:

public class UserContext {
    private static final ThreadLocal<Long> tl = new ThreadLocal<>();

    /**
     * 保存当前登录用户信息到ThreadLocal
     * @param userId 用户id
     */
    public static void setUser(Long userId) {
        tl.set(userId);
    }

    /**
     * 获取当前登录用户信息
     * @return 用户id
     */
    public static Long getUser() {
        return tl.get();
    }

    /**
     * 移除当前登录用户信息
     */
    public static void removeUser(){
        tl.remove();
    }
}

在公用模块下定义一个拦截器:

public class UserInfoInterceptor implements HandlerInterceptor {

    /**
     * 请求拦截器
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的 用户信息
        String userInfo = request.getHeader("user-info");
        // 2.判断是否为空
        if (StringUtils.isNotBlank(userInfo)) {
            UserContext.setUser(Long.valueOf(userInfo));
        }
        // 3.放行
        return true;
    }

    /**
     * 响应拦截器
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserContext.removeUser();
    }
}

编写 SpringMVC 的配置类,配置登录拦截器:

@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor());
    }
}

注意:这个配置类默认是不会生效的,基于SpringBoot的自动装配原理,我们要将其添加到 resources 目录下的 META-INF/spring.factories 文件中:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.hmall.common.config.MyBatisConfig,\
  com.hmall.common.config.MvcConfig

经测试,服务得到网关传递的用户身份信息。

服务信息鉴权

问题:因为之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。但是,有时候请求到达微服务后还需要调用其它多个微服务。我们没有实现服务之间的用户身份信息的传递。

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

因为我们之前微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求。所以我们可以采用Feign中提供的一个拦截器接口: feign.RequestInterceptor。

在公用模块下定义一个 userInfoRequestInterceptor

    /**
     * feign请求拦截器 微服务之间的远程调用时,将当前登录用户的userId传递给目标服务
     * @return
     */
    @Bean
    public RequestInterceptor userInfoRequestInterceptor() {
        return new RequestInterceptor() {
            public void apply(RequestTemplate requestTemplate) {
                Long userId = UserContext.getUser();
                if (userId != null) {
                    requestTemplate.header("user-info", userId.toString());
                }
            }
        };
    }

总结:

  • 为了实现网关处简便的登录校验,我们采用了 GlobalFilter
  • 为了实现网关传递用户信息到多个微服务,我们采用了 UserInfoInterceptor
  • 为了实现微服务之间用户身份信息传递,我们采用了 userInfoRequestInterceptor。