最近学习了解了一下Shiro,发现他在权限控制方面有很好的处理,现将相关的学习心得记录下来,作为自己的知识储备,也为有需要的小伙伴提供解决方案。
Shiro 是一款简单易用的Java安全框架,可以帮助我们完成:认证、授权、加密、会话管理,你可以快速集成到任何应用程序——从最小的移动应用程序到最大的web应用程序。
图一在很多地方都可以见到,他可以让我们从程序的角度来了解shiro怎样帮我们完成权限控制。
Subject, 当前登录主体。
SecurityManager,是Shiro的核心,一系列的认证、授权、加密、会话管理等操作,都是由他来统一调度。
Realm,相当于安全数据源,Shiro需要从Realm中获取安全数据,如用户、角色、权限。如果用户需要访问某个接口,首先需要Realm告知登录用户是否合法(如密码校验等),其次需要Realm告知用户是否有权限访问接口。Realm需要开发者自己编写处理逻辑。
图一
图二在很多地方也可以见到,这个可以反映Shiro的整体架构,各个组件的说明,可以查看开涛老师Shiro教程博客,上面写得很详细,大家可以查看他的博客,http://jinnianshilongnian.iteye.com/blog/2018936
图二
了解了Shiro的整体架构后,我接下来说明一下自己遇到的三个问题及解决方案。
一、前后端分离
项目中采用前后端分离的处理方法,并且前端是ajax跨域请求,无法带上cookie,虽然针对这点网上有解决办法,可是从安全角度考虑,也不想把sessionId保存在cookie中。查看Shiro默认的web会话管理器,DefaultWebSessionManager,可以发现Shiro默认是通过Cookie来获取sessionId,如果无法从cookie中获取,就会去查看url后是否带有JSESSIONID或jsessionid参数,来获取sessionId。我们都不满足,这样的话,原生的方法就无法获取sessionId,所以需要自己稍微改造一下获取sessionId的方法,如下所示,自行定义一个MyWebSessionManager,继承DefaultWebSessionManager类,更改获取sessionId的方法,最后需要将自定义的MyWebSessionManager注入到securityManager中。
public class MyWebSessionManager extends DefaultWebSessionManager{
.................
private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader("token"); if(Strings.isNullOrEmpty(id)){ id = this.getSessionIdCookieValue(request, response); } if (id != null) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "cookie"); } else { id = this.getUriPathSegmentParamValue(request, "JSESSIONID"); if (id == null) { String name = this.getSessionIdName(); id = request.getParameter(name); if (id == null) { id = request.getParameter(name.toLowerCase()); } } if (id != null) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "url"); } } if (id != null) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); } return id; }
....................
}
二、会话集群管理
后台部署在多台机器上,就涉及到session共享的问题。通过查看DefaultWebSessionManage源码,发现其父类DefaultSessionManager中含有SessionDAO属性,通过他可以对session进行管理,默认的是MemorySessionDAO,那么我们可以自己实现SessionDAO,注入到sessionManager中。如下所示,redisManager是普通的redis管理类,定义了自己的session储存类,最后需要将MyShiroSessionDAO注入到sessionManager中。
public class MyShiroSessionDAO extends EnterpriseCacheSessionDAO {
static final String SESSION_KEY = "session:";
//创建session
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.doCreate(session);
final String key = SESSION_KEY + session.getId().toString();
setShiroSession(key,session);
return sessionId;
}
//获取session
@Override
protected Session doReadSession(Serializable sessionId) {
Session session = super.doReadSession(sessionId);
if(session == null){
final String key = SESSION_KEY + sessionId;
session = getShiroSession(key);
}
return session;
}
//更新session
@Override
protected void doUpdate(Session session) {
super.doUpdate(session);
final String key = SESSION_KEY + session.getId();
setShiroSession(key, session);
}
//删除session
@Override
protected void doDelete(Session session) {
super.doDelete(session);
final String key = SESSION_KEY + session.getId();
byte[] keyBytes = key.getBytes();
redisManager.deletBytes(keyBytes);
}
//获取session
private Session getShiroSession(String key) {
byte[] keyBytes = key.getBytes();
Session session = null;
byte[] sessionBytes = redisManager.getBytes(keyBytes);
if(sessionBytes != null){
session = (Session)SerializeUtil.unserialize(sessionBytes);
}
return session;
}
//保存session
private void setShiroSession(String key, Session session){
Integer expire = 1800;
byte[] keyBytes = key.getBytes();
byte[] sessionBytes = SerializeUtil.serialize(session);
redisManager.setBytes(keyBytes, sessionBytes, expire);
}
}
三、去掉登录时url中的JSESSIONID
shiro配置文件中有一个loginUrl的配置,表示登录跳转的url,shiro默认的会在url后面加上“;JSESSIONID=”的后缀,看网上很多人说shiro1.4版本已经修改了这个bug,只要在sessionManager中的sessionIdUrlRewritingEnabled设置为false就可以了,只是我的好像没起作用,所以跟踪了源码后,发现到最后拼接跳转url的时候,会先判断ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED值,如果为false就不会在登录url后拼接JSESSIONID,于是可以覆写sessionManager中的onStart方法,如下所示,
protected void onStart(Session session, SessionContext context) {
super.onStart(session, context);
if(!WebUtils.isHttp(context)) {
log.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response pair. No session ID cookie will be set.");
} else {
ServletRequest request = WebUtils.getRequest(context);
//控制重定向到loginUrl时不带JSESSIONID
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED,isSessionIdUrlRewritingEnabled());
}
}
下面这个方法是把shiro的源码粘贴上去,isEncodeable方法是判断url是否加上JSESSIONID后缀的其中一步。
protected boolean isEncodeable(String location) {
if(Boolean.FALSE.equals(this.request.getAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED))) {
return false;
} else if(location == null) {
return false;
} else if(location.startsWith("#")) {
return false;
} else {
HttpServletRequest hreq = this.request;
HttpSession session = hreq.getSession(false);
return session == null?false:(hreq.isRequestedSessionIdFromCookie()?false:this.doIsEncodeable(hreq, session, location));
}
}
四、控制同一个账户只有一个用户在线
这个就不细说了,主要结合redis,把登录的用户名和session保存在redis中,在realm的doGetAuthorizationInfo方法中进行判断