zoukankan      html  css  js  c++  java
  • shiro实现无状态的会话,带源码分析

    转载请在页首明显处注明作者与出处

    朱小杰        http://www.cnblogs.com/zhuxiaojie/p/7809767.html

     

     

    一:说明

    在网上都找不到相关的信息,还是翻了大半天shiro的源码才找到答案。亲试绝对可行,带源码分析

    很多时候,开发的项目不仅仅是一个基于浏览器的项目,还可能是基于app的项目,基于小程序的项目,而这些项目都是无状态的。而普通web项目中,一个web项目的会话是由session保持的,而session又是由浏览器携带的cookie来验证身份的,可以这么说,一个会话就是依赖于cookie,但是app与小程序是没有cookie维持的。

      一般的作法会在header中带有一个token,或者是在参数中,后台根据这个token来进行校验这个用户的身份,但是这个时候,servlet中的session就无法保存,我们在这个时候,就要实现自己的会话创建,普通的作法就是重写session与request的接口,然后在过滤器在把它替换成自己的request,所以得到的session也是自己的session,然后根据token来创建和维护会话。

      但在shiro中会怎么做呢?

    二:shiro介绍

       shiro是一个权限验证框架,它比spring security的功能要少一些,但是我却更喜欢shiro,因为spring security封装的太死了,如果要重写一些功能,特别的麻烦,而shiro中使用了大量的策略模式,使得开发人员可以很好的替换成自己的策略,灵活性更加强,可以定义自己的过滤器来实现自己需要的一些功能。

      shiro中的权限操作是委托给securityManager的,而securityManager管理session又是委托给sessionManager的,在开发web项目中,我们一般会使用

    org.apache.shiro.web.mgt.DefaultWebSecurityManager

    来创建securityManager,我们看一下这个DefaultWebSecurityManager默认是使用的哪个session管理器,它的构造方法如下

        public DefaultWebSecurityManager() {
            super();
            ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
            this.sessionMode = HTTP_SESSION_MODE;
            setSubjectFactory(new DefaultWebSubjectFactory());
            setRememberMeManager(new CookieRememberMeManager());
            setSessionManager(new ServletContainerSessionManager());//这里可以看到是使用的servlet的默认管理器
        }

    可以看到,如果构造一个DefaultWebSecurityManager,它使用的是

    org.apache.shiro.web.session.mgt.ServletContainerSessionManager

    它是依赖于浏览器的cookie来维护session的,那肯定不能实现无状态的会话。

    不过shiro还提供了另一个基于web的session管理器,它就是

    org.apache.shiro.web.session.mgt.DefaultWebSessionManager

    如果我们想实现自己的一套session管理器,都会选择去继承它来重写

    小提示:笔者1.4.0的版本,当前是最新版本,无法直接在security中设置sessionManager的时候,直接new一个DefaultWebSessionManager,如下:

        @Bean
        public SecurityManager securityManager(){
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setSessionManager(new DefaultWebSessionManager());
            securityManager.setRealm(new WebRealm());
            return securityManager;
        }

    如果直接设置为DefaultWebSessionManager,那么在有http请求的时候会报错,提示找不到SecurityManager,解决办法是写一个类来继承它,哪怕继承后什么都不做,都可以解决这个问题

     

    三:重写shiro的sessionManager

    上面说到我们要重写DefaultWebSessionManager,那我们要怎么重写呢?

    import org.apache.shiro.session.mgt.SessionKey;
    import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
    import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
    import org.apache.shiro.web.util.WebUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.Serializable;
    import java.util.UUID;
    
    /**
     * @author zxj<br>
     * 时间 2017/11/8 15:55
     * 说明 ...
     */
    public class StatelessSessionManager extends DefaultWebSessionManager {
        /**
         * 这个是服务端要返回给客户端,
         */
        public final static String TOKEN_NAME = "TOKEN";
        /**
         * 这个是客户端请求给服务端带的header
         */
        public final static String HEADER_TOKEN_NAME = "token";
        public final static Logger LOG = LoggerFactory.getLogger(StatelessSessionManager.class);
    
    
        @Override
        public Serializable getSessionId(SessionKey key) {
            Serializable sessionId = key.getSessionId();
            if(sessionId == null){
                HttpServletRequest request = WebUtils.getHttpRequest(key);
                HttpServletResponse response = WebUtils.getHttpResponse(key);
                sessionId = this.getSessionId(request,response);
            }
            HttpServletRequest request = WebUtils.getHttpRequest(key);
            request.setAttribute(TOKEN_NAME,sessionId.toString());
            return sessionId;
        }
    
        @Override
        protected Serializable getSessionId(ServletRequest servletRequest, ServletResponse servletResponse) {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String token = request.getHeader(HEADER_TOKEN_NAME);
            if(token == null){
                token = UUID.randomUUID().toString();
            }
    
            //这段代码还没有去查看其作用,但是这是其父类中所拥有的代码,重写完后我复制了过来...开始
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                    ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
            //这段代码还没有去查看其作用,但是这是其父类中所拥有的代码,重写完后我复制了过来...结束
            return token;
        }
    
    }

    三:源码分析

    上面就是完整的重写的代码,我们一个一个方法来看

    3.1:第一个方法

    public Serializable getSessionId(SessionKey key)

    这个方法的覆盖和它的父类其实没有太大的区别,逻辑上面都是通过一个sessionKey来获取一个sessionId,但是重写的部分多了一个把获取到的token设置到request的部分,这是因为app调用登陆接口的时候,是没有token的,登陆成功后,产生了token,我们把它放到request中,返回结果给客户端的时候,把它从request中取出来,并且传递给客户端,客户端每次带着这个token过来,就相当于是浏览器的cookie的作用,也就能维护会话了

    这里不得不说一下sessionId和sessionKey的区别了,本人也是因为这个东西坑了好久,从字面上面看,sessionKey是一个对象,而sessionId是一个serializable对象,实际上从我们返回的token可以知道,它就是一个String。

    sessionKey是在sessionStore中,对应存储的key值,而sessionId则就是请求带来的token,或者是浏览器请求的cookie中的jsessionid。

    我们要想象一个,他们有什么关系呢?我们通过sessionId,应该得到sessionKey,然后通过sessionKey,能在sessionStore中找到session,那我们就把sessionId与sessionKey相等吧,这样就不用找对应关系了,因为sessionId就等于sessionKey的话,那我们也不需要保存他们之间的对应关系了,而其实DefaultWebSessionManager也是这样做的,因数sessionKey这个对象里面就有一个sessionId。

    但是有一个值得注意的是,这个方法会被调用多次,用户登陆成功以后,会话保持成功后,怎么调用,传入的sessionKey都是一样的,但是我们把镜头拉到用户登陆的那一次请求中,就会发现一些不同的地方了。

      我们可以看到,第一次调用时,sessionKey里面的sessionId是空的,按照我们的逻辑,我们会调用第二个方法,取得header中的token,然后返回sessionId为token。

      断点继续,第二次调用的时候,也会传入一个sessionKey,但是这个sessionKey里面的sessionId值却已经有了,它是一个uuid,但是sessionKey里面的sessionId,与第一次返回的sessionId不一致,或者说和我们的token不一致,这是为什么呢?

      因为当得到sessionId时,session管理器会尝试到sessionStore中通过这个sessionKey去获取一个session,但是可以肯定的是,这个session肯定是得不到的,因为还没有代码给它创建,所以当检测到获取到的session为null的时候,会调用sessionStore的createSession方法,这个时候,它会生成一个随机的sessionId,然后根据这个新生成的sessionId,创建一个session,然后会把这个sessionId设置到sessionKey里面,替换掉之前的sessionId,所以我们在这个方法后面的几次调用就就会发现第一次不一样,sessionId也和第一次返回的sessionId不一样,因为它创建session的时候生成了一个新的sessionId,这个时候我们要怎么办呢?

    我们就修改客户端的token,让它与最新生成的sessionId一致就行了,所以之前说的,这里面有一个把token设置到request中的代码,就是在返回给客户端的时候,通知给客户端最新的token,而不是继续沿用之前的token,因为这个token在sessionStore中是没法取出一个session的。

    还有一个要注意的地方,我们从request取出新的token返回给客户端的时候,要在认证完成之后,因为只有当认证完成之后,才会创建session,才会得到最新的token并返回给客户端,不然返回的是老的token。

    代码如下:

     @RequestMapping("/")
        public void login(@RequestParam("code")String code, HttpServletRequest request){
            Map<String,Object> data = new HashMap<>();
            if(SecurityUtils.getSubject().isAuthenticated()){
            //这里代码着已经登陆成功,所以自然不用再次认证,直接从rquest中取出就行了, data.put(StatelessSessionManager.HEADER_TOKEN_NAME,getServerToken()); data.put(BIND,ShiroKit.getUser().getTel()
    != null); response(data); } LOG.info("授权码为:" + code); AuthorizationService authorizationService = authorizationFactory.getAuthorizationService(Constant.clientType); UserDetail authorization = authorizationService.authorization(code); Oauth2UserDetail userDetail = (Oauth2UserDetail) authorization; loginService.login(userDetail); User user = userService.saveUser(userDetail,Constant.clientType.toString()); ShiroKit.getSession().setAttribute(ShiroKit.USER_DETAIL_KEY,userDetail); ShiroKit.getSession().setAttribute(ShiroKit.USER_KEY,user); data.put(BIND,user.getTel() != null);
          //这里的代码,必须放到login之执行,因为login后,才会创建session,才会得到最新的token咯 data.put(StatelessSessionManager.HEADER_TOKEN_NAME,getServerToken()); response(data); }

    我们把token返回给客户端,然后客户端每次请求时,带上这个token,我们就维持这个会话了

    3.2:第二个方法

    方法签名如下

    protected Serializable getSessionId(ServletRequest servletRequest, ServletResponse servletResponse) 

    第二个方法相对简单,因为仅仅是获取token而已,可以从header获取,参数中获取,cookie中获取,当然用户第一次请求的时候,肯定是没有token的,只有登陆成功后才会得到token,所以当token为null的时候,我们生成了一个uuid,但是这个uuid并不会成为后面的token,这个在上面有讲到,因为会被后面生成session时生成的sessionId给替换掉。

    而至少那一堆设置数据到request中的代码,我也没去看具体做什么用的,因为它的父类中,执行这个方法的时候,有这些代码的设置,复制过来,怕出什么问题。

    四:完整配置代码

    完整的配置代码如下:

    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.realm.Realm;
    import org.apache.shiro.spring.LifecycleBeanPostProcessor;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    /**
     * @author zxj<br>
     * 时间 2017/11/8 15:40
     * 说明 ...
     */
    @Configuration
    public class ShiroConfiguration {
    
        @Bean
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
            return new LifecycleBeanPostProcessor();
        }
    
        /**
         * 此处注入一个realm
         * @param realm
         * @return
         */
        @Bean
        public SecurityManager securityManager(Realm realm){
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setSessionManager(new StatelessSessionManager());
            securityManager.setRealm(realm);
            return securityManager;
        }
    
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
            ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
            bean.setSecurityManager(securityManager);
    
            Map<String,String> map = new LinkedHashMap<>();
            map.put("/public/**","anon");
            map.put("/login/**","anon");
            map.put("/**","user");
            bean.setFilterChainDefinitionMap(map);
    
            return bean;
        }
    }

     

    其实完整的配置代码都已经不重要了,重要的就是sessionManager,上面红色部分说明了怎么把我们自己写的sessionManager设置到securityManager中。

  • 相关阅读:
    关于使用gitlab协同开发提交代码步骤
    一些JavaScript中原理的简单实现
    关于JavaScript中bind、applay、call的区别
    在腾讯云centos7.2上安装配置Node.js记录
    JAVA Web期末项目第三阶段成果
    在腾讯云服务器上安装JDK+Tomcat并启动tomcat
    《JavaScript算法》二分查找的思路与代码实现
    将本地的一个项目托管到自己的GitHub仓库
    《JavaScript算法》常见排序算法思路与代码实现
    项目经理建议:管理时间等于管理自己
  • 原文地址:https://www.cnblogs.com/zhuxiaojie/p/7809767.html
Copyright © 2011-2022 走看看