自定义实现session持久化

自定义实现session持久化

使用场景

对于有登陆校验的网站,tomcat 重启以后,刷新页面又得从新登陆,影响用户体验.java

缘由:
tomcat 的session 在内存中,tomcat重启以后,内存中的session就销毁了.致使登陆信息丢失git

session持久化的目的

对于存储在session中的信息,服务器重启以后,不会丢失. 好比用户登陆以后,重启tomcat服务器,刷新页面,依然是登陆状态.github

目标

  1. 从新实现session的经常使用操做,如 getAttribute(String s) , setAttribute(String s, Object o) , removeAttribute(String s) ;
  2. 编写业务代码时无侵入,也就是说,实际编写业务代码时不用使用专门的API,仍然像以前同样操做HttpSession.

思路

  1. 增长过滤器 javax.servlet.Filter的实现类;
  2. 在请求以前包装 HttpServletRequest,例如包装类是SessionSyncRequestWrapper;
  3. 业务代码中调用 getSession 时,就会调用包装类SessionSyncRequestWrapper 的 getSession, 咱们只要在包装类SessionSyncRequestWrapper 中,重写getSession 便可.
  4. 自定义HttpSession 的包装类 CustomSharedHttpSession,继承HttpSession,
    在CustomSharedHttpSession 中重写HttpSession的方法

实现方案

1. 自定义过滤器RequestbodyFilter,实现javax.servlet.Filter;

在RequestbodyFilter 中使用责任链设计模式编写一套自定义的请求过滤器
主要类以下:
链条:redis

public class RequestFilterChain {
    private List<IRequestFilter> filterList = new ArrayList<>();
    private int index = 0;
    private boolean hasAddDefaultFilter = false;

    public RequestFilterChain addFilter(IRequestFilter filter) {
        if (hasAddDefaultFilter) {
            throw new RuntimeException("自定义过滤器必须在默认过滤器以前添加");
        }
        this.filterList.add(filter);
        return this;
    }

    public RequestFilterChain addDefaultFilter(IRequestFilter filter) {
        this.filterList.add(filter);
        hasAddDefaultFilter = true;
//        DefaultFormRequestWrapperFilter defaultDaoFilter = (DefaultFormRequestWrapperFilter) filter;
        return this;
    }

    public void reset() {
        this.index = 0;
    }

    private IRequestFilter next() {
        if (index == filterList.size()) {
            return null;
        }
        IRequestFilter filter = filterList.get(index);
        index++;
        return filter;

    }

    public void doFilter(HttpPutFormContentRequestWrapper request, HttpServletResponse response) throws IOException, ServletException {
        IRequestFilter filter = next();
        if (null == filter) {
            System.out.println("结束 index :" + index);
            return;
        }
        filter.doFilter(request, response, this);
    }

}

请求处理接口 :设计模式

public interface IRequestFilter {
    void doFilter(HttpPutFormContentRequestWrapper request, HttpServletResponse response, RequestFilterChain filterChain) throws IOException, ServletException;
}

在RequestbodyFilter 中处理链条:浏览器

wrapper.setChain(chain);
        RequestFilterChain requestFilterChain = new RequestFilterChain();


        addRequestFilter(requestFilterChain);
        HttpServletRequest httpServletRequest1 = (HttpServletRequest) request;
        boolean isLocalIp = WebServletUtil.isLocalIp(httpServletRequest1);
        if (!isLocalIp) {
            requestFilterChain.addFilter(new DecideUseCacheWhenOvertimeFilter());
        }

        requestFilterChain.addDefaultFilter(new DefaultFormRequestWrapperFilter());
        requestFilterChain.doFilter(wrapper, (HttpServletResponse) response);
        wrapper.resetCustom();

2. HttpSessionSyncShareFilter实现自定义请求接口 IRequestFilter

HttpSessionSyncShareFilter 中作了两件事:tomcat

  1. 获取session持久化方案,目前支持 database 和redis;
  2. 对请求request进行包装,包装成为 SessionSyncRequestWrapper;

HttpSessionSyncShareFilter中核心方法:服务器

@Override
    public void doFilter(HttpPutFormContentRequestWrapper request, HttpServletResponse response, RequestFilterChain filterChain) throws IOException, ServletException {
        ISharableSessionAPI sharedSessionAPI = getSharableSessionAPI(request, this.sessionImplType);
        System.out.println("sharableSessionAPI :" + sharedSessionAPI.getClass().getSimpleName());
        SessionSyncRequestWrapper sessionSyncRequestWrapper = new SessionSyncRequestWrapper(request, sharedSessionAPI);
        if (null == sessionSyncRequestWrapper.getChain()) {//NOTICE:必定要有这个判断
            sessionSyncRequestWrapper.setChain(request.getChain());
        }
        filterChain.doFilter(sessionSyncRequestWrapper, response);
    }

3. 请求包装类SessionSyncRequestWrapper

在SessionSyncRequestWrapper 中重写 getSession(boolean create) ,这样在业务代码中调用getSession(boolean create)方法时,
获取的是咱们自定义的session处理类 CustomSharedHttpSession,
CustomSharedHttpSession 继承 javax.servlet.http.HttpSessioncookie

