当前位置: 移动技术网 > IT编程>开发语言>Java > shiro源码篇 - shiro的session共享,你值得拥有

shiro源码篇 - shiro的session共享,你值得拥有

2018年10月30日  | 移动技术网IT编程  | 我要评论

前言

  开心一刻

    老师对小明说:"乳就是小的意思,比如乳猪就是小猪,乳名就是小名,请你用乳字造个句"
    小明:"我家很穷,只能住在40平米的乳房"
    老师:"..., 这个不行,换一个"
    小明:"我每天上学都要跳过我家门口的一条乳沟"
    老师:"......, 这个也不行,再换一个"
    小明:"老师,我想不出来了,把我的乳头都想破了!"

  路漫漫其修远兮,吾将上下而求索!

  github:

  码云(gitee):

前情回顾

  与中,shiro对session的操作基本都讲到了,但还缺一个session共享没有讲解;session共享的原理其实在一文已经讲过了,本文不讲原理,只看看shiro的session共享的实现。

  为何需要session共享

    如果是单机应用,那么谈不上session共享,session放哪都无所谓,不在乎放到默认的servlet容器中,还是抽出来放到单独的地方;

    也就是说session共享是针对集群(或分布式、或分布式集群)的;如果不做session共享,仍然采用默认的方式(session存放到默认的servlet容器),当我们的应用是以集群的方式发布的时候,同个用户的请求会被分发到不同的集群节点(分发依赖具体的负载均衡规则),那么每个处理同个用户请求的节点都会重新生成该用户的session,这些session之间是毫无关联的。那么同个用户的请求会被当成多个不同用户的请求,这肯定是不行的。

  如何实现session共享

    实现方式其实有很多,甚至可以不做session共享,具体有哪些,大家自行去查资料。本文提供一种方式:redis实现session共享,就是将session从servlet容器抽出来,放到redis中存储,所有集群节点都从redis中对session进行操作。

sessiondao

  sessiondao其实是用于session持久化的,但里面有缓存部分,具体细节我们往下看

  shiro已有sessiondao的实现如下

  sessiondao接口提供的方法如下

package org.apache.shiro.session.mgt.eis;

import org.apache.shiro.session.session;
import org.apache.shiro.session.unknownsessionexception;

import java.io.serializable;
import java.util.collection;


/**
 * 从eis操作session的规范(eis:例如关系型数据库, 文件系统, 持久化缓存等等, 具体依赖dao实现)
 * 提供了典型的crud的方法:create, readsession, update, delete
 */
public interface sessiondao {

    /**
     * 插入一个新的sesion记录到eis 
     */
    serializable create(session session);

    /**
     * 根据会话id获取会话
     */
    session readsession(serializable sessionid) throws unknownsessionexception;

    /**
     * 更新session; 如更新session最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
     */
    void update(session session) throws unknownsessionexception;

    /**
     * 删除session; 当会话过期/会话停止(如用户退出时)会调用
     */
    void delete(session session);

    /**
     * 获取当前所有活跃session, 所有状态不是stopped/expired的session
     * 如果用户量多此方法影响性能
     */
    collection<session> getactivesessions();
}
view code

    sessiondao给出了从持久层(一般而言是关系型数据库)操作session的标准。

  abstractsessiondao提供了sessiondao的基本实现,如下

package org.apache.shiro.session.mgt.eis;

import org.apache.shiro.session.session;
import org.apache.shiro.session.unknownsessionexception;
import org.apache.shiro.session.mgt.simplesession;

import java.io.serializable;


/**
 * sessiondao的抽象实现, 在会话创建和读取时做一些健全性检查,并在需要时允许可插入的会话id生成策略.
 * sessiondao的update和delete则留给子类来实现
 * eis需要子类自己实现
 */
public abstract class abstractsessiondao implements sessiondao {

    /**
     * sessionid生成器
     */
    private sessionidgenerator sessionidgenerator;

    public abstractsessiondao() {
        this.sessionidgenerator = new javauuidsessionidgenerator();    // 指定javauuidsessionidgenerator为默认sessionid生成器
    }

    /**
     * 获取sessionid生成器
     */
    public sessionidgenerator getsessionidgenerator() {
        return sessionidgenerator;
    }

