小编目前大一,刚开始着手学习微服务的相关知识,小编会把它们整理成知识点发布出来。我认为同为初学者,我把我对知识点的理解以这种代码加观点的方式分享出来不仅加深了我的理解,或许在某个时候对你也有所帮助,同时也欢迎大家在评论区分享你们的观点。

       知不足而奋进,望远山而前行。  

目录

前言

快速入门

路由属性

网关登录校验


前言

        在我们把一个单体架构拆分成微服务时会遇到,服务端口过多,并且将来有可能发生变化,前端不知道该请求谁,又或者是每个服务都可能需要登录用户信息,各自去做不仅麻烦,还有密钥泄露的风险诸如此类的等等问题。为了解决上述问题,我们就需要了解网关相关的知识。

        网关就是网络的关口,负责请求的路由、转发、身份校验。微服务就相当于小区的住户,网关就是小区的保安,记录着每位业主的住址信息,几楼几号,对于陌生人要进小区会查验身份,身份合格后才会放你进来,并告诉你要找的人的住址。在我们项目中,就是前端直接发送请求到网关,网关身份校验后,再通过路由转发去调用指定的微服务。        

        另外我们通过注册中心来进行服务拉取获取服务列表,实现了网关对微服务状态信息的获取,如果服务挂掉了,我们也就不会路由转发给它。

        在SpringCloud中网关的实现包括两种:Spring Cloud Gateway Netfilx Zuul,具体二者的区别见下图:

快速入门

        接下来我们就来尝试使用一下网关,网关的快速入门主要分为两步:第一步搭建好网关,第二步就是配置路由规则,剩下的事情网关就可以自动去完成了。但是虽说是两步,但是引入依赖,编写启动类这些基础的其实也在里面。其实搭建网关服务,依赖,启动类就是重新创建个模块,还是不难的。重点就是配置路由规则,具体规则如下:

        id这一块我们直接以服务名称做id,方便还不容易出错,uri属性也是比较简单的。第三个predicates属性,就是判断请求是否符合规则,这里我们通常使用模糊匹配,匹配整个controller所有的方法。

        首先创建一个新的模块,我就把它叫做hm-gateway吧,接着第二步引入相关依赖。

    <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>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

        第三步编写网关启动类

@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

        第四步就是最重要的配置路由规则了,按照上面定义的规则,我们不难写出下面的代码。

