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

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


  



原文地址:https://www.cnblogs.com/xzn-smy/p/9117993.html

时间: 2024-08-29 03:05:33

Shiro企业级实战详解,统一的Session管理。的相关文章

(转) shiro权限框架详解06-shiro与web项目整合(上)

http://blog.csdn.net/facekbook/article/details/54947730 shiro和web项目整合,实现类似真实项目的应用 本文中使用的项目架构是springMVC+mybatis,所以我们是基于搭建好的项目进行改造的. 将shiro整合到web应用中 登录 退出 认证信息在页面展现,也就是显示菜单 shiro的过滤器 将shiro整合到web应用中 数据库脚步 sql脚步放到项目中,项目上传到共享的资源中,文章最后给出共享url. 去除项目中不使用shi

第131讲:Hadoop集群管理工具均衡器Balancer 实战详解学习笔记

第131讲:Hadoop集群管理工具均衡器Balancer 实战详解学习笔记 为什么需要均衡器呢? 随着集群运行,具体hdfs各个数据存储节点上的block可能分布得越来越不均衡,会导致运行作业时降低mapreduce的本地性. 分布式计算中精髓性的一名话:数据不动代码动.降低本地性对性能的影响是致使的,而且不能充分利用集群的资源,因为导致任务计算会集中在部分datanode上,更易导致故障. balancer是hadoop的一个守护进程.会将block从忙的datanode移动到闲的datan

第130讲:Hadoop集群管理工具DataBlockScanner 实战详解学习笔记

第130讲:Hadoop集群管理工具DataBlockScanner 实战详解学习笔记 DataBlockScanner在datanode上运行的block扫描器,定期检测当前datanode节点上所有的block,从而在客户端读到有问题的块前及时检测和修复有问题的块. 它有所有维护的块的列表,通过对块的列表依次的扫描,查看是否有校验问题或错误问题,它还有截流机制. 什么叫截流机制?DataBlockScanner扫描时会消耗大量的磁盘带宽,如果占用磁盘带宽太大,会有性能问题.所以它会只占用一小

机器学习Spark Mllib算法源码及实战详解进阶与提高视频教程

38套大数据,云计算,架构,数据分析师,Hadoop,Spark,Storm,Kafka,人工智能,机器学习,深度学习,项目实战视频教程 视频课程包含: 38套大数据和人工智能精品高级课包含:大数据,云计算,架构,数据挖掘实战,实时推荐系统实战,电视收视率项目实战,实时流统计项目实战,离线电商分析项目实战,Spark大型项目实战用户分析,智能客户系统项目实战,Linux基础,Hadoop,Spark,Storm,Docker,Mapreduce,Kafka,Flume,OpenStack,Hiv

Scala 深入浅出实战经典 第78讲:Type与Class实战详解

王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载: 百度云盘:http://pan.baidu.com/s/1c0noOt6 腾讯微云:http://url.cn/TnGbdC 360云盘:http://yunpan.cn/cQ4c2UALDjSKy 访问密码 45e2土豆:http://www.tudou.com/programs/view/2vZ06RMcD6I/优酷:http://v.youku.com/v_show/id

Scala 深入浅出实战经典 第53讲:Scala中结构类型实战详解

王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-64讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 腾讯微云:http://url.cn/TnGbdC 360云盘:http://yunpan.cn/cQ4c2UALDjSKy 访问密码 45e2土豆:http://www.tudou.com/programs/view/pR_4sY0cJLs/优酷:http://v.youku.com/v_show/id_

Dream------scala--类的属性和对象私有字段实战详解

Scala类的属性和对象私有字段实战详解 一.类的属性 scala类的属性跟java有比较大的不同,需要注意的是对象的私有(private)字段 1.私有字段:字段必须初始化(当然即使不是私有字段也要赋值) 2.属性默认是public级别的,而且无法用public修饰. 3.可以有很多类,并且默认是public级别(如果声明的时候加上会报错,不知为何) 4.如果属性是public的,会默认生成类属性的getter和setter方法,无需显示的提供getter,setter方法 5.私有字段(用p

Scala 深入浅出实战经典 第57讲:Scala中Dependency Injection实战详解

王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 腾讯微云:http://url.cn/TnGbdC 360云盘:http://yunpan.cn/cQ4c2UALDjSKy 访问密码 45e2土豆:http://www.tudou.com/programs/view/5LnLNDBKvi8/优酷:http://v.youku.com/v_show/id_

Scala 深入浅出实战经典 第54讲:Scala中复合类型实战详解

王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-64讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 腾讯微云:http://url.cn/TnGbdC 360云盘:http://yunpan.cn/cQ4c2UALDjSKy 访问密码 45e2土豆:http://www.tudou.com/programs/view/a6qIB7SqOlc/优酷:http://v.youku.com/v_show/id_