首页 > 其他分享 >通过网关实现登录验证以及微服务获取服务和OpenFeign传递用户

通过网关实现登录验证以及微服务获取服务和OpenFeign传递用户

时间:2024-06-07 17:45:57浏览次数:14  
标签:OpenFeign 网关 服务 请求 登录 校验 过滤器

 

网关登录校验

单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。

鉴权思路分析

我们的登录是基于JWT来实现的,校验JWT的算法复杂,而且需要用到秘钥。如果每个微服务都去做登录校验,这就存在着两大问题:
  • 每个微服务都需要知道JWT的秘钥,不安全
  • 每个微服务重复编写登录校验代码、权限校验代码,麻烦
  既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做,这样之前说的问题就解决了:
  • 只需要在网关和用户服务保存秘钥
  • 只需要在网关开发登录校验功能
  此时,登录校验的流程如图:

不过,这里存在几个问题:
  • 网关路由是配置的,请求转发是Gateway内部代码,我们如何在转发之前做登录校验?
  • 网关校验JWT之后,如何将用户信息传递给微服务?
  • 微服务之间也会相互调用,这种调用不经过网关,又该如何传递用户信息?
  这些问题将在接下来几节一一解决。

网关过滤器

登录校验必须在请求转发到微服务之前做,否则就失去了意义。而网关的请求转发是Gateway内部代码实现的,要想在请求转发之前做登录校验,就必须了解Gateway内部工作的基本原理。 暂时无法在飞书文档外展示此内容 如图所示:
  1. 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。
  2. WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为Filter)。
  3. 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为prepost两部分,分别会在请求路由到微服务之前之后被执行。
  4. 只有所有Filterpre逻辑都依次顺序执行通过后,请求才会被路由到微服务。
  5. 微服务返回结果后,再倒序执行Filterpost逻辑。
  6. 最终把响应结果返回。
  如图中所示,最终请求转发是有一个名为NettyRoutingFilter的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter之前,这就符合我们的需求了!   那么,该如何实现一个网关过滤器呢? 网关过滤器链中的过滤器有两种:
  • GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.
  • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。
  注意:过滤器链之外还有一种过滤器,HttpHeadersFilter,用来处理传递到下游微服务的请求头。例如org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter可以传递代理请求原本的host头到下游微服务。   其实GatewayFilterGlobalFilter这两种过滤器的方法签名完全一致:
