• 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这块登陆和身份权限验证也就简单多了。自己也可以写一套了,不需要这么的复杂,

    有其他的业务也可以直接扩展。




      

    
    
    
  • 相关阅读:
    对es6中Promise和async的理解
    js里面的map、filter、forEach、reduce、for in、for of等遍历方法
    浏览器怎么解析一个hmtl文档
    js获取浏览器版本
    js中的浅复制和深复制
    作为一个程序员,如何调试抓取跳转页面前发送的请求
    escape、unescape、encodeURIComponent、decodeURLComponent
    css超过一定长度显示省略号 强制换行
    gojs去除水印
    版本控制系统svn的超基础使用
  • 原文地址:https://www.cnblogs.com/xzn-smy/p/9117993.html
Copyright © 2020-2023  润新知