springsession分布式登录被覆盖_Spring Session 原理分析

分布式环境下,单机Session维护会遇到问题。SpringSession通过Filter实现Session存储环境的透明切换,利用Redis等存储来解决Session共享。它装饰HttpSession,当Session失效时删除存储信息,请求结束后持久化Session。分布式Session与JWT是两种常见的认证方案,各有优缺点,适用于不同的业务场景。
摘要由CSDN通过智能技术生成

为什么要分布式 Session 呢?

请参考下图:

73d3417d4bb12bf91b17f9e5894833b6.png

当后台集群部署时,单机的 Session 维护就会出现问题。

假设登录的认证授权发生在 Tomcat A 服务器上, Tomcat A 在本地存储了用户 Session ,并签发认证令牌,用于验证用户身份。

下次请求可能分发给 Tomcat B 服务器,而 Tomcat B 并没有用户 Session ,用户携带的认证令牌无效,得到 401 。

30406f367a297c446c2e4cd540b640b0.png

除了 JWT 无状态的认证方式,另一种主流的实现方案就是采用分布式 Session 。

public interface HttpSession {    public void setAttribute(String name, Object value);}

HttpSession 内的存储就是 name 与 value 的键值对映射,且存在过期时间,这与 Redis 的设计相符合,分布式 Session 通常使用 Redis 进行实现。

无论是在单机环境,还是在引入了 Spring Session 的集群环境下,代码实现都是相同的,即屏蔽了底层的细节,可以在不改动 HttpSession 使用的相关代码的情况下,实现 Session 存储环境的切换。

logger.debug("记录当前用户ID");httpSession.setAttribute(UserService.USER_ID, persistUser.getId());

这听起来很酷,那么 Spring Session 具体是如何在不改动代码的情况下进行 Session 存储环境切换的呢?

原理

官方文档: How HttpSession Integration Works - Spring Session

回顾

之前在学习 Spring Security 原理之时,我们从官方文档中找到了这样一张图。

bdf638b0f13f5ef209bae6efdffe3047.png

所有的认证授权拦截都是基于 Filter 实现的,而这里的 Spring Session ,也是基于 Filter 。

原理分析

因为 HttpSession 和 HttpServletRequest (获取 HttpSession 的 API )都是接口,这意味着可以将这些 API 替换成自定义的实现。

核心源码如下:

注:以下代码中部分无关代码已被删减。
public class SessionRepositoryFilter extends OncePerRequestFilter {  @Override  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {    /** 替换 request */    SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response, this.servletContext);    /** 替换 response */    SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response);    /** try-finally,finally 必定执行 */    try {      /** 执行后续过滤器链 */      filterChain.doFilter(wrappedRequest, wrappedResponse);    } finally {      /** 后续过滤器链执行完毕,提交 session,用于存储 session 信息并返回 set-cookie 信息 */      wrappedRequest.commitSession();    }  }}

response 封装器核心源码如下:

private final class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper {  SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request, HttpServletResponse response) {    super(response);    this.request = request;  }  @Override  protected void onResponseCommitted() {    /** response 提交后提交 session */    this.request.commitSession();  }}

request 封装器核心源码如下:

private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {  private SessionRepositoryRequestWrapper(HttpServletRequest request, HttpServletResponse response, ServletContext servletContext) {    super(request);    this.response = response;    this.servletContext = servletContext;  }  /**   * 将 sessionId 写入 reponse,并持久化 session   */  private void commitSession() {    /** 获取当前 session 信息 */    S session = getCurrentSession().getSession();    /** 持久化 session */    SessionRepositoryFilter.this.sessionRepository.save(session);    /** reponse 写入 sessionId */    SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, session.getId());  }  /**   * 重写 HttpServletRequest 的 getSession 方法   */  @Override  public HttpSessionWrapper getSession(boolean create) {    /** 从持久化中查询 session */    S requestedSession = getRequestedSession();    /** session 存在,直接返回 */    if (requestedSession != null) {      currentSession = new HttpSessionWrapper(requestedSession, getServletContext());      currentSession.setNew(false);      return currentSession;    }    /** 设置不创建,返回空 */    if (!create) {      return null;    }    /** 创建 session 并返回 */    S session = SessionRepositoryFilter.this.sessionRepository.createSession();    currentSession = new HttpSessionWrapper(session, getServletContext());    return currentSession;  }  /**   * 从 repository 查询 session   */  private S getRequestedSession() {    /** 查询 sessionId 信息 */    List sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);    /** 遍历查询 */    for (String sessionId : sessionIds) {      S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId);      if (session != null) {        this.requestedSession = session;        break;      }    }    /** 返回持久化 session */    return this.requestedSession;  }  /**   * http session 包装器   */  private final class HttpSessionWrapper extends HttpSessionAdapter {    HttpSessionWrapper(S session, ServletContext servletContext) {      super(session, servletContext);    }    @Override    public void invalidate() {      super.invalidate();      /** session 不合法,从存储中删除信息 */      SessionRepositoryFilter.this.sessionRepository.deleteById(getId());    }  }}

原理简单,装饰 HttpSession , Session 失效时从存储中删除,在请求结束之后,存储 session 。

总结

分布式环境下的认证方案: JWT 与分布式 Session 。

个人觉得两种方案都很好, JWT ,无状态,服务器不用维护 Session 信息,但如何让 JWT 失效是一个难题。

分布式 Session ,使用起来简单,但需要额外的存储空间。

实际应用中,要兼顾当前的业务场景与安全性进行方案的选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值