    /**
     * 设置sessionid生成器
     */
    public void setsessionidgenerator(sessionidgenerator sessionidgenerator) {
        this.sessionidgenerator = sessionidgenerator;
    }

    /**
     * 生成一个新的sessionid, 并将它应用到session实例
     */
    protected serializable generatesessionid(session session) {
        if (this.sessionidgenerator == null) {
            string msg = "sessionidgenerator attribute has not been configured.";
            throw new illegalstateexception(msg);
        }
        return this.sessionidgenerator.generateid(session);
    }

    /**
     * sessiondao中create实现; 将创建的sesion保存到eis.
     * 子类docreate方法的代理,具体的细节委托给了子类的docreate方法
     */
    public serializable create(session session) {
        serializable sessionid = docreate(session);
        verifysessionid(sessionid);
        return sessionid;
    }

    /**
     * 保证从docreate返回的sessionid不是null,并且不是已经存在的.
     * 目前只实现了null校验,是否已存在是没有校验的,可能shiro的开发者会在后续补上吧.
     */
    private void verifysessionid(serializable sessionid) {
        if (sessionid == null) {
            string msg = "sessionid returned from docreate implementation is null.  please verify the implementation.";
            throw new illegalstateexception(msg);
        }
    }

    /**
     * 分配sessionid给session实例
     */
    protected void assignsessionid(session session, serializable sessionid) {
        ((simplesession) session).setid(sessionid);
    }

    /**
     * 子类通过实现此方法来持久化session实例到eis.
     */
    protected abstract serializable docreate(session session);

    /**
     * sessiondao中readsession实现; 通过sessionid从eis获取session对象.
     * 子类doreadsession方法的代理,具体的获取细节委托给了子类的doreadsession方法.
     */
    public session readsession(serializable sessionid) throws unknownsessionexception {
        session s = doreadsession(sessionid);
        if (s == null) {
            throw new unknownsessionexception("there is no session with id [" + sessionid + "]");
        }
        return s;
    }

    /**
     * 子类通过实现此方法从eis获取session实例
     */
    protected abstract session doreadsession(serializable sessionid);

}
view code

    sessiondao的基本实现,实现了sessiondao的create、readsession(具体还是依赖abstractsessiondao子类的docreate、doreadsession实现);同时加入了自己的sessionid生成器,负责sessionid的操作。

  cachingsessiondao提供了session缓存的功能,如下

package org.apache.shiro.session.mgt.eis;

import org.apache.shiro.cache.cache;
import org.apache.shiro.cache.cachemanager;
import org.apache.shiro.cache.cachemanageraware;
import org.apache.shiro.session.session;
import org.apache.shiro.session.unknownsessionexception;
import org.apache.shiro.session.mgt.validatingsession;

import java.io.serializable;
import java.util.collection;
import java.util.collections;

/**
 * 应用层与持久层(eis,如关系型数据库、文件系统、nosql)之间的缓存层实现
 * 缓存着所有激活状态的session
 * 实现了cachemanageraware,会在shiro加载的过程中调用此对象的setcachemanager方法
 */
public abstract class cachingsessiondao extends abstractsessiondao implements cachemanageraware {

    /**
     * 激活状态的sesion的默认缓存名
     */
    public static final string active_session_cache_name = "shiro-activesessioncache";

    /**
     * 缓存管理器,用来获取session缓存
     */
    private cachemanager cachemanager;

    /**
     * 用来缓存session的缓存实例
     */
    private cache<serializable, session> activesessions;

    /**
     * session缓存名, 默认是active_session_cache_name.
     */
    private string activesessionscachename = active_session_cache_name;

    public cachingsessiondao() {
    }

    /**
     * 设置缓存管理器
     */
    public void setcachemanager(cachemanager cachemanager) {
        this.cachemanager = cachemanager;
    }

    /**
     * 获取缓存管理器
     */
    public cachemanager getcachemanager() {
        return cachemanager;
    }

    /**
     * 获取缓存实例的名称,也就是获取activesessionscachename的值
     */
    public string getactivesessionscachename() {
        return activesessionscachename;
    }

    /**
     * 设置缓存实例的名称,也就是设置activesessionscachename的值
     */
    public void setactivesessionscachename(string activesessionscachename) {
        this.activesessionscachename = activesessionscachename;
    }

