开发者

SpringCloud开启session共享并存储到Redis的实现

开发者 https://www.devze.com 2023-02-15 10:26 出处:网络 作者: 一枝花傲寒
目录一、原架构二、调整架构以及相应的代码1、Redis和session的配置2、增加配置类3、应答过滤器增加session设置4、增加控制台处理的过滤器ConsoleFilter5、前端请求中增加(跨域时)三、部署模式1、同域2、跨域总结备
目录
  • 一、原架构
  • 二、调整架构以及相应的代码
    • 1、Redis和session的配置
    • 2、增加配置类
    • 3、应答过滤器增加session设置
    • 4、增加控制台处理的过滤器ConsoleFilter
    • 5、前端请求中增加(跨域时)
  • 三、部署模式
    • 1、同域
    • 2、跨域
  • 总结

    备注:以下所有的gateway均指SpringCloud Gateway

    一、原架构

    前端<->gateway<->console后端

    原来session是在console-Access中维护的,当中间有了一层gateway之后,gateway会认为session变了,从而将session的cookie信息重置,导致无法在前端的后续请求无法将cookie带上来

    如下图所示的spring-web的代码中这个state会变成State.NEW而非State.STARTED

    SpringCloud开启session共享并存储到Redis的实现

    在这种情况下,部署的时候只有跳过gateway才能正常进行

    即按照如下方式才能进行session的判断

    前端<->console后端

    二、调整架构以及相应的代码

    SpringCloud开启session共享并存储到Redis的实现

    整个业务处理和原来的没有任何改变

    将session的判断控制挪到gateway当中

    首先将console后端中登录以及后续业务当中涉及到session处理的部分都删除

    然后开始改造gateway

    1、Redis和session的配置

    gateway的pom.XML增加

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    

    因为需要依赖Redis了,所以启动类当中删除RedisAutoConfiguration.class

    SpringCloud开启session共享并存储到Redis的实现

    Nacos或者配置文件增加redis配置

    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    spring.redis.username=redis7
    spring.redis.password=XXX
    spring.redis.database=10
    spring.redis.pool.max-active=100
    spring.redis.pool.max-wait=500
    spring.redis.pool.max-idle=10
    spring.redis.pool.min-idle=10
    spring.redis.timeout=1000
    

    2、增加配置类

    /**
     * 指定saveMode为ALWAYS 功能和flushMode类似
     *
     * @author fengwei
     * @since 2022/11/8
     */
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.HttpCookie;
    import org.springframework.session.SaveMode;
    import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession;
    import org.springframework.util.MultiValueMap;
    import org.springframework.web.server.ServerWebExchange;
    import org.springframework.web.server.session.CookieWebSessionIdResolver;
    import org.springframework.web.server.session.WebSessionIdResolver;
    
    import Java.util.Base64;
    import java.util.Collections;
    import java.util.List;
    import java.util.stream.Collectors;
    
    @EnableRedisWebSession(saveMode = SaveMode.ALWAYS, maxInactiveIntervalInSeconds = 1200)
    @Configuration
    @Slf4j
    public class RedisSessionConfig {
        /**
         * @return
         * @reference https://docs.spring.io/spring-session/reference/guides/boot-webflux-custom-cookie.html
         */
        @Bean
        public WebSessionIdResolver webSessionIdResolver() {
            CustomWebSessionIdResolver customWebSessionIdResolver = new CustomWebSessionIdResolver();
            //以下四项配置主要用于跨域调用让客户端处理cookie信息;若同域调用,下面四行可删除
            customWebSessionIdResolver.addCookieInitializer((builder) -> builder.httpOnly(true));
            customWebSessionIdResolver.addCookieInitializer((builder) -> builder.path("/"));
            customWebSessionIdResolver.addCookieInitializer((builder) -> builder.sameSite("None"));
    www.devze.com        customWGaiORVoebSessionIdResolver.addCookieInitializer((builder) -> builder.secure(true));
            return customWebSessionIdResolver;
        }
    
        private static class CustomWebSessionIdResolver extends CookieWebSessionIdResolver {
            // 重写resolve方法 对SESSION进行base64解码
            @Override
            public List<String> resolveSessionIds(ServerWebExchange exchange) {
                MultiValueMap<String, HttpCookie> cookieMap = exchange.getRequest().getCookies();
                // 获取SESSION
                List<HttpCookie> 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);
                    String decodedCookieString = new String(decodedCookieBytes);
                    log.debug("base64Value:{}, decodedCookieString:{} ", base64Value, decodedCookieString);
                    return decodedCookieString;
                } catch (Exception ex) {
                    //如果转不了base64,就认为原始值就是返回的
                    log.debug("Unable to Base64 decode value:{} ", base64Value);
                    return base64Value;
                }
            }
        }
    
    }
    

    3、应答过滤器增加session设置

    在ResponseLogFilter类中增加

    /*如果是控制台登录,则从里面取出securityRandom存在websession里面*/
    if (request.getPath().toString().startsWith("/console/access/user/login")) {
        jsONObject jsonObject = JSONObject.parseobject(finalResponseBody);
        if ("0000".equals(jsonObject.getString("result"))) {
            JSONObject jsonObjectData = (JSONObject) jsonObject.get("data");
            String securityRandom = (String) jsonObjectData.get("securityrandom");
            exchange.getSession().subscribe(webSession -> {
                webSession.getAttributes().put("securityrandom", securityRandom);
            });
            try {
                //给200毫秒让进行session开发者_JS教程设置
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    

    4、增加控制台处理的过滤器ConsoleFilter

    首选在配置文件或者Nacos增加配置项

    #Y标识需要检查session;N表示不检查session。开发的时候可以配置为N
    websession.ifcheck=Y

    过滤器ConsoleFilter

    import com.alibaba.cloud.commons.lang.StringUtils;
    import com.jieyi.util.OrderedConstant;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    
    import java.nio.charset.StandardCharsets;
    import java.util.Arrays;
    import java.util.List;
    
    /**
     * 控制台登录的过滤器,主要是拿用户凭证的securityRandom
     *
     * @author fengwei
     * @date 2022-11-8
     */
    @Component
    @Slf4j
    public class ConsoleFilter implements GlobalFilter, Ordered {
        private static final List<String> WHITE_LIST = Arrays.asList("/console/access/user/getOtp/V1", "/console/access/user/login/V1");
    
        @Value("${websession.ifcheck}")
        private String websessionIfcheck;
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getPath().toString();
    
    
            //不校验session就直接通过了
            if (!"Y".equals(websessionIfcheck)) {
                return chain.filter(exchange);
            }
            //在url白名单中放行
            boolean isWhiteUrl = WHITE_LIST.stream().anyMatch(path::endsWith);
            if (isWhiteUrl) {
                return chain.filter(exchange);
            }
    
            if (path.startsWith("/console")) {
                return exchange.getSession().flatMap(webSession -> {
                    String securityrandomInSession = webSession.getAttribute("securityrandom");
                    log.info("securityrandomInSession:{}", securityrandomInSession);
                    if (StringUtils.isEmpty(securityrandomInSession)) {
                        byte[] bytes = "{\"status\":\"401\",\"msg\":\"Not login or login timeout\"}".getBytes(StandardCharsets.UTF_8);
               GaiORVo         DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
                        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                        return exchange.getResponse().writeWith(Flux.just(buffer));
                    }
                    return chain.filter(exchange);
                });
    
            } else {
                return chain.filter(exchange);
            }
    
        }
    
        @Override
        public int getOrder() {
            return OrderedConstant.LOGGING_FILTER;
        }
    }
    
    

    5、前端请求中增加(跨域时)

    withCredentials = true

    只有增加这个请求才能携带和处理cookie

    三、部署模式

    1、同域

    对于同域的部署http和https均可(当然更建议https)

    提供一个nginx同域部署的参考:

    server {
      #console-samedomain-test
      listen 38093 ssl;
      proxy_set_header Host $host:38093;
      root html;
      index index.html index.htm;
      ssl_certificate      cert/server.crt;
      ssl_certificate_key  cert/server.key;
      ssl_session_timeout  5m;
      ssl_ciphers  ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
      ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
      ssl_prefer_server_ciphers  on;
      location / {
        proxy_pass http://127.0.0.1:30000/;
      }
      location /ccuconsole {
        proxy_pass http://127.0.0.1:5120/ccuconsole;
      }
    }
    

    该配置为https,走的38093端口

    前端页面访问https://ip:38093/ccuconsole

    所有的请求都通过该同域的ip和端口转发到http://127.0.0.1:30000对应的服务(确保该服务中不存在/ccuconsole开头的路径)中

    2、跨域

    对于跨域的部必须使用https(现在的浏览器版本的要求,浏览器已不再支持http的跨域了)

    提供一个nginx跨域部署的参考:

    server {
      #console-web-crossdomain-test
      listen 38091 ssl;
      proxy_set_header Host $host:38091;
      root html;
      index index.html index.htm;
      ssl_certificate      cert/server.crt;
      ssl_certificate_key  cert/server.key;
      ssl_session_timeout  5m;
      ssl_ciphers  ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
      ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
      ssl_prefer_server_ciphers  on;
      location /ccuconsole {
        proxy_pass http://127.0.0.1:5120/ccuconsole;
      }
    }
    
    server {
      #console-crossdomain-test
      listen 38092 ssl;
      proxy_set_header Host $host:38092;
      root html;
      index index.html index.htm;
      ssl_certificate      cert/server.crt;
      ssl_certificate_key  cert/server.key;
      ssl_session_timeout  5m;
      ssl_ciphers  ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULandroidL:!MD5:!ADH:!RC4;
      ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
      ssl_prefer_server_ciphers  on;
      location / {
        proxy_pass http:www.devze.com//127.0.0.1:30000/;
      }
    }
    

    前端页面访问https://ip:38091/ccuconsole

    所有的请求都通过该同域的ip和38092转发到http://127.0.0.1:30000对应的服务(确保该服务中有无/ccuconsole开头的路径并不影响,但是为了可切换同域部署,不推荐存在/ccuconsole开头的路径)中

    总结

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号