4. CustomSharedHttpSession 重写HttpSession的三个经常使用方法

  1. getAttribute
/***
     * 须要重写
     * @param s
     * @return
     */
    @Override
    public Object getAttribute(String s) {
        Object o1 = null;
        if (null == this.getHttpSession()) {
            o1 = sharedSessionAPI.getSessionAttribute(this.JSESSIONID, s);
            return o1;
        }

        Object o = this.httpSession.getAttribute(s);
        if (o == null) {
            String currentSessionId = this.httpSession.getId();
            o = sharedSessionAPI.getSessionAttribute(currentSessionId, s);
            if (null != o) {
                //使用新的 JSESSIONID 保存到redis 中
                this.setAttribute(s, o);
                return o;
            }
            if ((!currentSessionId.equals(this.JSESSIONID))) {
                Object o2 = sharedSessionAPI.getSessionAttribute(this.JSESSIONID, s); //RedisCacheUtil.getSessionAttribute(this.JSESSIONID + s);
                if (null != o2) {
                    this.httpSession.setAttribute(s, o2);
                    //此时 this.JSESSIONID有值,可是currentSessionId没有值,全部要手动同步
                    sharedSessionAPI.setSessionAttribute(currentSessionId, s, o2);
                    o = o2;
                }
            }
        }
        return o;
    }
  1. setAttribute
/**
     * 须要重写
     *
     * @param s
     * @param o
     */
    @Override
    public void setAttribute(String s, Object o) {
        String sessionId = null;
        if (null == this.httpSession) {
            sessionId = this.JSESSIONID;
        } else {
            this.httpSession.setAttribute(s, o);
            sessionId = this.httpSession.getId();
        }
        sharedSessionAPI.setSessionAttribute(sessionId, s, o);
    }
  1. removeAttribute
@Override
    public void removeAttribute(String s) {
        if (null != this.httpSession) {
            this.httpSession.removeAttribute(s);
            String sessionId = this.httpSession.getId();
            sharedSessionAPI.setSessionAttribute(sessionId, s, null);
        }
        sharedSessionAPI.setSessionAttribute(this.JSESSIONID, s, null);
    }

难点解析

CustomSharedHttpSession的构造方法有三个参数:session

  1. 原始的javax.servlet.http.HttpSession
  2. JSESSIONID :从请求头中获取的JSESSIONID;
  3. ISharableSessionAPI:接口,自定义session属性和值的操做.

ISharableSessionAPI 接口 :

/***
 * see CustomSharedSessionAPI,CustomSharedHttpSession
 */
public interface ISharableSessionAPI {
    Object getSessionAttribute(String sessionId, String key);

    void setSessionAttribute(String sessionId, String s, Object o);
}

getAttribute(String s) 中的逻辑有点复杂,咱们进行详细解析

在(1)中,尝试获取原始的session,
若是原始的session为空,则调用ISharableSessionAPI获取属性值,直接返回,
并无判断属性值是否为空.

在(2)中,若是原始session不为空,则从原始session获取属性值o,
若是o不为空则直接返回,
由于已经取到值了,没有必要从ISharableSessionAPI 中获取.

在(3)中,若是o为空,就须要尝试从ISharableSessionAPI 中获取了;
先拿原始session的id 做为key,来获取,
若是属性值不为空,则同步到原始session中,由于刚才在(2)中得知原始session没有属性值.
而后返回.

进入步骤(4)中,说明 以原始session的id 做为key没有获取到值,
那么以this.JSESSIONID 做为key,调用ISharableSessionAPI 获取属性值,
若是获取到值,则同步到原始session,

(5)中为何又要设置一遍ISharableSessionAPI的保存呢?
这里是关键!!!
这里是关键!!!
这里是关键!!!

缘由以下:
咱们按照时间顺序走一遍流程:

浏览器第一次访问服务器, 服务器生成原始session,好比key为AAA,
登陆时,保存username到原始session 和ISharableSessionAPI 中.

此时tomcat重启了,
第二次访问,浏览器带过去cookie ,sessionid :AAA,
可是tomcat重启以后,原来的session属性都没了,因此经过AAA获取不到属性值,
tomcat会生成新的session id : BBB 因而以AAA为key,调用ISharableSessionAPI,成功获取到值,而且同步到session id : BBB,

若是没有步骤(5)的话, (5)的做用是把属性值同步到key为AAA的ISharableSessionAPI中.

此时tomcat又重启了,
第二次访问,浏览器带过去cookie ,sessionid :BBB,
tomcat重启以后,原来的session属性都没了,因此经过BBB获取不到属性值,
tomcat会生成新的session id : CCC
因而以BBB为key,调用ISharableSessionAPI,不可能获取到值,

这就出现bug了,原本是有属性值的,重启一次,能够获取,重启第二次,就没法获取了.

总结

  1. 咱们自定义的session持久化机制,是根据浏览器 cookie中 JSESSIONID 来关联登陆信息的,
    可是tomcat每重启次,session id会变化,因此才须要步骤(5)
  2. 咱们只须要实现ISharableSessionAPI,就能够完成session持久化的功能

代码:https://github.com/liuyu520/oa_framework

相关文章
相关标签/搜索