    /**
     * 获取缓存实例
     */
    public cache<serializable, session> getactivesessionscache() {
        return this.activesessions;
    }

    /**
     * 设置缓存实例
     */
    public void setactivesessionscache(cache<serializable, session> cache) {
        this.activesessions = cache;
    }

    /**
     * 获取缓存实例
     * 注意:不会返回non-null值
     *
     * @return the active sessions cache instance.
     */
    private cache<serializable, session> getactivesessionscachelazy() {
        if (this.activesessions == null) {
            this.activesessions = createactivesessionscache();
        }
        return activesessions;
    }

    /**
     * 创建缓存实例
     */
    protected cache<serializable, session> createactivesessionscache() {
        cache<serializable, session> cache = null;
        cachemanager mgr = getcachemanager();
        if (mgr != null) {
            string name = getactivesessionscachename();
            cache = mgr.getcache(name);
        }
        return cache;
    }

    /**
     * abstractsessiondao中create的重写
     * 调用父类(abstractsessiondao)的create方法, 然后将session缓存起来
     * 返回sessionid
     */
    public serializable create(session session) {
        serializable sessionid = super.create(session);    // 调用父类的create方法
        cache(session, sessionid);                        // 以sessionid作为key缓存session
        return sessionid;
    }

    /**
     * 从缓存中获取session; 若sessionid为null,则返回null
     */
    protected session getcachedsession(serializable sessionid) {
        session cached = null;
        if (sessionid != null) {
            cache<serializable, session> cache = getactivesessionscachelazy();
            if (cache != null) {
                cached = getcachedsession(sessionid, cache);
            }
        }
        return cached;
    }

    /**
     * 从缓存中获取session
     */
    protected session getcachedsession(serializable sessionid, cache<serializable, session> cache) {
        return cache.get(sessionid);
    }

    /**
     * 缓存session,以sessionid作为key
     */
    protected void cache(session session, serializable sessionid) {
        if (session == null || sessionid == null) {
            return;
        }
        cache<serializable, session> cache = getactivesessionscachelazy();
        if (cache == null) {
            return;
        }
        cache(session, sessionid, cache);
    }

    protected void cache(session session, serializable sessionid, cache<serializable, session> cache) {
        cache.put(sessionid, session);
    }

    /**
     * abstractsessiondao中readsession的重写
     * 先从缓存中获取,若没有则调用父类的readsession方法获取session
     */
    public session readsession(serializable sessionid) throws unknownsessionexception {
        session s = getcachedsession(sessionid);        // 从缓存中获取
        if (s == null) {
            s = super.readsession(sessionid);           // 调用父类readsession方法获取
        }
        return s;
    }

    /**
     * sessiondao中update的实现
     * 更新session的状态
     */
    public void update(session session) throws unknownsessionexception {
        doupdate(session);                               // 更新eis中的session
        if (session instanceof validatingsession) {
            if (((validatingsession) session).isvalid()) {
                cache(session, session.getid());         // 更新缓存中的session
            } else {
                uncache(session);                        // 移除缓存中的sesson
            }
        } else {
            cache(session, session.getid());
        }
    }

    /**
     * 由子类去实现,持久化session到eis
     */
    protected abstract void doupdate(session session);

    /**
     * sessiondao中delete的实现
     * 删除session
     */
    public void delete(session session) {
        uncache(session);                                // 从缓存中移除
        dodelete(session);                               // 从eis中删除
    }

    /**
     * 由子类去实现,从eis中删除session
     */
    protected abstract void dodelete(session session);

    /**
     * 从缓存中移除指定的session
     */
    protected void uncache(session session) {
        if (session == null) {
            return;
        }
        serializable id = session.getid();
        if (id == null) {
            return;
        }
        cache<serializable, session> cache = getactivesessionscachelazy();
        if (cache != null) {
            cache.remove(id);
        }
    }

    /**
     * sessiondao中getactivesessions的实现
     * 获取所有的存活的session
     */
    public collection<session> getactivesessions() {
        cache<serializable, session> cache = getactivesessionscachelazy();
        if (cache != null) {
            return cache.values();
        } else {
            return collections.emptyset();
        }
    }
}
view code

    是应用层与持久化层之间的缓存层,不用频繁请求持久化层以提升效率。重写了abstractsessiondao中的create、readsession方法,实现了sessiondao中的update、delete、getactivesessions方法,预留doupdate和dodelele给子类去实现(doxxx方法操作的是持久层)

  memorysessiondao,sessiondao的简单内存实现,如下

