• Spring Cloud Gateway中session共享


    Spring Cloud Gateway中session共享

    背景

    在进行zuul切换到gateway时,需要重新实现session共享,本文主要分享一下自己实现的方案。

    zuul中的session共享

    在zuul中,是通过spring-session-data-redis这个组件,将session的信息存放到redis中实现的session共享。这次也简单说明下如何实现以及一些注意的点。

    首先在网关zuul以及所有的微服务中添加spring-session-data-redis依赖:

    
    
      org.springframework.boot
      spring-boot-starter-data-redis
    
    
      org.springframework.session
      spring-session-data-redis
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    之后添加redis配置信息:

    spring:
      redis:
        host: localhost
        port: 6379
    
    • 1
    • 2
    • 3
    • 4

    添加EnableRedisHttpSession注解:

    /**
     * 指定flushMode为IMMEDIATE 表示立即将session写入redis
     *
     * @author yuanzhihao
     * @since 2022/5/8
     */
    @EnableRedisHttpSession(flushMode = FlushMode.IMMEDIATE)
    @Configuration
    public class RedisSessionConfig {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在网关zuul工程中,路由跳转到微服务时,需要添加sensitiveHeaders,设置为空,表示将敏感信息透传到下游微服务,这边需要将cookie的信息传下去,session共享保存到redis里面需要用到:

    zuul:
      routes:
        portal:
          path: /portal/**
          sensitiveHeaders: # 将敏感信息传到下游服务
          serviceId: portal
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    指定server.servlet.context-path路径:

    server.servlet.context-path=/gateway
    
    • 1

    zuul测试工程

    在我的代码库中,我提交了一个简单的demo,主要有四个工程,分别是网关zuul、主页portal、两个客户端client-1、server-1。

    网关zuul中添加路由信息:

    spring:
      application:
        name: zuul
      redis:
        host: localhost
        port: 6379
    server:
      servlet:
        context-path: /gateway
    zuul:
      routes:
        portal:
          path: /portal/**
          sensitiveHeaders:
          serviceId: portal
        client-1:
          path: /client1/**
          sensitiveHeaders:
          serviceId: eureka-client1
        server-1:
          path: /server1/**
          sensitiveHeaders:
          serviceId: eureka-server1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    添加登录过滤器,对所有的请求进行拦截,对于没有登录的请求会自动跳转到登录页面:

    /**
     * 登录过滤器
     *
     * @author yuanzhihao
     * @since 2022/5/8
     */
    @Component
    @Slf4j
    public class LoginFilter extends ZuulFilter {
        private static final List white_List = Arrays.asList("/login", "/logout");
    
        @Override
        public String filterType() {
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            return -1;
        }
    
        @Override
        public boolean shouldFilter() {
            HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
            String requestURI = request.getRequestURI();
            for (String uri : white_List) {
                if (requestURI.endsWith(uri)) {
                    return false;
                }
            }
            return true;
        }
    
        @SneakyThrows
        @Override
        public Object run() throws ZuulException {
            RequestContext currentContext = RequestContext.getCurrentContext();
            HttpServletRequest request = currentContext.getRequest();
            HttpSession session = request.getSession();
            UserInfo userInfo = (UserInfo) session.getAttribute("userInfo");
            if (userInfo == null) {
                HttpServletResponse response = currentContext.getResponse();
                response.sendRedirect("/gateway/portal/login");
            }
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    portal中简单实现了登录逻辑:

    /**
     * @author yuanzhihao
     * @since 2022/5/8
     */
    @Controller
    public class LoginController {
        @GetMapping(value = "/login")
        public String login(HttpServletRequest request, HashMap map) {
            UserInfo userInfo = (UserInfo) request.getSession().getAttribute("userInfo");
            if (userInfo != null) {
                map.put("userInfo", userInfo);
                return "index";
            }
            return "login";
        }
    
        @PostMapping("/login")
        public String login(UserInfo userInfo, HashMap map, HttpServletRequest request) {
            // 设置session
            request.getSession().setAttribute("userInfo", userInfo);
            map.put("userInfo", userInfo);
            return "index";
        }
    
        @GetMapping("/logout")
        public String logout(HttpServletRequest request) {
            request.getSession().invalidate();
            return "logout";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    在客户端client-1和server-1中可以请求到当前session中的用户信息:

    @GetMapping("/hello")
    public String hello(HttpServletRequest request) {
      UserInfo userInfo = (UserInfo) request.getSession().getAttribute("userInfo");
      return "Client1 Hello " + userInfo.getUsername();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    未登录时,通过网关访问其他微服务页面会重定向:
    在这里插入图片描述

    登录后,可以正常访问,并且在其他微服务中可以获取到session中的用户信息:
    在这里插入图片描述
    client-1:
    在这里插入图片描述
    server-1:
    在这里插入图片描述

    spring cloud gateway中session共享

    在spring cloud gateway中,和zuul有一些区别,下面整理了这些区别以及要如何修改。

    httpSession和webSession

    首先spring cloud gateway是基于webflux,是非阻塞的,zuul是基于servlet的,是阻塞的(这部分差异大家可以自行了解一下,我也不是很熟~)。他们的session是两种实现,在zuul中是httpSession,而到了gateway中是webSession。

    在gateway中需要将EnableRedisHttpSession注解换成EnableRedisWebSession:

    /**
     * 指定saveMode为ALWAYS 功能和flushMode类似
     *
     * @author yuanzhihao
     * @since 2022/5/6
     */
    @EnableRedisWebSession(saveMode = SaveMode.ALWAYS)
    @Configuration
    @Slf4j
    public class RedisSessionConfig {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    同时需要覆盖webSession中读取sessionId的写法,将SESSION信息进行base64解码,默认实现中是没有base64解码的,sessionId传到下游时不一致,会导致session不共享:

    // 覆盖默认实现
    @Bean
    public WebSessionIdResolver webSessionIdResolver() {
        return new CustomWebSessionIdResolver();
    }
    
    private static class CustomWebSessionIdResolver extends CookieWebSessionIdResolver {
        // 重写resolve方法 对SESSION进行base64解码
        @Override
        public List resolveSessionIds(ServerWebExchange exchange) {
            MultiValueMap cookieMap = exchange.getRequest().getCookies();
            // 获取SESSION
            List cookies = cookieMap.get(getCookieName());
            if (cookies == null) {
                return Collections.emptyList();
            }
            return cookies.stream().map(HttpCookie::getValue).map(this::base64Decode).collect(Collectors.toList());
        }
    
        private String base64Decode(String base64Value) {
            try {
                byte[] decodedCookieBytes = Base64.getDecoder().decode(base64Value);
                return new String(decodedCookieBytes);
            } catch (Exception ex) {
                log.debug("Unable to Base64 decode value: " + base64Value);
                return null;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    这边可以参考下具体的源码。httpSession在读取的时候,会进行解码,具体方法地址org.springframework.session.web.http.DefaultCookieSerializer#readCookieValues
    在这里插入图片描述

    添加context-path

    spring-cloud-gateway不是基于servlet的,所以设置了server.servlet.context-path属性并不生效,这边参考其他人的方案使用了另一种方法添加了context-path。使用StripPrefix的方式。StripPrefix的参数表示在进行路由转发到下游服务之前,剥离掉请求中StripPrefix参数个数的路径参数。比如StripPrefix为2,像网关发起的请求是/gateway/client1/name,转发到下游时,请求路径会变成/name,这样就添加完成了context-path。

    具体路由的配置信息如下:

    spring:
      application:
        name: gateway
      cloud:
        gateway:
          routes:
           - id: client1
             uri: lb://eureka-client1
             predicates:
               - Path=/gateway/client1/**
             filters:
               - StripPrefix=2
           - id: server1Session
             uri: lb://eureka-server1
             predicates:
               - Path=/gateway/server1/**
             filters:
               - StripPrefix=2
           - id: portal
             uri: lb://portal
             predicates:
               - Path=/gateway/portal/**
             filters:
               - StripPrefix=2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    到现在差不多就完成了gateway的session共享。

    gateway测试工程

    这边测试工程和上面一致,只是将网关换成了gateway。

    我们在gateway中添加一个登录过滤器拦截所有的请求,对于没有登录的请求跳转到登录页面:

    /**
     * 登录过滤器
     *
     * @author yuanzhihao
     * @since 2022/5/6
     */
    @Component
    @Slf4j
    public class LoginGlobalFilter implements GlobalFilter, Ordered {
        private static final List white_List = Arrays.asList("/login", "/logout");
    
        // 登录地址
        private static final String PORTAL_URL = "https://localhost:7885/gateway/portal/login";
    
        @SneakyThrows
        @Override
        public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            System.err.println("login filter starter");
            // 判断是否登录
            AtomicBoolean isLogin = new AtomicBoolean(false);
            exchange.getSession().subscribe(webSession -> {
                UserInfo userInfo = webSession.getAttribute("userInfo");
                System.err.println("userInfo is " + userInfo);
                if (userInfo != null) {
                    isLogin.set(true);
                }
            });
            // 这边添加一个延时, 等待获取到session
            Thread.sleep(200);
    
            // url白名单
            String path = exchange.getRequest().getURI().getPath();
            boolean isWhiteUrl = white_List.stream().anyMatch(path::endsWith);
    
            // 登录状态或者在url白名单中 放行
            if (isLogin.get() || isWhiteUrl) {
                return chain.filter(exchange);
            }
    
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.SEE_OTHER);
            response.getHeaders().set(HttpHeaders.LOCATION, PORTAL_URL);
            response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
            return response.setComplete();
        }
    
        @Override
        public int getOrder() {
            return -1;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    这边我添加了一个200ms的睡眠,因为测试验证的时候,当请求进入这个过滤器时,获取到的webSession是空,导致逻辑异常。猜测是由于spring-cloud-gateway是基于netty实现的非阻塞IO,所以获取session有一定的延迟,所有添加了一个sleep阻塞。后续会考虑修改。

    之前也尝试过使用block()方法修改为阻塞的,但是抛异常了,具体原因没有分析出来。

    这边通过gateway访问和zuul的结果一致:
    在这里插入图片描述
    在其他微服务中也可以获取到session中的用户信息:
    在这里插入图片描述在这里插入图片描述

    结语

    以上就是Spring Cloud Gateway中session共享的方案,在网络上相关的文章很少,如果大家有其他不错的方案,希望也可以分享一下。

    参考地址:

    https://stackoverflow.com/questions/50325674/spring-cloud-gateway-api-context-path-on-routes-not-working

    https://github.com/spring-cloud/spring-cloud-gateway/issues/1920

    https://www.shuzhiduo.com/A/QV5Zg2o2dy/

    代码地址:https://github.com/yzh19961031/SpringCloudDemo

    先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

  • 相关阅读:
    数据分析面试题(2023.09.08)
    102. 管道漫游案例
    [libevent:构建高性能事件驱动应用的利器]
    Word控件Spire.Doc 【图像形状】教程(6): 如何在 C#、VB.NET 的 Word 文档中插入形状和形状组
    Elasticsearch 7.X版本常用语法语句
    创建线程的几种方式
    Spring Cache代理对象 redis篇
    【C++项目】boost搜索引擎项目
    【pytorch】目标检测:一文搞懂如何利用kaggle训练yolov5模型
    Redis单线程
  • 原文地址:https://blog.csdn.net/asdfadafd/article/details/126114124