前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >微服务网关与用户身份识别,服务提供者之间的会话共享关系

微服务网关与用户身份识别,服务提供者之间的会话共享关系

作者头像
愿天堂没有BUG
发布2022-10-28 11:40:22
6540
发布2022-10-28 11:40:22
举报
文章被收录于专栏:愿天堂没有BUG(公众号同名)

服务提供者之间的会话共享关系

一套分布式微服务集群可能会运行几个或者几十个网关(gateway),以及几十个甚至几百个Provider微服务提供者。如果集群的节点规模较小,那么在会话共享关系上,同一个用户在所有的网关和微服务提供者之间共享同一个分布式Session是可行的,如图6-8所示。

图6-8 共享分布式Session

如果集群的节点规模较大,分布式Session在IO上就会存在性能瓶颈。除此之外,还存在一个架构设计上的问题:在网关(如Zuul)和微服务提供者之间传递Session ID,并且双方依赖了相同的会话信息(如用户详细信息),将导致网关和微服务提供者、微服务提供者与微服务提供者之间的耦合度很高,这在一定程度上降低了微服务的移植性和复用性,违背了系统架构高内聚、低耦合的原则。

架构的调整方案是:缩小分布式Session的共享规模,网关(如Zuul)和微服务提供者之间按需共享分布式Session。网关和微服务提供者不再直接传递Session ID作为用户身份标识,而是改成传递用户ID,如图6-9所示。

图6-9 Session共享的架构与实现方案

以上介绍的Session共享的架构,第一种可理解为全局共享,第二种可理解为局部按需共享。无论如何,Session共享的架构与实现方案肯定不止以上两种,而且以上第二种方案也不一定是最优的。疯狂创客圈的crazy-springcloud脚手架对上面的第二种分布式Session架构方案提供了实现代码,供大家参考和学习。

分布式Session的起源和实现方案

HTTP本身是一种无状态的协议,这就意味着每一次请求都需要进行用户的身份信息查询,并且需要用户提供用户名和密码来进行用户认证。为什么呢?服务端并不知道是哪个用户发出的请求。所以,为了能识别是哪个用户发出的请求,需要在服务端存储一份用户身份信息,并且在登录成功后将用户身份信息的标识传递给客户端,客户端保存好用户身份标识,在下次请求时带上该身份标识。然后,在服务端维护一个用户的会话,用户的身份信息保存在会话中。通常,对于传统的单体架构服务器,会话都是保存在内存中的,而随着认证用户增多,服务端的开销会明显增大。

大家都知道,单体架构模式最大的问题是没有分布式架构,无法支持横向扩展。在分布式微服务架构下,需要在服务节点之间进行会话的共享。解决方案是使用一个统一的Session数据库来保存会话数据并实现共享。当然,这种Session数据库一定不能是重量级的关系型数据库,而应该是轻量级的基于内存的高速数据库(如Redis)。

在生产场景中,可以使用成熟稳定的Spring Session开源组件作为分布式Session的解决方案,不过Spring Session开源组件比较重,在简单的Session共享场景中可以自己实现一套相对简单的RedisSession组件,具体的实现方案可以参考疯狂创客圈的社群博客“RedisSession自定义”一文。从学习角度来说,自制一套RedisSession方案可以帮助大家深入了解Web请求的处理流程,使得大家更容易学习Spring Session的核心原理。

Spring Session作为独立的组件将Session从Web容器中剥离,存储在独立的数据库中,目前支持多种形式的数据库:内存数据库(如Redis)、关系型数据库(如MySQL)、文档型数据库(如MogonDB)等。通过合理的配置,当请求进入Web容器时,Web容器将Session的管理责任委托给Spring Session,由Spring Session负责从数据库中存取Session,若其存在,则返回,若其不存在,则新建并持久化至数据库中。

Spring Session的核心组件和存储细节

这里先介绍Spring Session的3个核心组件:Session接口、RedisSession会话类、SessionRepository存储接口。

1.Session接口

Spring Session单独抽象出Session接口,该接口是SpringSession对会话的抽象,主要是为了鉴定用户,为HTTP请求和响应提供上下文容器。Session接口的主要方法如下:

(1)getId:获取Session ID。

(2)setAttribute:设置会话属性。

(3)getAttribte:获取会话属性。

(4)setLastAccessedTime:设置会话过程中最近的访问时间。

(5)getLastAccessedTime:获取最近的访问时间。

(6) setMaxInactiveIntervalInSeconds:设置会话的最大闲置时间。

(7) getMaxInactiveIntervalInSeconds:获取最大闲置时间。

(8)isExpired:判断会话是否过期。