package org.apache.shiro.session.mgt.eis;

import org.apache.shiro.session.session;
import org.apache.shiro.session.unknownsessionexception;
import org.apache.shiro.util.collectionutils;
import org.slf4j.logger;
import org.slf4j.loggerfactory;

import java.io.serializable;
import java.util.collection;
import java.util.collections;
import java.util.concurrent.concurrenthashmap;
import java.util.concurrent.concurrentmap;


/**
 * 基于内存的sessiondao的简单实现,所有的session存在concurrentmap中
 * defaultsessionmanager默认用的memorysessiondao
 */
public class memorysessiondao extends abstractsessiondao {

    private static final logger log = loggerfactory.getlogger(memorysessiondao.class);

    private concurrentmap<serializable, session> sessions;                // 存放session的容器

    public memorysessiondao() {
        this.sessions = new concurrenthashmap<serializable, session>();
    }

    // abstractsessiondao 中docreate的重写; 将session存入sessions
    protected serializable docreate(session session) {
        serializable sessionid = generatesessionid(session);        // 生成sessionid
        assignsessionid(session, sessionid);                        // 将sessionid赋值到session
        storesession(sessionid, session);                            // 存储session到sessions
        return sessionid;
    }

    // 存储session到sessions
    protected session storesession(serializable id, session session) {
        if (id == null) {
            throw new nullpointerexception("id argument cannot be null.");
        }
        return sessions.putifabsent(id, session);
    }

    // abstractsessiondao 中doreadsession的重写; 从sessions中获取session
    protected session doreadsession(serializable sessionid) {
        return sessions.get(sessionid);
    }

    // sessiondao中update的实现; 更新sessions中指定的session
    public void update(session session) throws unknownsessionexception {
        storesession(session.getid(), session);
    }

    // sessiondao中delete的实现; 从sessions中移除指定的session
    public void delete(session session) {
        if (session == null) {
            throw new nullpointerexception("session argument cannot be null.");
        }
        serializable id = session.getid();
        if (id != null) {
            sessions.remove(id);
        }
    }

    // sessiondao中sessiondao中delete的实现的实现; 获取sessions中全部session
    public collection<session> sessiondao中delete的实现() {
        collection<session> values = sessions.values();
        if (collectionutils.isempty(values)) {
            return collections.emptyset();
        } else {
            return collections.unmodifiablecollection(values);
        }
    }

}
view code

    将session保存在内存中,存储结构是concurrenthashmap;项目中基本不用,即使我们不实现自己的sessiondao,一般用的也是enterprisecachesessiondao。

  enterprisecachesessiondao,提供了缓存功能的session维护,如下

package org.apache.shiro.session.mgt.eis;

import org.apache.shiro.cache.abstractcachemanager;
import org.apache.shiro.cache.cache;
import org.apache.shiro.cache.cacheexception;
import org.apache.shiro.cache.mapcache;
import org.apache.shiro.session.session;

import java.io.serializable;
import java.util.concurrent.concurrenthashmap;

public class enterprisecachesessiondao extends cachingsessiondao {

    public enterprisecachesessiondao() {
        
        // 设置默认缓存器,并实例化mapcache作为cache实例
        setcachemanager(new abstractcachemanager() {
            @override
            protected cache<serializable, session> createcache(string name) throws cacheexception {
                return new mapcache<serializable, session>(name, new concurrenthashmap<serializable, session>());
            }
        });
    }

    // abstractsessiondao中docreate的重写; 
    protected serializable docreate(session session) {
        serializable sessionid = generatesessionid(session);
        assignsessionid(session, sessionid);
        return sessionid;
    }

    // abstractsessiondao中doreadsession的重写
    protected session doreadsession(serializable sessionid) {
        return null; //should never execute because this implementation relies on parent class to access cache, which
        //is where all sessions reside - it is the cache implementation that determines if the
        //cache is memory only or disk-persistent, etc.
    }

