1.当项目采用Shiro之后,对于分布式的多台服务器间session不会共享,这会造成去每台服务器都会重新登录,并且很有可能造成当用户权限更改后,多台服务器权限不一致的问题(不想这么麻烦的话,也可以采用一致性哈希解决问题)

2.为解决这个问题,我采用了Redis进行ShiroSession共享。本文将着重分析,怎样在分布式下,集成Shiro

3.读本文前,需要对Shiro进行深入了解.

需要重写的类有哪些,为什么要进行重写

1.重写SimpleSession,因为我需要对session进行自定义的更细处理,避免每次请求,都需更新或创建session,大家也可按照自己的逻辑进行更改.另外为了能让shiro创建的是我们自定义的session,我们需要对
SessionFactory进行实现和shiro内进行相应的配置.

/**
 * shiro session 工厂 创建全局化session
 * 
 * @author william_zhong
 * @version 1.5.0
 * @time 2017-6-19
 **/
@Component
public class CsxShiroSessionFactory implements SessionFactory {

    @Override
    public Session createSession(SessionContext paramSessionContext) {

        CsxSession session = new CsxSession();

        return session;
    }

}
//shiro内的xml如下
    <!-- 自定义全局session -->
    <bean id="shiroSessionFactory" class="com.csx.shiro.CsxShiroSessionFactory" />

2.重写AuthorizingRealm和AuthorizationFilter这个就不说了,懂得都懂

3.重写WebSessionManager,目的是为了每次请求时,都能进入到我自定义的session会话管理器中。由于移动端是采用登录凭证进行访问的,所以我需要对获取的登录凭证进行自己的解密处理

/****
 * 重写shiro session管理
 * 
 * @author william_zhong
 * @version 1.5.0
 * @time 2017-6-19
 * 
 ***/
public class CsxAppSessionMannager extends DefaultWebSessionManager {

    private final Log log = LogFactory.getLog("CsxAppSessionMannager.class");

    public CsxAppSessionMannager() {

        super();

    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {

        log.info("会话管理开始:获取sessionId");
        try {
            // 获取token
            String token = request.getParameter(ShiroConstant.tokenContanst);

            if (StringUtils.isNotEmpty(token)) {

                // 解析加密的token,获取sessionid
                String jSESSIONID = null;

                String userId = RSAsecurity.DecryptStr(token.trim().replaceAll(" ", "+"));
                // 获取解密后的用户id
                jSESSIONID = ShiroRedisPool.getObject(String.class,ShiroRedisPool.shiroUserIdKey + userId);
                if (jSESSIONID != null) {
                    request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,

                            ShiroHttpServletRequest.URL_SESSION_ID_SOURCE); // session来源--url

                    request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, jSESSIONID);

                    request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
                }

                return jSESSIONID;
            }

        } catch (ShiroCustomizeException e) {
            // 跳转至登录页面进行登录
            log.error("会话管理失败原因为:" + e);
        }
        return super.getSessionId(request, response);
    }

}

4.重写会话持久层,只需继承CachingSessionDAO或EnterpriseCacheSessionDAO即可,值得注意的是,我是禁用了配置内的本地缓存,目的是为了分布式的多台服务器之间的session同步.

/***
 * shiro 自定义会话持久层 继承CachingSessionDAO即可
 * 
 * @author william_zhong
 * @version 1.5.0
 * @time 2017-6-14
 ***/
public class ShiroSessionCustomizeDao extends EnterpriseCacheSessionDAO {

    private final Log log = LogFactory.getLog("ShiroSessionCustomizeDao.class");

    /*****
     * 创建session,保存到数据库
     * 
     * @author william_zhong
     * @version 1.5.0
     * @time 2017-6-15
     *****/
    @Override
    protected Serializable doCreate(Session session) {

        log.info("第一次初始化session" + session);
        // 自定义获取sessionId
        Serializable sessionId = super.doCreate(session);
        // 第一次不存储至redis中
        return sessionId;
    }

    // 获取session
    @Override
    protected Session doReadSession(Serializable sessionId) {
        // 先从缓存中获取session,如果没有再去数据库中获取
        // Session session = super.doReadSession(sessionId);
        log.info("读取session内容");
        Session session = null;
        try {
            session = ShiroRedisPool.getSessionToRedis(ShiroRedisPool.shiroUserLoginKey + sessionId.toString());
        } catch (NullPointerException e) {
            log.info("shirosession 获取为空");
        }

        return session;
    }

