Shiro Security是非常不错的Security框架
最近在我的项目中进行相关整合,shiro不难,难就难在如何对已经成熟的系统进行整合
作为相关切入点,我也考虑了很久,整体运用上了如张开涛大佬所说
对于Subject我们一般这么使用:
1、身份验证(login)
2、授权(hasRole*/isPermitted*或checkRole*/checkPermission*)
3、将相应的数据存储到会话(Session)
4、切换身份(RunAs)/多线程身份传播
5、退出
回归标题,正常整合过后,基本可以正确的进行登录与登出
那么开始进行细节休整
大体介绍我们的系统架构是springmvc进行开发,一个项目里分出了两套系统,系统与系统间的区分仅仅只是 通过url路径上的不同,来表现。那么现在就出现了一种情况,系统1基本用户都能登入,而系统2却只有相关权限人才能登入。
表面上视乎能在登陆上做控制,比如login的时候通过权限判断就可以做到。那么这时候考虑的是如果用户之间通过url强行进入呢。
比如系统1用户登录,直接修改url进入系统2。此时属于非法访问。
正常我们会在filter内做过滤,也好做,在相关登录逻辑内对session赋予标记,在filter做过滤就over了,不符合直接logout。
想到这里,作为一个shiro框架使用者是不是感觉shiro貌似没起作用。
于是思考一个比较合理的方案,于是决定当用户登陆系统时,对sessionid进行赋值,系统1则在sessionid前+上相关字符串标记session为系统1用户。系统2则在sessionid前+上相关字符串标记session为系统2用户。
需求清晰,那么进行可行性分。
那么结合shiro中的session管理,开始做起了调查。
首先进行是的shiro如何修改sessionid,这能找却不能满足我的需求。因为市面上的他们都是以单系统,或者双项目双系统进行写的。完全不能符合我想要的。
一般都是这么做的
上面的解决方案是对不同项目进行不同的jsessionid名的配置
但我一个项目里怎么可能出现两个shiro,不符合我的要求
于是考虑shiro内置处理session我要做点手脚。
首先是查到了sessionid生成器
自定义了一个id生成器
public class SysSessionIdGenerator implements SessionIdGenerator { @Override public Serializable generateId(Session session) { if(session.getAttribute("sysType")!=null){ return session.getAttribute("sysType").toString()+"_"+UUID.randomUUID().toString(); } return UUID.randomUUID().toString(); } }
主要实现SessionIdGenerator generateId的方法。这里可以看见我吧sysType加到uuid前面。
然后就是注入给shiro使用
配置需要加入
sessionDAO 也要记得注入给 sessionManager 这里我就不写了
那么问题又来了,逻辑上不上应该生成session的时候调用吗,那么shiro的session是在什么时候生成的呢。
我翻了翻源码
subject.getSession()
这个get方法实现的
public Session getSession() { return getSession(true); } public Session getSession(boolean create) { if (log.isTraceEnabled()) { log.trace("attempting to get session; create = " + create + "; session is null = " + (this.session == null) + "; session has id = " + (this.session != null && session.getId() != null)); } if (this.session == null && create) { //added in 1.2: if (!isSessionCreationEnabled()) { String msg = "Session creation has been disabled for the current subject. This exception indicates " + "that there is either a programming error (using a session when it should never be " + "used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " + "for the current Subject. See the " + DisabledSessionException.class.getName() + " JavaDoc " + "for more."; throw new DisabledSessionException(msg); } log.trace("Starting session for host {}", getHost()); SessionContext sessionContext = createSessionContext(); Session session = this.securityManager.start(sessionContext); this.session = decorate(session); } return this.session; }
可以看出,当你不传参数的时候默认进行调用ture,
简单的 说当getsession(true)时,会判断现在是否有session,如果没有,则新生成一个,有则就用现有的。false则是如果没有,就不生成了返回null。
发现一个问题,当我刚刚进入登入页面的时候,此时shiro已经生成了一个session,于是在登陆校验时候不会生成新的session了。于是考虑了各种办法,比如登入的时候先logout一下等等,当然这些都叫做歪门邪道。后来发现了这么一篇文章
Shiro 自己实现登录后重新生成sessionid
我用了他的方法反正是没成功,系统还变的有点混乱。
仔细一看他的文章中有这么一段:
使用过程中发现Shiro在登录之后不会生成新的Jessionid。这显然会出现 Session_Fixation。
Shiro自己说会在下一个版本1.3 fix这个问题。
我shiro起步是张开涛大大文章里的版本,所以是1.2.2的。尝试性的换个版本,看了下官网的版本是1.3.2
先换了再说。
发现确实登陆之前与之后sessionid变了,看来在1.3.2的时候会在getsession重新获得session。
但是这一点我并不明确,只能推测是这样。
但是这还不是我的正道。重新明确技术细节,发现我需要重载getsession方法,在getsession的时候把sysTpye(系统标记)字符串传递进去。
还是刚刚上面的代码有这么一行
SessionContext sessionContext = createSessionContext(); Session session = this.securityManager.start(sessionContext);
它吧sessionContext传递进去创建了。那么我似乎可以在这里做文章,查阅资料后发现SessionContext继承了Map。那么我就可以直接对它进行put了。
那么继续往下挖掘源码。
这篇挖掘的文章可以看看,我反正看完思路清晰了一点,毕竟自己debug比较混乱。
shiro源码分析
此时考虑到sessionContext对象还不是最终目标session,那么我赋予的值要么shiro会对其进行全部输出到session里,要么什么也不做
public class SimpleSessionFactory implements SessionFactory { /** * Creates a new {@link SimpleSession SimpleSession} instance retaining the context's * {@link SessionContext#getHost() host} if one can be found. * * @param initData the initialization data to be used during {@link Session} creation. * @return a new {@link SimpleSession SimpleSession} instance */ public Session createSession(SessionContext initData) { if (initData != null) { String host = initData.getHost(); if (host != null) { return new SimpleSession(host); } } return new SimpleSession(); } }
最后扒到这里,它只是取了host 然后赋值,生成simplesession对象。
看来这里是SessionContext的数据终点,那么我systpye也得在这里进行操作了。
查询 SessionFactory相关资料后,发现原来我们自己也可以定义自己的SessionFactory
对象。于是自定义了一个SessionFactory
public class HrsystemSessionFactory implements SessionFactory { @Override public Session createSession(SessionContext initData) { Session session = null; if (initData != null) { String host = initData.getHost(); if (host != null) { session = new SimpleSession(host); } if(initData.get("sysType")!=null){ session.setAttribute("sysType", initData.get("sysType")); } }else{ session = new SimpleSession(); } return session; } }
这里做的是把sysType的值赋值给session,然后配置文件注入。
那么与刚刚的SysSessionIdGenerator对接上了。
接下来就是重头戏,重载getsession();以上都有废话之嫌,长话短说。
首先拓展suject接口
public interface SysSubject extends Subject { Session getSession(String sysType); }
这里我直接继承Subject接口;
public static Subject getSubject() { Subject subject = ThreadContext.getSubject(); if (subject == null) { subject = (new Subject.Builder()).buildSubject(); ThreadContext.bind(subject); } return subject; }
Subject实例是使用ThreadLocal模式来获取,若没有则创建一个并绑定到当前线程。此时创建使用的是Subject内部类Builder来创建的,Builder会创建一个SubjectContext接口的实例DefaultSubjectContext,最终会委托securityManager来根据SubjectContext信息来创建一个Subject
上面代码就是前面介绍源码的文章里有讲的。
那么主要的就是需要进行实例化编写了。
这里有点回到原点了,从创建session变成了如何创建subject对象。
但是仔细剖析方法结构,会发现其实subject获取模式与session获取模式是一样的。
但是重写subjectFactory在网络与张开涛里面都没有触及或者很少。这可能需要阅读源码才理解。
代码如下
public class HrsystemSubjectFactory extends DefaultWebSubjectFactory { public HrsystemSubjectFactory() { super(); } public Subject createSubject(SubjectContext context) { if (!(context instanceof WebSubjectContext)) { return super.createSubject(context); } WebSubjectContext wsc = (WebSubjectContext) context; SecurityManager securityManager = wsc.resolveSecurityManager(); Session session = wsc.resolveSession(); boolean sessionEnabled = wsc.isSessionCreationEnabled(); PrincipalCollection principals = wsc.resolvePrincipals(); boolean authenticated = wsc.resolveAuthenticated(); String host = wsc.resolveHost(); ServletRequest request = wsc.resolveServletRequest(); ServletResponse response = wsc.resolveServletResponse(); return new HrsystemSubject(principals, authenticated, host, session, sessionEnabled, request, response, securityManager); } /** * @deprecated since 1.2 - override {@link #createSubject(org.apache.shiro.subject.SubjectContext)} directly if you * need to instantiate a custom {@link Subject} class. */ @Deprecated protected Subject newSubjectInstance(PrincipalCollection principals, boolean authenticated, String host, Session session, ServletRequest request, ServletResponse response, SecurityManager securityManager) { return new WebDelegatingSubject(principals, authenticated, host, session, true, request, response, securityManager); } }
主要是在createSubject方法中实例化HrsystemSubject对象将它传递出去。
而这个HrsystemSubject实例化SysSubject接口 与继承WebDelegatingSubject对象
public class HrsystemSubject extends WebDelegatingSubject implements SysSubject{ public HrsystemSubject(PrincipalCollection principals, boolean authenticated, String host, Session session, boolean sessionEnabled, ServletRequest request, ServletResponse response, SecurityManager securityManager) { super(principals, authenticated, host, session, sessionEnabled, request, response, securityManager); } public Session getSession(String type) { SessionContext sessionContext = createSessionContext(); sessionContext.put("sysType", type); Session session = this.securityManager.start(sessionContext); super.session = decorate(session); return super.session; } }
ok那么此时还没完,我们需要把HrsystemSubjectFactory注入到shiro中去。
这里我完全是模仿着session套路感觉注入的了,因为并没有文章这么做。(或者我没看到吧)
于是此时SecurityUtils.getSubject();get的出来的对象就是我们的HrsystemSubject了。
但是这里运用了下父子继承原理,get对象实际是Subject,内部对象的实例其实是HrsystemSubject
那么我们在对其强制装换成我们刚刚定的(SysSubject)SecurityUtils.getSubject();
于是关于getsession的重载就完成了。
那么逻辑上输入getsession(string sysType)那么就可以对sessionid进行我想要的值了。也能做逻辑控制了。运用场景还是挺广的。