    // cachingsessiondao中doupdate的重写
    protected void doupdate(session session) {
        //does nothing - parent class persists to cache.
    }

    // cachingsessiondao中dodelete的重写
    protected void dodelete(session session) {
        //does nothing - parent class removes from cache.
    }
}
view code

    设置了默认的缓存管理器(abstractcachemanager)和默认的缓存实例(mapcache),实现了缓存效果。从父类继承的持久化操作方法(doxxx)都是空实现,也就说enterprisecachesessiondao是没有实现持久化操作的,仅仅只是简单的提供了缓存实现。当然我们可以继承enterprisecachesessiondao,重写doxxx方法来实现持久化操作。

  总结下:sessiondao定义了从持久层操作session的标准;abstractsessiondao提供了sessiondao的基础实现,如生成会话id等;cachingsessiondao提供了对开发者透明的session缓存的功能,只需要设置相应的 cachemanager 即可;memorysessiondao直接在内存中进行session维护;而enterprisecachesessiondao提供了缓存功能的session维护,默认情况下使用 mapcache 实现,内部使用concurrenthashmap保存缓存的会话。因为shiro不知道我们需要将session持久化到哪里(关系型数据库,还是文件系统),所以只提供了memorysessiondao持久化到内存(听起来怪怪的,内存中能说成持久层吗)

shiro session共享

  共享实现

    shiro的session共享其实是比较简单的,重写cachemanager,将其操作指向我们的redis,然后实现我们自己的cachingsessiondao定制缓存操作和缓存持久化。

    自定义cachemanager

      shirorediscachemanager

package com.lee.shiro.config;

import org.apache.shiro.cache.cache;
import org.apache.shiro.cache.cacheexception;
import org.apache.shiro.cache.cachemanager;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.stereotype.component;

@component
public class shirorediscachemanager implements cachemanager {

    @autowired
    private cache shirorediscache;

    @override
    public <k, v> cache<k, v> getcache(string s) throws cacheexception {
        return shirorediscache;
    }
}
view code

      shirorediscache

package com.lee.shiro.config;

import org.apache.shiro.cache.cache;
import org.apache.shiro.cache.cacheexception;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.beans.factory.annotation.value;
import org.springframework.data.redis.core.redistemplate;
import org.springframework.stereotype.component;

import java.util.collection;
import java.util.set;
import java.util.concurrent.timeunit;

@component
public class shirorediscache<k,v> implements cache<k,v>{

    @autowired
    private redistemplate<k,v> redistemplate;

    @value("${spring.redis.expiretime}")
    private long expiretime;

    @override
    public v get(k k) throws cacheexception {
        return redistemplate.opsforvalue().get(k);
    }

    @override
    public v put(k k, v v) throws cacheexception {
        redistemplate.opsforvalue().set(k,v,expiretime, timeunit.seconds);
        return null;
    }

    @override
    public v remove(k k) throws cacheexception {
        v v = redistemplate.opsforvalue().get(k);
        redistemplate.opsforvalue().getoperations().delete(k);
        return v;
    }

    @override
    public void clear() throws cacheexception {
    }

    @override
    public int size() {
        return 0;
    }

    @override
    public set<k> keys() {
        return null;
    }

    @override
    public collection<v> values() {
        return null;
    }
}
view code

    自定义cachingsessiondao

      继承enterprisecachesessiondao,然后重新设置其cachemanager(替换掉默认的内存缓存器),这样也可以实现我们的自定义cachingsessiondao,但是这是优选吗;如若我们实现持久化,继承enterprisecachesessiondao是优选,但如果只是实现session缓存,那么cachingsessiondao是优选,自定义更灵活。那么我们还是继承cachingsessiondao来实现我们的自定义cachingsessiondao

      shirosessiondao

package com.lee.shiro.config;

import org.apache.shiro.session.session;
import org.apache.shiro.session.mgt.eis.cachingsessiondao;
import org.springframework.stereotype.component;

import java.io.serializable;

@component
public class shirosessiondao extends cachingsessiondao {

    @override
    protected void doupdate(session session) {
    }

    @override
    protected void dodelete(session session) {
    }

    @override
    protected serializable docreate(session session) {
        // 这里绑定sessionid到session,必须要有
        serializable sessionid = generatesessionid(session);
        assignsessionid(session, sessionid);
        return sessionid;
    }