    /**
     * 更新session的最后一次访问时间
     * 
     * @author william_zhong
     * @version 1.5.0
     * @time 2017-6-16
     * 
     ***/
    @Override
    protected void doUpdate(Session session) {
        // super.doUpdate(session);从本地读,但是分布式我禁用了cahce,统一从redis获取
        log.info("更新session");
        if (session instanceof CsxSession) {

            CsxSession csxSession = (CsxSession) session;

            if (csxSession.getIsEffectiveFlag()) {
                // 若是存储选项,则执行存储操作
                if (csxSession.getIsSaveFlag()&&csxSession.getAttribute("userId")!=null) {
                    log.info("对session重新赋值");
                    csxSession.setIsSaveFlag(false);
                    ShiroRedisPool.setObject(ShiroRedisPool.shiroUserIdKey + csxSession.getAttribute("userId"),
                            session.getId().toString(), ReadProperties.getSHIRO_SESSION_TIMEOUT());
                    ShiroRedisPool.setSessionToredis(ShiroRedisPool.shiroUserLoginKey + session.getId().toString(),
                            Base64Util.objectToString(csxSession), ReadProperties.getSHIRO_SESSION_TIMEOUT());
                } else {
                    ShiroRedisPool.setObject(ShiroRedisPool.shiroUserIdKey + csxSession.getUserId(),
                            session.getId().toString(), ReadProperties.getSHIRO_SESSION_TIMEOUT());
                    ShiroRedisPool.updateExtensionTime(ShiroRedisPool.shiroUserLoginKey + session.getId().toString(),
                            ReadProperties.getSHIRO_SESSION_TIMEOUT());
                    log.info("只更新时间");
                }
            }
        }
    }

    @Override
    public void update(Session session) {
        this.doUpdate(session);
    }

    // 删除session
    @Override
    protected void doDelete(Session session) {
        // super.doDelete(session);
        log.info("删除session");
        ShiroRedisPool.delObject(ShiroRedisPool.shiroUserLoginKey + session.getId().toString());
    }

}

配置如下

<!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="csxAppAuthorizingRealm" />
        <property name="sessionManager" ref="sessionManager" />
        <property name="cacheManager" ref="shiroCacheManager" />
    </bean>
<!-- 会话管理器 -->
    <bean id="sessionManager" class="com.csx.shiro.CsxAppSessionMannager">
        <property name="globalSessionTimeout" value="2592000000" />
        <property name="deleteInvalidSessions" value="true" />
        <property name="sessionFactory" ref="shiroSessionFactory" />
        <!-- 会话验证调度器 采用quartz检测会话是否过时,由于我们采用了redis,自带定时销毁所以不用 -->
        <property name="sessionValidationSchedulerEnabled" value="false" />
        <property name="sessionDAO" ref="shiroSessionCustomizeDao" />
        <!-- 是否启用/禁用Session Id Cookie,默认是启用的;如果禁用后将不会设置Session Id Cookie,即默认使用了Servlet容器的JSESSIONID,且通过URL重写(URL中的“;JSESSIONID=id”部分)保存Session 
            Id -->
        <property name="sessionIdCookieEnabled" value="false" />
        <property name="sessionListeners" ref="shiroSessionListener" />
    </bean>
<!-- 项目自定义的Realm -->
    <bean id="csxAppAuthorizingRealm" class="com.csx.shiro.CsxAppAuthorizingRealm" />

5.重写报错的shiro异常,目的是为了更好的用户体验

自定义的异常

/***
 * shiro 自定义异常
 * 
 * 1.解析token 失败,请重新登录
 * 2.用户信息过期,请重新登录
 * 3.用户权限不够,请切换账户
 * 
 * @author william_zhong
 * @version 1.5.0
 * @time 2017-6-16
 * 
 ***/
public class ShiroCustomizeException extends Exception {

    private static final long serialVersionUID = -2777701677658086556L;

    public static final String tokenParseFailMSG = "解析用户信息失败,请重新登录";

    public static final String userInformationExpiredMSG="用户信息过期,请重新登录";

    public static final String userInsufficientRightsMSG="用户权限不够,请切换账号";

    private String description;

    public ShiroCustomizeException( String description) {
        super(description);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getName());
        sb.append(getMessage());
        if (getDescription() != null) {
            sb.append(" - ");
            sb.append(getDescription());
        }
        return sb.toString();
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

}
public class CsxAppShiroExceptionResolver implements HandlerExceptionResolver {

    private static final Log log = LogFactory.getLog("CsxAppShiroExceptionResolver.class");

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
            Exception ex) {

        log.info("shiro 抛出异常");
        // 如果是shiro无权操作,因为shiro 在操作auno等一部分不进行转发至无权限url
        if (ex instanceof ShiroCustomizeException) {
            ModelAndView mv = new ModelAndView("redirect:/shiroAjaxExceptionDealApp.do?msg="+ex.getMessage());
            return mv;
        }else{
            ModelAndView mv = new ModelAndView("redirect:/shiroAjaxExceptionDealApp.do?msg=远程服务器报错");
            return mv;
        }
    }

}

shiro内的配置如下

<!-- 自定义异常处理 -->
    <bean id="exceptionResolver" class="com.csx.shiro.CsxAppShiroExceptionResolver" />