Shiro企业级实战详解,统一的Session管理。
基础的什么配置这些都不说了,百度一下什么都有,直接上干货。
Shiro切入点是从web.xml文件,通过filter进行拦截。
直接看DelegatingFilterProxy这个类,很简单,父类就是一个filter,肯定会初始化filter,后面会调用这个方法:
@Override protected void initFilterBean() throws ServletException { synchronized (this.delegateMonitor) { if (this.delegate == null) { // If no target bean name specified, use filter name. if (this.targetBeanName == null) { this.targetBeanName = getFilterName(); } // Fetch Spring root application context and initialize the delegate early, // if possible. If the root application context will be started after this // filter proxy, we'll have to resort to lazy initialization. WebApplicationContext wac = findWebApplicationContext(); if (wac != null) { this.delegate = initDelegate(wac); } } } }
这个方法主要的作用就是获取filterName,通过filterName和类型从spring Bean容器中获取Bean然后赋予delegate。filtetName很显然是shiroFilter,然后这个类型就是Filter.class。
至于想要获取的这个Bean,也很简单。直接找spring中shiro的这块配置。
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="${shiro.loginUrl}"/> <!--登录成功默认跳转页面,不配置则跳转至”/”。 如果登陆前点击的一个需要登录的页面,则在登录自动跳转到那个需要登录的页面。不跳转到此。 --> <property name="successUrl" value="${shiro.successUrl}"/> <!--没有权限跳转的链接 --> <property name="unauthorizedUrl" value="/login"/> <property name="filterChainDefinitions"> <value> /favicon.ico =anon /css/** = anon /js/** = anon /druid/** = anon /ecoupon/info/detail = anon /** = authc </value> </property> </bean>
<!-- 权限管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- 基于redis登录校验的实现 -->
<property name="realm" ref="systemAuthorizingRealm"/>
<!-- session 管理器 -->
<property name="sessionManager" ref="sessionManager"/>
</bean>
ShiroFilterFactoryBean这个类实现FactoryBean,故在getBean时会执行
public Object getObject() throws Exception { if (instance == null) { instance = createInstance(); } return instance; }
protected AbstractShiroFilter createInstance() throws Exception { log.debug("Creating Shiro Filter instance."); SecurityManager securityManager = getSecurityManager(); if (securityManager == null) { String msg = "SecurityManager property must be set."; throw new BeanInitializationException(msg); } if (!(securityManager instanceof WebSecurityManager)) { String msg = "The security manager does not implement the WebSecurityManager interface."; throw new BeanInitializationException(msg); } FilterChainManager manager = createFilterChainManager(); //Expose the constructed FilterChainManager by first wrapping it in a // FilterChainResolver implementation. The AbstractShiroFilter implementations // do not know about FilterChainManagers - only resolvers: PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver(); chainResolver.setFilterChainManager(manager); //Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built //FilterChainResolver. It doesn't matter that the instance is an anonymous inner class //here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts //injection of the SecurityManager and FilterChainResolver: return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver); }
所以最后肯定就会获取到SpringShiroFilter对应的bean。其实就是最后new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver)产生的对象,
这样整个入口算是清楚了。
比如现在正好有一个请求过来了,第一步,肯定是DelegatingFilterProxy中进行doFilter拦截住。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { // Lazily initialize the delegate if necessary. Filter delegateToUse = this.delegate; if (delegateToUse == null) { synchronized (this.delegateMonitor) { if (this.delegate == null) { WebApplicationContext wac = findWebApplicationContext(); if (wac == null) { throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener registered?"); } this.delegate = initDelegate(wac); } delegateToUse = this.delegate; } } // Let the delegate perform the actual doFilter operation. invokeDelegate(delegateToUse, request, response, filterChain); }
delegateToUse这个我们也获取到了,后面最重要的是invokeDelegate(delegateToUse, request, response, filterChain)这个方法。
后面会调用OncePerRequestFilter中的doFilter方法,很简单,这个是SpringShiroFilter的父类,然后就会执行doFilterInternal的实现,这个实现在AbstractShiroFilter
中。重点来了,
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException { Throwable t = null; try {
这里就是对serverRequest和serverResponse包装 final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain); final ServletResponse response = prepareServletResponse(request, servletResponse, chain); 这个才是我们的重头戏 final Subject subject = createSubject(request, response); //noinspection unchecked
这个会执行SubjectCallAble中的call方法将生成的subject对象绑定到ThreadContext中,这个原理很简单,就是ThreadLocal对当前线程进行绑定。 subject.execute(new Callable() { public Object call() throws Exception {
绑定到当前线程之后,更新session最后获取时间。 updateSessionLastAccessTime(request, response); executeChain(request, response, chain); return null; } }); } catch (ExecutionException ex) { t = ex.getCause(); } catch (Throwable throwable) { t = throwable; } if (t != null) { if (t instanceof ServletException) { throw (ServletException) t; } if (t instanceof IOException) { throw (IOException) t; } //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one: String msg = "Filtered request failed."; throw new ServletException(msg, t); } }
后面会进入这个创建Subject的方法,看看具体干了什么
public Subject createSubject(SubjectContext subjectContext) { //create a copy so we don't modify the argument's backing map:
很简单,这个就是相当于包装,把subjectContext包装进去。 SubjectContext context = copy(subjectContext); 把secutityManager放进去。 context = ensureSecurityManager(context); 获取服务器session context = resolveSession(context);
这个其实没啥用,使用认证信息和权限信息缓存时才会有用,这个在多节点的服务来说,基本不会在服务器做这个,相当于把一个Map的key放进context中,第一次进来,肯定是空。
等到认证完成后,才会有值,是从session中获取的。
context = resolvePrincipals(context);
这个很重要,就是将认证状态、host、request、response等信息放到subject中,
Subject subject = doCreateSubject(context); save(subject); return subject; }
// 将认证状态和上面说的那个key放到session中,刚开始肯定什么都不做,都是空。登陆后才会真正在session中写入。
save(subject);
return subject;
以上Filter走完后,中间其他过程不要理会,后面进行登陆操作。
通过SecurityUtils.getSubject()获取当前线程中的subject,也就是上面filter中创建的subject。
调用subject.login()方法进行登陆认证。
public void login(AuthenticationToken token) throws AuthenticationException { clearRunAsIdentitiesInternal();
获取登陆中的subject,解释在下面方法中。 Subject subject = securityManager.login(this, token); PrincipalCollection principals; String host = null; if (subject instanceof DelegatingSubject) { DelegatingSubject delegating = (DelegatingSubject) subject; //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals: principals = delegating.principals; host = delegating.host; } else { principals = subject.getPrincipals(); } if (principals == null || principals.isEmpty()) { String msg = "Principals returned from securityManager.login( token ) returned a null or " + "empty value. This value must be non null and populated with one or more elements."; throw new IllegalStateException(msg); } this.principals = principals; this.authenticated = true; if (token instanceof HostAuthenticationToken) { host = ((HostAuthenticationToken) token).getHost(); } if (host != null) { this.host = host; }
// 从登陆中的subject中获取session,并将session放入filter中生成的subject。 Session session = subject.getSession(false); if (session != null) { this.session = decorate(session); } else { this.session = null; } }
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try {
// 在自己实现的Realm中进行认证并获取认证信息 info = authenticate(token); } catch (AuthenticationException ae) { try { onFailedLogin(token, ae, subject); } catch (Exception e) { if (log.isInfoEnabled()) { log.info("onFailedLogin method threw an " + "exception. Logging and propagating original AuthenticationException.", e); } } throw ae; //propagate } 将token和info还有以前的subject来创建登陆中的subject,这是一个新的subject,此处会在服务端生成一个session,并将一些信息返回给写入
如:认证状态和上面说的那个无用的key。 Subject loggedIn = createSubject(token, info, subject); onSuccessfulLogin(token, info, loggedIn); return loggedIn; }
以上shiro登陆流程算是走完了,如果使用默认的sessionManager,也就是ServletContainerSessionManager会在会话后在cookie生成一个JsessionId
用户在第二次进来的时候服务器会根据JseeionId帮你找到服务器的上次登陆的session,里面包含登陆的信息。
save(subject);也就是这个方法里面做的。最简单的shiro登陆流程算是结束了。
但是这种在服务器存放的session有很多问题,在多节点的项目中,必然是登陆功能要重新登陆好几次,原因很简单,session不共享。
为了解决这个问题,可以不要使用默认的sessionManager(如果没有配置就是默认的ServletContainerSessionManager)。
配置使用DefaultWebSessionManager,然后在内部配置实现。
<!-- 权限管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- 基于redis登录校验的实现 -->
<property name="realm" ref="systemAuthorizingRealm"/>
<!-- session 管理器 -->
<property name="sessionManager" ref="sessionManager"/>
</bean>
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!--session 超时时间:30分钟 -->
<property name="globalSessionTimeout" value="${shiro.sessionTimeout}"/>
<property name="sessionIdCookie" ref="sessionIdCookie"/>
<!--持久化shiro session,以适应集群环境-->
<property name="sessionDAO" ref="redisSessionDao"/>
</bean>
<!-- 指定本系统SESSIONID, 默认为: JSESSIONID 问题: 与SERVLET容器名冲突, 如JETTY, TOMCAT
等默认JSESSIONID, 当跳出SHIRO SERVLET时如ERROR-PAGE容器会为JSESSIONID重新分配值导致登录会话丢失! -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="${shiro.sid}"/>
<property name="httpOnly" value="true"/>
<property name="domain" value="${shiro.domain}"/>
</bean>
<bean id="redisSessionDao" class="com.***.***.shiro.RedisSessionDAO">
<property name="expire" value="2700"/>
<property name="keyPrefix" value="${shiro.sessionkey}"/>
</bean>
使用redis作为存储session的容器就可以完美的解决了,另外cookie也完全可以自定义,不必完全是JsessionId。
中间过程很简单,就不一一叙述了。
protected void mergePrincipals(Subject subject) { //merge PrincipalCollection state: PrincipalCollection currentPrincipals = null; //SHIRO-380: added if/else block - need to retain original (source) principals //This technique (reflection) is only temporary - a proper long term solution needs to be found, //but this technique allowed an immediate fix that is API point-version forwards and backwards compatible // //A more comprehensive review / cleaning of runAs should be performed for Shiro 1.3 / 2.0 + if (subject.isRunAs() && subject instanceof DelegatingSubject) { try { Field field = DelegatingSubject.class.getDeclaredField("principals"); field.setAccessible(true); currentPrincipals = (PrincipalCollection)field.get(subject); } catch (Exception e) { throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e); } } if (currentPrincipals == null || currentPrincipals.isEmpty()) { currentPrincipals = subject.getPrincipals(); } 获取session,获取不到不会创建 Session session = subject.getSession(false); if (session == null) { if (!CollectionUtils.isEmpty(currentPrincipals)) {
获取session,获取不到会自己创建,这个在shiro中实现的居然直接是uuid,有点随意啊。 session = subject.getSession(); session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals); } //otherwise no session and no principals - nothing to save } else { PrincipalCollection existingPrincipals = (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); if (CollectionUtils.isEmpty(currentPrincipals)) { if (!CollectionUtils.isEmpty(existingPrincipals)) { session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); } //otherwise both are null or empty - no need to update the session } else { if (!currentPrincipals.equals(existingPrincipals)) { session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals); } //otherwise they're the same - no need to update the session } } }
在这个getSession方法中会对根据不同的sessionManager获取sessionId。
redis的sessionDAO实现也发出来吧,都是可以直接用的。
public class RedisSessionDAO extends AbstractSessionDAO { private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class); /** * The Redis key prefix for the sessions */ private String keyPrefix = "shiro_redis_session:"; /** * redis 缓存过期时间/秒 */ private int expire = 60 * 60; /** * save session * * @param session * @throws UnknownSessionException */ private void saveSession(Session session) throws UnknownSessionException { logger.debug("saveSession"); if (session == null || session.getId() == null) { logger.error("session or session id is null"); return; } byte[] key = getByteKey(session.getId()); byte[] value = SerializeUtils.serialize(session); session.setTimeout(expire * 1000); RedisUtils.set(key, value, expire); } @Override public void update(Session session) throws UnknownSessionException { logger.debug("update"); this.saveSession(session); } @Override public void delete(Session session) { logger.debug("delete"); if (session == null || session.getId() == null) { logger.error("session or session id is null"); return; } RedisUtils.del(this.getByteKey(session.getId())); } @Override public Collection<Session> getActiveSessions() { logger.debug("getActiveSessions"); Set<Session> sessions = new HashSet<>(); Set<byte[]> keys = RedisUtils.keys(this.keyPrefix + "*"); if (keys != null && keys.size() > 0) { for (byte[] key : keys) { Session s = (Session) SerializeUtils.deserialize(RedisUtils.get(key)); sessions.add(s); } } return sessions; } @Override protected Serializable doCreate(Session session) { logger.debug("doCreate"); Serializable sessionId = this.generateSessionId(session); this.assignSessionId(session, sessionId); this.saveSession(session); return sessionId; } @Override protected Session doReadSession(Serializable sessionId) { logger.debug("doReadSession,sessionId:{}", sessionId); if (sessionId == null) { logger.error("session id is null"); return null; } try { return (Session) SerializeUtils.deserialize(RedisUtils.get(this.getByteKey(sessionId))); } catch (Exception e) { logger.error("Failed to deserialize", e); return null; } } /** * 获得byte[]型的key * * @param sessionId * @return */ private byte[] getByteKey(Serializable sessionId) { String preKey = this.keyPrefix + sessionId; return preKey.getBytes(StandardCharsets.UTF_8); } /** * Returns the Redis session keys * prefix. * * @return The prefix */ public String getKeyPrefix() { return keyPrefix; } /** * Sets the Redis sessions key * prefix. * * @param keyPrefix The prefix */ public void setKeyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; } public int getExpire() { return expire; } public void setExpire(int expire) { this.expire = expire; } }
说的比较笼统,只要自己debug过的就会很清楚,弄懂之后shiro这块登陆和身份权限验证也就简单多了。自己也可以写一套了,不需要这么的复杂,
有其他的业务也可以直接扩展。