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