SpringCloud开启session共享并存储到Redis的实现
作者:一枝花傲寒
备注:以下所有的gateway均指SpringCloud Gateway
一、原架构
前端<->gateway<->console后端
原来session是在console-access中维护的,当中间有了一层gateway之后,gateway会认为session变了,从而将session的cookie信息重置,导致无法在前端的后续请求无法将cookie带上来
如下图所示的spring-web的代码中这个state会变成State.NEW而非State.STARTED
在这种情况下,部署的时候只有跳过gateway才能正常进行
即按照如下方式才能进行session的判断
前端<->console后端
二、调整架构以及相应的代码
整个业务处理和原来的没有任何改变
将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
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")); customWebSessionIdResolver.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设置 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); 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:!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/; } }
前端页面访问https://ip:38091/ccuconsole
所有的请求都通过该同域的ip和38092转发到http://127.0.0.1:30000对应的服务(确保该服务中有无/ccuconsole开头的路径并不影响,但是为了可切换同域部署,不推荐存在/ccuconsole开头的路径)中
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
您可能感兴趣的文章:
- SpringCloud Hystrix熔断器使用方法介绍
- SpringCloud Gateway动态路由配置详解
- Spring Cloud Gateway远程命令执行漏洞分析(CVE-2022-22947)
- springcloud整合openfeign使用实例详解
- Spring Cloud Alibaba实现服务的无损下线功能(案例讲解)
- springcloud-gateway集成knife4j的示例详解
- Spring Cloud Alibaba 整合Nacos的详细使用教程
- Spring Cloud Ribbon 负载均衡使用策略示例详解
- SpringCloud @RefreshScope刷新机制深入探究
- SpringCloud @RefreshScope刷新机制浅析
- SpringCloud启动失败问题汇总
- 一文吃透Spring Cloud gateway自定义错误处理Handler
- SpringCloud Gateway路由组件详解
- SpringCloud OpenFeign基本介绍与实现示例
- Spring Cloud Gateway替代zuul作为API网关的方法
- SpringCloud使用Feign实现远程调用流程详细介绍
- SpringCloud修改Feign日志记录级别过程浅析
- Spring Cloud原理以及核心组件详解