Spring Session和Tomcat的Session在实现模式上有很大不同,Tomcat中直接实现Servlet规范的HttpSession接口,而SpringSession中则抽象出单独的Session接口。问题是:Spring Session如何处理自己定义的Session接口和Servlet规范的HttpSession接口的关系呢?Spring Session定义了一个适配器类,可以将Session实例适配成Servlet规范中的HttpSession实例。

Spring Session之所以要单独抽象出Session接口,主要是为了应对多种传输、存储场景下的会话管理,比如HTTP会话场景(HttpSession)、WebSocket会话场景(WebSocket Session)、非Web会话场景(如Netty传输会话)、Redis存储场景(RedisSession)等。

2.RedisSession会话类

RedisSession用于使用Redis进行会话属性存储的场景。在RedisSession中有两个非常重要的成员属性,分别说明如下:

(1)cached:实际上是一个MapSession实例,用于进行本地缓存,每次在进行getAttribute操作时优先从本地缓存获取,没有取到再从Redis中获取,以提升性能。而MapSession是由Spring SecurityCore定义的一个通过内部的HashMap缓存键-值对的本地缓存类。

(2)delta:用于跟踪变化数据,目的是保存变化的Session的属性。

RedisSession提供了一个非常重要的saveDelta方法,用于持久化Session至Redis中:当调用RedisSession中的saveDelta方法后,变化的属性将被持久化到Redis中。

3.SessionRepository存储接口

SessionRepository为管理Spring Session的存储接口,主要的方法如下:

(1)createSession:创建Session实例。

(2)findById(String id):根据id查找Session实例。

(3)void delete(String id):根据id删除Session实例。

(4)save(S session):存储Session实例。

根据Session的实现类不同,Session存储实现类分为很多种。

RedisSession会话的存储类为 RedisOperationsSessionRepository,由其负责Session数据到Redis数据库的读写。

接下来简单看一下Redis中的Session数据存储细节。

RedisSession在Redis缓存中的存储细节大致有3种Key(根据版本不同可能不完全一致),分别如下:

代码语言:javascript
复制
spring:session:SESSION_KEY:sessions:0cefe354-3c24-40d8-a859-fe7d9d3c0dbaspring:session:SESSION_KEY:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fespring:session:SESSION_KEY:expirations:1581695640000

第一种Key(键)的Value(值)用来存储Session的详细信息,Key的最后部分为Session ID,这是一个UUID。这个Key的Value在Redis中是一个hash类型,内容包括Session的过期时间间隔、最近的访问时间、属性等。Key的过期时间为Session的最大过期时间+5分钟。如果设置的Session过期时间为30分钟,那么这个Key的过期时间为35分钟。第二种Key用来表示Session在Redis中已经过期,这个键-值对不存储任何有用数据,只是为了表示Session过期而设置。

第三种Key存储过去一段时间内过期的Session ID集合。这个Key的最后部分是一个时间戳,代表计时的起始时间。这个Key的Value所使用的Redis数据结构是set,set中的元素是时间戳滚动至下一分钟计算得出的过期Session Key(第二种Key)。

Spring Session的使用和定制

结合Redis使用Spring Session需要导入以下两个Maven依赖包:

代码语言:javascript
复制
 <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-core</artifactId> </dependency>

按照Spring Session官方文档的说明,在添加所需的依赖项后,可以通过以下配置启用基于Redis的分布式Session:

代码语言:javascript
复制
@EnableRedisHttpSessionpublic class Config { //创建一个连接到默认Redis (localhost:6379)的RedisConnectionFactory @Bean public LettuceConnectionFactory connectionFactory() { return new LettuceConnectionFactory(); }}

@EnableRedisHttpSession注释创建一个名为 springSessionRepositoryFilter的过滤器,它负责将原始的HttpSession替换为RedisSession。为了使用Redis数据库,这里还创建了一个连接Spring Session到Redis服务器的RedisConnectionFactory实例,该实例连接的默认为Redis,主机和端口分别为localhost和6379。有关Spring Session的具体配置可参阅参考文档,地址为

代码语言:javascript
复制
https://www.springcloud.cc/spring-session.html。

在crazy-springcloud脚手架的共享Session架构中,网关和微服务提供者之间、微服务提供者和微服务提供者之间所传递的不是SessionID而是User ID,所以目标Provider收到请求之后,需要通过User ID找到Session ID,然后找到RedisSession,最后从Session中加载缓存数据。整个流程需要定制3个过滤器,如图6-10所示。

图6-10 crazy-springcloud脚手架共享Session架构中的过滤器

第一个过滤器叫作SessionIdFilter,其作用是根据请求头中的用户身份标识User ID定位到分布式会话的Session ID。

第二个过滤器叫作 CustomedSessionRepositoryFilter,这个类的源码来自Spring Session,其主要的逻辑是将request(请求)和response(响应)进行包装,将HttpSession替换成RedisSession。

第三个过滤器叫作SessionDataLoadFilter,其判断RedisSession中的用户数据是否存在,如果是首次创建的Session,就从数据库中将常用的用户数据加载到Session,以便控制层的业务逻辑代码能够被高速访问。

在crazy-springcloud脚手架中,按照高度复用的原则,所有和会话有关的代码都封装在base-session基础模块中。如果某个Provider模块需要用到分布式Session,只需要在Maven中引入base-session模块依赖即可。

通过用户身份标识查找Session ID

通过用户身份标识(User ID)查找Session ID的工作是由SessionIdFilter过滤器完成的。在前面介绍的UAA提供者服务(crazymakeruaa)中,用户的User ID和Session ID之间的绑定关系位于缓存Redis中。

base-session借鉴了同样的思路。当带着User ID的请求进来时,SessionIdFilter会根据User ID去Redis查找绑定的Session ID。如果查找成功,那么过滤器的任务完成;如果查找不成功,后面的两个过滤器就会创建新的RedisSession,并将在Redis中缓存User ID和Session ID之间的绑定关系。

SessionIdFilter的代码如下:

代码语言:javascript
复制
package com.crazymaker.springcloud.base.filter;//省略import@Slf4jpublic class SessionIdFilter extends OncePerRequestFilter{ public SessionIdFilter(RedisRepository redisRepository, RedisOperationsSessionRepository sessionRepository) { this.redisRepository = redisRepository; this.sessionRepository = sessionRepository; } /** *RedisSession DAO */ private RedisOperationsSessionRepository sessionRepository; /** *Redis DAO */ RedisRepository redisRepository; /** *返回true代表不执行过滤器,false代表执行 */ @Override protected boolean shouldNotFilter(HttpServletRequest request) { String userIdentifier = request.getHeader(SessionConstants.USER_IDENTIFIER); if (StringUtils.isNotEmpty(userIdentifier)) { return false; } return true; } /** *将session userIdentifier(用户id)转成session id * *@param request请求 *@param response响应 *@param chain过滤器链 */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { /** *从请求头中获取session userIdentifier(用户id) */ String userIdentifier = request.getHeader(SessionConstants.USER_IDENTIFIER); SessionHolder.setUserIdentifer(userIdentifier); /** *在Redis中,根据用户id获取缓存的session id */ String sid = redisRepository.getSessionId(userIdentifier); if (StringUtils.isNotEmpty(sid)) { /** *判断分布式Session是否存在 */ Session session = sessionRepository.findById(sid); if (null != session) { //保存session id线程局部变量,供后面的过滤器使用 SessionHolder.setSid(sid); } } chain.doFilter(request, response); }}

SessionIdFilter过滤器中含有两个DAO层的成员:一个RedisRepository类型的DAO成员,负责根据User ID去Redis查找绑定的Session ID;另一个DAO成员的类型为Spring Session专用的 RedisOperationsSessionRepository,负责根据Session ID去查找RedisSession实例,用于验证Session是否真正存在。

查找或创建分布式Session

SessionIdFilter过滤处理完成后,请求将进入下一个过滤器 CustomedSessionRepositoryFilter。这个类的源码来自Spring Session,其主要的逻辑是将request(请求)和response(响应)进行包装,并将原始请求的HttpSession替换成RedisSession。定制之后的过滤器稍微做了一点过滤条件的修改:如果请求头中携带了用户身份标识,就开启分布式Session,否则不会进入分布式Session的处理流程。

CustomedSessionRepositoryFilter的部分代码如下:

代码语言:javascript
复制
package com.crazymaker.springcloud.base.filter;//省略importpublic class CustomedSessionRepositoryFilter<S extends Session> extends OncePerRequestFilter{ //执行过滤 @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ... //包装上一个过滤器的HttpServletRequest请求至SessionRepositoryRequestWrapper SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response, this.servletContext); //包装上一个过滤器的HttpServletResponse响应至SessionRepositoryResponseWrapper SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response); try { filterChain.doFilter(wrappedRequest, wrappedResponse); } finally { //会话持久化到数据库 wrappedRequest.commitSession(); } } /** *返回true代表不执行过滤器,false代表执行 */ @Override protected boolean shouldNotFilter(HttpServletRequest request) { //如果请求中携带了用户身份标识 if (null == SessionHolder.getUserIdentifer()) { return true; } return false; } ...}

SessionRepositoryFilter首先会根据一个sessionIds清单进行Session查找,查找失败才创建新的RedisSession。它会调用CustomedSessionIdResolver实例的resolveSessionIds方法获取sessionIds清单。

作为Session ID的解析器,CustomedSessionIdResolver的部分代码如下:

代码语言:javascript
复制
package com.crazymaker.springcloud.base.core;...@Datapublic class CustomedSessionIdResolver implements HttpSessionIdResolver{ ... /** *解析session id,用于在Redis中进行Session查找 *@param request请求 *@return session id列表 */ @Override public List<String> resolveSessionIds(HttpServletRequest request) { //获取第一个过滤器保存的session id String sid = SessionHolder.getSid(); return (sid != null) ? Collections.singletonList(sid) : Collections.emptyList(); }...}

CustomedSessionRepositoryFilter会对sessionIds清单进行判断,然后根据结果进行分布式Session的查找或创建:

(1)如果清单中的某个Session ID对应的Session存在于Redis,过滤器就会将分布式RedisSession查找出来作为当前Session。

(2)如果清单为空,或者所有Session ID对应的RedisSession都不在于Redis,过滤器就会创建一个新的RedisSession。

加载高速访问数据到分布式Session

CustomedSessionRepositoryFilter处理完成后,请求将进入下一个过滤器SessionDataLoadFilter。这个类的主要逻辑是加载需要高速访问的数据到分布式Session,具体如下:

(1)获取前面的SessionIdFilter过滤器加载的Session ID,用于判断Session ID是否变化。如果变化就表明旧的Session不存在或者旧的Session ID已经过期,需要更新Session ID,并且在Redis中进行缓存。

(2)获取前面的 CustomedSessionRepositoryFilter创建的Session,如果是新创建的Session,就加载必要的需要高速访问的数据,以提高后续操作的性能。

需要高速访问的数据比较常见的有用户的基础信息、角色、权限等,还有一些基础的业务信息。

CustomedSessionRepositoryFilter的部分代码如下:

代码语言:javascript
复制
package com.crazymaker.springcloud.base.filter;...@Slf4jpublic class SessionDataLoadFilter extends OncePerRequestFilter{ UserLoadService userLoadService; RedisRepository redisRepository; public SessionDataLoadFilter(UserLoadService userLoadService, RedisRepository redisRepository) { this.userLoadService = userLoadService; this.redisRepository = redisRepository; }... @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //获取前面的SessionIdFilter过滤器加载的session id String sid = SessionHolder.getSid(); //获取前面的CustomedSessionRepositoryFilter创建的session,加载必要的数据到session HttpSession session = request.getSession(); /** *之前的session不存在 */ if (StringUtils.isEmpty(sid) || !sid.equals(request.getSession().getId())) { //取得当前的session id sid = session.getId(); //user id和session id作为键-值保存到redis redisRepository.setSessionId(SessionHolder.getUserIdentifier(), sid); SessionHolder.setSid(sid); } /** *获取session中的用户信息为空表示用户第次发起请求加载用户信息到中 *为空表示用户第一次发起请求,加载用户信息到session中 */ if (null == session.getAttribute(G_USER)) { String uid = SessionHolder.getUserIdentifier(); UserDTO userDTO = null; if (SessionHolder.getSessionIDStore().equals(SessionConstants.SESSION_STORE)) { //用户端:装载用户端的用户信息 userDTO = userLoadService.loadFrontEndUser(Long.valueOf(uid)); } else { //管理控制台:装载管理控制台的用户信息 userDTO = userLoadService.loadBackEndUser(Long.valueOf(uid)); } /** *将用户信息缓存起来 */ session.setAttribute(G_USER, JsonUtil.pojoToJson(userDTO)); } /** *将session请求保存到SessionHolder的ThreadLocal本地变量中,方便统一获取 */ SessionHolder.setSession(session); SessionHolder.setRequest(request); filterChain.doFilter(request, response); } /** *返回true代表不执行过滤器,false代表执行 */ @Override protected boolean shouldNotFilter(HttpServletRequest request) { if (null == SessionHolder.getUserIdentifier()) { return true; } return false; }}

本文给大家讲解的内容是 微服务网关与用户身份识别,服务提供者之间的会话共享关系

  1. 下篇文章给大家讲解的是 Nginx/OpenResty详解,Nginx简介;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!

本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-08-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 愿天堂没有BUG 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 服务提供者之间的会话共享关系
  • 分布式Session的起源和实现方案
  • Spring Session的核心组件和存储细节
  • Spring Session的使用和定制
  • 通过用户身份标识查找Session ID
  • 查找或创建分布式Session
  • 加载高速访问数据到分布式Session
  • 本文给大家讲解的内容是 微服务网关与用户身份识别,服务提供者之间的会话共享关系
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档