/**
 * 处理请求并将其传递给下一个过滤器
 * @param exchange 当前请求的上下文,其中包含request、response等各种数据
 * @param chain 过滤器链,基于它向下传递请求
 * @return 根据返回值标记当前请求是否被完成或拦截,chain.filter(exchange)就放行了。
 */
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
FilteringWebHandler在处理请求时,会将GlobalFilter装饰为GatewayFilter,然后放到同一个过滤器链中,排序以后依次执行。   Gateway中内置了很多的GatewayFilter,详情可以参考官方文档: https://docs.spring.io/spring-cloud-gateway/docs/3.1.7/reference/html/#gatewayfilter-factories   Gateway内置的GatewayFilter过滤器使用起来非常简单,无需编码,只要在yaml文件中简单配置即可。而且其作用范围也很灵活,配置在哪个Route下,就作用于哪个Route. 例如,有一个过滤器叫做AddRequestHeaderGatewayFilterFacotry,顾明思议,就是添加请求头的过滤器,可以给请求添加一个请求头并传递到下游微服务。 使用的使用只需要在application.yaml中这样配置:
spring:
  cloud:
    gateway:
      routes:
      - id: test_route
        uri: lb://test-service
        predicates:
          -Path=/test/**
        filters:
          - AddRequestHeader=key, value # 逗号之前是请求头的key,逗号之后是value
  如果想要让过滤器作用于所有的路由,则可以这样配置:
spring:
  cloud:
    gateway:
      default-filters: # default-filters下的过滤器可以作用于所有路由
        - AddRequestHeader=key, value
      routes:
      - id: test_route
        uri: lb://test-service
        predicates:
          -Path=/test/**

网关过滤器

登录校验必须在请求转发到微服务之前做,否则就失去了意义。而网关的请求转发是Gateway内部代码实现的,要想在请求转发之前做登录校验,就必须了解Gateway内部工作的基本原理。 暂时无法在飞书文档外展示此内容 如图所示:
  1. 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。
  2. WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为Filter)。
  3. 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为prepost两部分,分别会在请求路由到微服务之前之后被执行。
  4. 只有所有Filterpre逻辑都依次顺序执行通过后,请求才会被路由到微服务。
  5. 微服务返回结果后,再倒序执行Filterpost逻辑。
  6. 最终把响应结果返回。
  如图中所示,最终请求转发是有一个名为NettyRoutingFilter的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter之前,这就符合我们的需求了!   那么,该如何实现一个网关过滤器呢? 网关过滤器链中的过滤器有两种:
  • GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.
  • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。
  注意:过滤器链之外还有一种过滤器,HttpHeadersFilter,用来处理传递到下游微服务的请求头。例如org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter可以传递代理请求原本的host头到下游微服务。   其实GatewayFilterGlobalFilter这两种过滤器的方法签名完全一致:
/**
 * 处理请求并将其传递给下一个过滤器
 * @param exchange 当前请求的上下文,其中包含request、response等各种数据
 * @param chain 过滤器链,基于它向下传递请求
 * @return 根据返回值标记当前请求是否被完成或拦截,chain.filter(exchange)就放行了。
 */
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
FilteringWebHandler在处理请求时,会将GlobalFilter装饰为GatewayFilter,然后放到同一个过滤器链中,排序以后依次执行。   Gateway中内置了很多的GatewayFilter,详情可以参考官方文档: https://docs.spring.io/spring-cloud-gateway/docs/3.1.7/reference/html/#gatewayfilter-factories   Gateway内置的GatewayFilter过滤器使用起来非常简单,无需编码,只要在yaml文件中简单配置即可。而且其作用范围也很灵活,配置在哪个Route下,就作用于哪个Route. 例如,有一个过滤器叫做AddRequestHeaderGatewayFilterFacotry,顾明思议,就是添加请求头的过滤器,可以给请求添加一个请求头并传递到下游微服务。 使用的使用只需要在application.yaml中这样配置:
spring:
  cloud:
    gateway:
      routes:
      - id: test_route
        uri: lb://test-service
        predicates:
          -Path=/test/**
        filters:
          - AddRequestHeader=key, value # 逗号之前是请求头的key,逗号之后是value
  如果想要让过滤器作用于所有的路由,则可以这样配置:
spring:
  cloud:
    gateway:
      default-filters: # default-filters下的过滤器可以作用于所有路由
        - AddRequestHeader=key, value
      routes:
      - id: test_route
        uri: lb://test-service
        predicates:
          -Path=/test/**
 

GlobalFilter

自定义GlobalFilter则简单很多,直接实现GlobalFilter即可:
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 编写过滤器逻辑
        System.out.println("未登录,无法访问");
        // 放行
        // return chain.filter(exchange);

        // 拦截
        ServerHttpResponse response = exchange.getResponse();
        response.setRawStatusCode(401);
        return response.setComplete();
    }

    @Override
    public int getOrder() {
        // 过滤器执行顺序,值越小,优先级越高
        return 0;
    }
}
 

登录校验

接下来,我们就利用自定义GlobalFilter来完成登录校验。

JWT工具

登录校验需要用到JWT,而且JWT的加密需要秘钥和加密工具。这些在hm-service中已经有了,我们直接拷贝过来: 具体作用如下:
  • AuthProperties:配置登录校验需要拦截的路径,因为不是所有的路径都需要登录才能访问
  • JwtProperties:定义与JWT工具有关的属性,比如秘钥文件位置
  • SecurityConfig:工具的自动装配
  • JwtTool:JWT工具,其中包含了校验和解析token的功能
  • hmall.jks:秘钥文件
  其中AuthPropertiesJwtProperties所需的属性要在application.yaml中配置:
hm:
  jwt:
    location: classpath:hmall.jks # 秘钥地址
    alias: hmall # 秘钥别名
    password: hmall123 # 秘钥文件密码
    tokenTTL: 30m # 登录有效期
  auth:
    excludePaths: # 无需登录校验的路径
      - /search/**
      - /users/login
      - /items/**
 

登录校验过滤器

接下来,我们定义一个登录校验的过滤器: 代码如下:
package com.hmall.gateway.filter;

import com.hmall.common.exception.UnauthorizedException;
import com.hmall.common.utils.CollUtils;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.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.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
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private final JwtTool jwtTool;

    private final AuthProperties authProperties;

    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 (!CollUtils.isEmpty(headers)) {
            token = headers.get(0);
        }
        // 4.校验并解析token
        Long userId = null;
        try {
            userId = jwtTool.parseToken(token);
        } catch (UnauthorizedException e) {
            // 如果无效,拦截
            ServerHttpResponse response = exchange.getResponse();
            response.setRawStatusCode(401);
            return response.setComplete();
        }

        // TODO 5.如果有效,传递用户信息
        System.out.println("userId = " + userId);
        // 6.放行
        return chain.filter(exchange);
    }

    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;
    }
}
  重启测试,会发现访问/items开头的路径,未登录状态下不会被拦截:   访问其他路径则,未登录状态下请求会被拦截,并且返回401状态码:  

微服务获取用户

现在,网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢? 由于网关发送请求到微服务依然采用的是Http请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal,方便后续使用。 据图流程图如下:   因此,接下来我们要做的事情有:
  • 改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务
  • 编写微服务拦截器,拦截请求获取用户信息,保存到ThreadLocal后放行
 

保存用户到请求头

首先,我们修改登录校验拦截器的处理逻辑,保存用户信息到请求头中:  

拦截器获取用户

在hm-common中已经有一个用于保存登录用户的ThreadLocal工具: 其中已经提供了保存和获取用户的方法:   接下来,我们只需要编写拦截器,获取用户信息并保存到UserContext,然后放行即可。   由于每个微服务都有获取登录用户的需求,因此拦截器我们直接写在hm-common中,并写好自动装配。这样微服务只需要引入hm-common就可以直接具备拦截器功能,无需重复编写。   我们在hm-common模块下定义一个拦截器: 具体代码如下:
package com.hmall.common.interceptor;

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 {
        // 1.获取请求头中的用户信息
        String userInfo = request.getHeader("user-info");
        // 2.判断是否为空
        if (StrUtil.isNotBlank(userInfo)) {
            // 不为空,保存到ThreadLocal
                UserContext.setUser(Long.valueOf(userInfo));
        }
        // 3.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserContext.removeUser();
    }
}
  接着在hm-common模块下编写SpringMVC的配置类,配置登录拦截器: 具体代码如下:
packagecom.hmall.common.config;

importcom.hmall.common.interceptors.UserInfoInterceptor;
importorg.springframework.boot.autoconfigure.condition.ConditionalOnClass;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.web.servlet.DispatcherServlet;
importorg.springframework.web.servlet.config.annotation.InterceptorRegistry;
importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@ConditionalOnClass(DispatcherServlet.class)
publicclassMvcConfig implementsWebMvcConfigurer {
    @Override
    publicvoidaddInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(newUserInfoInterceptor());
    }
}
  不过,需要注意的是,这个配置类默认是不会生效的,因为它所在的包是com.hmall.common.config,与其它微服务的扫描包不一致,无法被扫描到,因此无法生效。 基于SpringBoot的自动装配原理,我们要将其添加到resources目录下的META-INF/spring.factories文件中: 内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.hmall.common.config.MyBatisConfig,\
  com.hmall.common.config.MvcConfig
 

恢复购物车代码

之前我们无法获取登录用户,所以把购物车服务的登录用户写死了,现在需要恢复到原来的样子。 找到cart-service模块的com.hmall.cart.service.impl.CartServiceImpl 修改其中的queryMyCarts方法:  

OpenFeign传递用户

前端发起的请求都会经过网关再到微服务,由于我们之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。 但有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务。比如下单业务,流程如下:   下单的过程中,需要调用商品服务扣减库存,调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是,订单服务调用购物车时并没有传递用户信息,购物车服务无法知道当前用户是谁!   由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头。   微服务之间调用是基于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:
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
    return new RequestInterceptor() {
        @Override
        public void apply(RequestTemplate template) {
            // 获取登录用户
            Long userId = UserContext.getUser();
            if(userId == null) {
                // 如果为空则直接跳过
                return;
            }
            // 如果不为空则放入请求头中,传递给下游微服务
            template.header("user-info", userId.toString());
        }
    };
}
由于UserContext在common包中,所以需要引入在api模块中引入common包:
<dependency
    <groupIdcom.heima</groupId
    <artifactIdhm-common</artifactId
    <version1.0.0</version
</dependency
  然后在新启动类配置feign客户端的配置类(注意:A调用B接口,那么就需要在A服务的启动类上面新增): 好了,现在微服务之间通过OpenFeign调用时也会传递登录用户信息了。
 
 

标签:OpenFeign,网关,服务,请求,登录,校验,过滤器
From: https://www.cnblogs.com/linzepro/p/18237604

相关文章

  • 自动化搭建专属 AI 绘图服务
    通义万相AIGC技术已经比较成熟,结合阿里云的计算和存储产品可以方便的搭建自己专属的 AI 绘图服务。例如《创意加速器:AI 绘画创作》这个解决方案,利用阿里自研的通义万相AIGC技术在 Web 服务中实现先进的图像生成。AI 绘画服务搭建步骤从架构图中可以看到,要想搭建这样一套......
  • 【计算机毕业设计】基于SSM++jsp的在线医疗服务系统【源码+lw+部署文档】
    包含论文源码的压缩包较大,请私信或者加我的绿色小软件获取免责声明:资料部分来源于合法的互联网渠道收集和整理,部分自己学习积累成果,供大家学习参考与交流。收取的费用仅用于收集和整理资料耗费时间的酬劳。本人尊重原创作者或出版方,资料版权归原作者或出版方所有,本人不对所......
  • AlmaLinux 9 环境下部署 PostgreSQL 16 服务
    #将SELinux设置为宽容模式sudosetenforcePermissivesudovi/etc/selinux/configSELINUX=permissive#关闭系统防火墙服务sudosystemctlstopfirewalld.servicesudosystemctldisablefirewalld.service#安装PostgreSQL16服务sudoyuminstall-yhttps:/......
  • 09.0手工制作docker镜像-单服务ssh
    手动将容器保存为镜像-单服务ssh本页测试内容,将centos6.9镜像安装ssh服务并提交新的镜像并可使用。dockercommit容器id或者容器的名字新的镜像名字[:版本号可选]dockercommittestcentos6.9-ssh:v11)基于容器制作镜像,首先创建一个容器dockerrun-itcentos:6.9......
  • 09.1手工制作docker镜像-多服务ssh+nginx
    手工制作docker镜像-多服务ssh+nginx一个容器多个服务基于centos6.9系统添加yum源与epel源安装nginx、ssh服务yuminstallnginxopenssh-server-y因镜像系统为纯系统,没有root密码,所以需要配置密码echo123456|passwd--stdinroot注:因配置yum源存在坑,详见cento......
  • Java微服务实战:使用Spring Boot构建高效服务
    引言在当今的软件开发实践中,微服务架构已成为推动快速开发和部署的关键因素之一。与传统的单体应用相比,微服务架构提供了更高的灵活性和可维护性。本文将探讨如何使用Java和SpringBoot来构建一个微服务应用,介绍基本概念,并通过一个简单的例子演示从零开始构建微服务的全过程。......
  • DNS服务协议有哪些?
    DNS是互联网中的关键基础设施之一,它使得人们能够通过易记的域名来访问互联网上的各种资源,而无需记忆复杂的IP地址。DNS的主要功能包括域名解析、IP地址反向解析、负载均衡、邮件交换和安全性等。通过DNS,我们可以更方便、快捷地访问互联网上的资源。DNS域名系统在转换ip过程中,通常......
  • 今日好料推荐(运维服务白皮书+运维管理体系)
    今日好料推荐(运维服务白皮书+运维管理体系)本文内容是运维服务内容的理解和总结参考资料内容:企业应用运维管理指标体系白皮书&云网运维服务白皮书&金融IT运维对应用性能的提升&华为确定性运维体系手册运维服务的重要性及其标准在现代信息技术环境中,运维服务对于保障业务......
  • 在微服务架构模式中Redis的应用
    1. Redis概述                Redis是一种开源的内存数据库,也被称为数据结构服务器,它支持多种数据类型,如字符串、哈希表、列表、集合和有序集合。Redis具有快速、高效的特点,可以在内存中存储数据,并通过持久化机制将数据写入磁盘,保证数据的持久性。Redis还提......
  • k8s中ingress访问不到服务
    情况:通过k8s部署nacos,部署之后,测试通过service进行访问,但是通过ingress域名访问失败。原因:1、我的ingress-controller部署时使用的是deployment,该方式,只会在一个节点上部署pod,而不是全部的pod上进行部署。2、采用的网络使用的hostNetwork:true,这种方式使用的是本机的网络,除非......