引言
本周,大部分时间去撰写毕业设计中期报告,在部署Alice
学生管理系统测试环境时想起本系统借助Redis
实现分布式Session
。
为什么要分布式Session
呢?
请参考下图:
当后台集群部署时,单机的Session
维护就会出现问题。
假设登录的认证授权发生在Tomcat A
服务器上,Tomcat A
在本地存储了用户Session
,并签发认证令牌,用于验证用户身份。
下次请求可能分发给Tomcat B
服务器,而Tomcat B
并没有用户Session
,用户携带的认证令牌无效,得到401
。
除了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
原理之时,我们从官方文档中找到了这样一张图。
所有的认证授权拦截都是基于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
,使用起来简单,但需要额外的存储空间。
实际应用中,要兼顾当前的业务场景与安全性进行方案的选择。
丹墀(chí)对策三千字,金榜题名五色春。
预祝黄庭祥考研顺利,期待着下一个春天的好消息。