spring:
  cloud:
    gateway:
      routes:
        - id: item-service
          uri: lb://item-service
          predicates:
            - Path=/items/**,/search/**
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/users/**,/addresses/**

        以上步骤完成后,我们就可以去浏览器测试一下了。

        通过网关端口8080访问,也是请求到了数据,证明没有什么问题。

路由属性

        以上的路由属性配置已经能满足我们基本的需求了,但是难免有时候会出现我们有别的需求,所以我们有必要了解一下路由属性。

        我们知道在yaml文件中所有配置属性最终都会有一个对应的java类来读取,网关路由对应的java类型是RouteDefinition,其中常见的属性有:id、uri、predicates、filters。

        在Spring内部提供了12种基本的路由断言RoutePredicateFactory实现:

        网关中提供了33种路由过滤器,每种过滤器都有独特的作用。

        接下来我们就来尝试使用一下过滤器,首先我们要到yml文件种去配置过滤器,我这里就选择添加请求头。

    gateway:
      routes:
        - id: item-service
          uri: lb://item-service
          predicates:
            - Path=/items/**,/search/**
          filters:
            - AddRequestHeader=truth, anyone long-press like button will be rich

        设置好后,我们就去找个方法拿到请求头并打印到控制台。

@ApiOperation("分页查询商品")
    @GetMapping("/page")
    public PageDTO<ItemDTO> queryItemByPage(PageQuery query, @RequestHeader(value = "truth", required = false) String truth) {
        System.out.println("truth:" + truth);
        // 1.分页查询
        Page<Item> result = itemService.page(query.toMpPage("update_time", false));
        // 2.封装并返回
        return PageDTO.of(result, ItemDTO.class);
    }

        接下来我们就看看访问商品服务的分页查询功能时,能不能获取到请求头并打印到控制台。 

        很显然我们是成功了的。 另外过滤器也可以不用配置到routes中,也可以配置与routes同级,这样就叫做默认过滤器。这样就实现了给所有路由配置了过滤器,方便省事,同时也是生效的

 网关登录校验

     思路分析

        用户微服务实现了登录授权后会携带JWT令牌,但是其它服务,比如订单,购物车等也需要用户的信息的话,不能每个服务都做JWT校验,所以JWT的校验应该由网关来实现,同时该检验一定要放在路由转发之前。

        网关请求处理的流程图见下:

        既然NettyFilter是负责转发请求的,所以我们只需要自定义一个过滤器保证执行顺序在Netty路由过滤器之前并且在它的pre阶段中实现JWT的校验可以实现登录校验了。 另外网关在校验之后还需要将用户信息传递给微服务,这一块我们就可以将用户信息保存到请求头,这样微服务也可以从请求头中取出用户信息。还有一点需要注意,有时候微服务之间也会互相调用,那如何在微服务之间传递用户信息呢,微服务之间是通过OpenFeign去实现的,所以这一块和网关保存用户信息到请求头还是有所不同。

自定义过滤器

        网关过滤器有两种,分别是:GatewayFilterGlobalFilter。二者的区别如下:

        这两种过滤器的过滤方法都是filter,无论是返回值类型,还是参数都是一样的。

 

    自定义GlobalFilter

        其实这个也比较简单,我们只要让我们自定以的Filter继承GlobalFilter,接着重写filter方法就行了,另外我们还要保证我们的过滤器是在NettyFilter之前执行,所以我们还要继承Order接口,来设置一些优先级,NettyFilter是最低优先级,int的最大值,我们只要比这个数小,那么优先级就比它大。

@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // TODO 模拟实现登录逻辑
        ServerHttpRequest request = exchange.getRequest();
        HttpHeaders headers = request.getHeaders();
        System.out.println("headers:" + headers);
        // 放行
        return chain.filter(exchange);
    }

    //  设置我们过滤器的优先级要比NettyFilter高
    //  NettyFilter是int的最大值,我们只要比这个数小,优先级就比它大
    @Override
    public int getOrder() {
        return 0;
    }
}
    自定义GatewayFilter

        GatewayFilter相比于GlobalFilter更加灵活,可以任意指定路由,但是如果我们想自定义GatewayFilter就比较麻烦了。将来我们使用GlobalFilter更多,这一块仅作了解就好了。

        自定以GatewayFilter不是直接实现GatewayFilter,而是实现AbstractGatewayFilterfactory

         下面就是使用工厂模式定义一个无参的GatewayFilter,我们将它的优先级指定为1,这样它就比我们刚刚定义的GlobalFilter级别高了。

@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
    @Override
    public GatewayFilter apply(Object config) {
        return new OrderedGatewayFilter(new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                System.out.println("print any filter running");
                return chain.filter(exchange);
            }
        }, 1);
    }
}

        这一种过滤器仅作了解就好了,大部分情况我们使用的都是GlobalFilter

实现登录校验

        接着我们就来手写一个GlobalFilter来实现登录校验,这一块其实和我们之前写的JWT校验都差不多,只不过我们要设置一下排除一些路径,因为有些路径的访问时不需要做登录拦截的。

@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. 获取request对象
        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();
        }
        // TODO 5.传递用户信息
        System.out.println("userId = " + userId);
        // 6. 放行
        return chain.filter(exchange);
    }

    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;
    }
}

        这一块的代码也没有什么好说的,都是和之前一样的逻辑。但是我们还留下了一个问题,那就是我们还要将用户信息传递下去,这个应该怎么实现呢? 

网关传递用户

        对于微服务之间的服务调用或者网关到服务的路由发送,我们都可以通过SpringMVC提供的拦截器,获取用户信息并保存到ThreadLocal中。

        这里其实分为两步,第一步我们要在网关的登录校验过滤器中,把获取到的用户写入请求头,这一块其实已经提供好了现成的API,我们直接调用就好了。

        // 5.传递用户信息
        String userInfo = userId.toString();
        ServerWebExchange swe = exchange.mutate()
                .request(builder -> builder.header("user-info", userInfo))
                .build();

        第二步就是在common模块中编写SpringMVC拦截器,获取登录用户。具体为什么定义在common模块时因为定义在这里就只需要写一遍了。

        拦截器定义这一块没什么好说的,都是和之前差不多的

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

    @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 {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor());
    }
}

        好了,以上我们就学会了从网关到微服务的用户信息传递。

OpenFeign传递用户

        接着我们来看看微服务之间的调用如何进行用户信息的传递。例如当我们去支付订单后,支付服务需要去调用购物车服务删除购物车的数据。

        现在我们在服务之间的调用时也应该让它去带上请求头,但是这个服务之间的请求时OpenFeign帮我们发送的,所以应该怎么告诉OpenFeign发送千秋之前带上请求头呢?

        OpenFeign中提供了一个拦截器接口,所有由OpenFeign发起的请求都会先调用拦截器处理请求。

         这一块我们直接使用匿名内部类就好了。

@Bean
    public RequestInterceptor userInfoRequestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                Long userId = UserContext.getUser();
                if (userId != null) {
                    requestTemplate.header("user-info", userId.toString());
                }
            }
        };
    }

        到这里我们就实现好了微服务之间调用获取用户信息的需求。

        可能到这里已经那么多过滤器拦截器什么的,已经绕晕了,对照上面这张图,相信你可以理清这之间的关系。

        以上就是网关的相关知识。

        带着决心起床,带着满意入睡。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部