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

时间:2022-12-22 15:00:58

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