    @override
    protected session doreadsession(serializable sessionid) {
        return null;
    }
}
view code

    最后将shirosessiondao实例赋值给sessionmanager实例,再讲sessionmanager实例赋值给securitymanager实例即可

    具体代码请参考

  源码解析

    底层还是利用filter + httpservletrequestwrapper将对session的操作接入到自己的实现中来,而不走默认的servlet容器,这样对session的操作完全由我们自己掌握。

    中其实讲到了shiro中对session操作的基本流程,这里不再赘述,没看的朋友可以先去看看再回过头来看这篇。本文只讲shiro中,如何将一个请求的session接入到自己的实现中来的;shiro中有很多默认的filter,我会单独开一篇来讲shiro的filter,这篇我们先不纠结这些filter。

    onceperrequestfilter中dofilter方法如下

public final void dofilter(servletrequest request, servletresponse response, filterchain filterchain)
        throws servletexception, ioexception {
    string alreadyfilteredattributename = getalreadyfilteredattributename();
    if ( request.getattribute(alreadyfilteredattributename) != null ) {    // 当前filter已经执行过了,进行下一个filter
        log.trace("filter '{}' already executed.  proceeding without invoking this filter.", getname());
        filterchain.dofilter(request, response);
    } else //noinspection deprecation
        if (/* added in 1.2: */ !isenabled(request, response) ||
            /* retain backwards compatibility: */ shouldnotfilter(request) ) {    // 当前filter未被启用或忽略此filter,则进行下一个filter;shouldnotfilter已经被废弃了
        log.debug("filter '{}' is not enabled for the current request.  proceeding without invoking this filter.",
                getname());
        filterchain.dofilter(request, response);
    } else {
        // do invoke this filter...
        log.trace("filter '{}' not yet executed.  executing now.", getname());
        request.setattribute(alreadyfilteredattributename, boolean.true);

        try {
            // 执行当前filter
            dofilterinternal(request, response, filterchain);
        } finally {
            // 一旦请求完成,我们清除当前filter的"已经过滤"的状态
            request.removeattribute(alreadyfilteredattributename);
        }
    }
}
view code

    上图中,我可以看到abstractshirofilter的dofilterinternal放中将request封装成了shiro自定义的shirohttpservletrequest,将response也封装成了shiro自定义的shirohttpservletresponse。既然filter中将request封装了shirohttpservletrequest,那么到我们应用的request就是shirohttpservletrequest类型,也就是说我们对session的操作最终都是由shiro完成的,而不是默认的servlet容器。

    另外补充一点,shiro的session创建不是懒创建的。servlet容器中的session创建是第一次请求session(第一调用request.getsession())时才创建。shiro的session创建如下图

    此时,还没登录,但是subject、session已经创建了,只是subject的认证状态为false,说明还没进行登录认证的。至于session创建过程已经保存到redis的流程需要大家自行去跟,或者阅读我之前的博文

总结

  1、当以集群方式对外提供服务的时候,不做session共享也是可以的

    可以通过ip_hash的机制将同个ip的请求定向到同一台后端,这样保证用户的请求始终是同一台服务处理,与单机应用基本一致了;但这有很多方面的缺陷(具体就不详说了),不推荐使用。

  2、servlet容器之间做session同步也是可以实现session共享的

    一个servlet容器生成session,其他节点的servlet容器从此servlet容器进行session同步,以达到session信息一致。这个也不推荐,某个时间点会有session不一致的问题,毕竟同步过程受到各方面的影响,不能保证session实时一致。

  3、session共享实现的原理其实都是一样的,都是filter + httpservletrequestwrapper,只是实现细节会有所区别;有兴趣的可以看下spring-session的实现细节。

  4、如果我们采用的spring集成shiro,其实可以将缓存管理器交由spring管理,相当于由spring统一管理缓存。

  5、shiro的cachemanager不只是管理session缓存,还管理着身份认证缓存、授权缓存,shiro的缓存都是cachemanager管理。但是身份认证缓存默认是关闭的,个人也不推荐开启。

  6、shiro的session创建时机是在登录认证之前,而不是第一次调用getsession()时。

参考

  《跟我学shiro》

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网