Shiro-分布式下的解决方案及其实现

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" />

参考文章:
1.分布式系统唯一ID生成方案汇总 http://www.cnblogs.com/haoxinyue/p/5208136.html
2.使用redis进行基于shiro的session集群共享 http://www.cnblogs.com/sunshine-2015/p/5686750.html
3.Shiro源码分析之两种Session的方式 http://www.th7.cn/Program/java/201507/513741.shtml
4.Shiro多端登录控制 http://jinnianshilongnian.iteye.com/blog/2039760
5.切换角色的处理 http://jinnianshilongnian.iteye.com/blog/2044616
6.shiro 拦截器 http://jinnianshilongnian.iteye.com/blog/2025656

这是一个shiro的入门Demo.. 使用了Spring MVC,mybaits等技术.. 数据库设计 : User : name--password Role : id--userid--roleName Function : id--userid--url tinys普通用户只能访问index.jsp admin用户通过添加了admin的permission,所以可以访问admin.jsp role用户通过添加了role角色,所以可以访问role.jsp 这是最基本的shiro的运用..目的是让你快速了解shiro的机制.. 这个Demo体现shiro的地方主要在两个类以及shiro.xml的配置文件 CustomRealm : 处理了登录验证以及授权.. ShiroAction : 用来传递登录时的用户数据..转换为token传递给realm...之后根据结果做相应的逻辑处理.. shiro.xml : shiro的主要配置... 规则定义在以下地方 : /login.jsp* = anon /index.jsp* = authc /index.do* = authc /admin.jsp*=authc,perms[/admin] /role.jsp*=authc,roles[role] ------------------------------------------------------------------------------------------------------------------------------------------------------------- 2015-10-28更新 --通过添加了以下内容来使用注解方式配置权限.... unauth login --修改了过滤链 //简单的讲就是把需要特别处理的路径写到前面,越特殊写到越前 /shiro/login.do*=anon /login.jsp* = anon /admin.jsp*=authc,perms[/admin] /role.jsp*=authc,roles[role] /** = authc --------------------------------------------------------------------------------------------------------------------------------------------------- 15-10-29 添加了使用ehcache的缓存机制 添加了redis缓存...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值