shiro分布式之subject的信息维护

在技术群有一位提出

​ shiro框架下获取主题subject信息都是从SecurityUtils.getSubject()方法中获取,跟踪到最后都是从ThreadLocal变量中获取的值,只能在当前内存中存储,那分布式部署的话,会产生获取到的subject的主体信息不一致的问题?

那么 先看下上述问题的代码:

   //SecurityUtils
   public static Subject getSubject() {
        Subject subject = ThreadContext.getSubject();
        if (subject == null) {
            subject = (new Subject.Builder()).buildSubject();
            ThreadContext.bind(subject);
        }
        return subject;
    }
//看下ThreadContext的方法 getSubject->get->getValue->resources
    //1
    public static Subject getSubject() {
        return (Subject) get(SUBJECT_KEY);
    }

//2
   public static Object get(Object key) {
        if (log.isTraceEnabled()) {
            String msg = "get() - in thread [" + Thread.currentThread().getName() + "]";
            log.trace(msg);
        }

        Object value = getValue(key);
        if ((value != null) && log.isTraceEnabled()) {
            String msg = "Retrieved value of type [" + value.getClass().getName() + "] for key [" +
                    key + "] " + "bound to thread [" + Thread.currentThread().getName() + "]";
            log.trace(msg);
        }
        return value;
    }
   //3 
     private static Object getValue(Object key) {
        Map<Object, Object> perThreadResources = resources.get();
        return perThreadResources != null ? perThreadResources.get(key) : null;
    }
    //4
        private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();

单看这些,可能有的朋友也会认同如此,都是从resources这个线程私有变量拿去。

但是我是做过基于redis分布搭建shiro项目的,肯定应该不会存在上述现象,还是查源码为先吧。

1.分析这个问题时,先看这个ThreadLocal变量究竟是哪个大神类在给他赋值?

//ThreadContext类

//获取的变量key,请注意看,无论怎么取subject,key值永远不变  ThreadContext.class.getName() + "_SUBJECT_KEY"

    public static final String SECURITY_MANAGER_KEY = ThreadContext.class.getName() + "_SECURITY_MANAGER_KEY";
    public static final String SUBJECT_KEY = ThreadContext.class.getName() + "_SUBJECT_KEY";
    
    //找到一个引用,置入subject的信息
    public static void bind(Subject subject) {
        if (subject != null) {
            put(SUBJECT_KEY, subject);
        }
    }
    

对bind方法进行跟踪

--SubjectThreadState.bind()
----SubjectCallable.call()
------DelegatingSubject.execute(Callable<V> callable)
----SubjectRunnable.run()
------DelegatingSubject.execute(Runnable runnable)

可以看到SubjectThreadState字面意思应该是线程状态的信息,最终都跟踪到DelegatingSubject这个代理类上,其执行线程方法,更新绑定的subject值;

再继续溯源,找到AbstractShiroFilter类的doFilterInternal。


//
  protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
            throws ServletException, IOException {

        Throwable t = null;

        try {
            final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
            final ServletResponse response = prepareServletResponse(request, servletResponse, chain);

            final Subject subject = createSubject(request, response);

            //noinspection unchecked
            //在该处调用线程方法更新
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    updateSessionLastAccessTime(request, response);
                    executeChain(request, response, chain);
                    return null;
                }
            });
        } catch (ExecutionException ex) {
            t = ex.getCause();
        } catch (Throwable throwable) {
            t = throwable;
        }

看到这里,应该了解到一点,就是每个请求进来的时候,这个主线程会有对应的新线程去更新赋值subject的信息。

2.那么问题又来了,每次请求进来时这个subject信息是在这里生成的,那原来已经登录的用户再次来请求的subject已经变了,怎么登录成功呢?还有没登录的时候,这里会怎么处理呢?

借着这个问题,咱么看一下这个shiro过滤器都干啥了?

​ 一般web项目都指定 ShiroFilter这个过滤器,在初始化到第一个请求到达之前,首先设置了一些环境参数,获取配置信息,初始化一些变量,包含SecurityManager这个核心(可以看init(),onFilterConfigSet()函数)

第一个请求到达进入doFilterInternal方法内,调用prepareServletRequest,prepareServletResponse,判断为http方式时,封装Shiro格式的输入输出流。

还有就是一旦发生请求,那么会话session就已经存在了

接下来创建新的subject具体过程:

//AbstractShiroFilter
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
        return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
    }

跟踪buildWebSubject方法,DefaultSecurityManager类

//DefaultSecurityManager
  public Subject createSubject(SubjectContext subjectContext) {
        //create a copy so we don't modify the argument's backing map:
        //英文不好,自行百度吧
        SubjectContext context = copy(subjectContext);

        //ensure that the context has a SecurityManager instance, and if not, add one:
         //英文不好,自行百度吧
        context = ensureSecurityManager(context);

        //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
        //sending to the SubjectFactory.  The SubjectFactory should not need to know how to acquire sessions as the
        //process is often environment specific - better to shield the SF from these details:
         //这里是重点
        context = resolveSession(context);

        //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
        //if possible before handing off to the SubjectFactory:
        context = resolvePrincipals(context);
        //也是重点
        Subject subject = doCreateSubject(context);

        //save this subject for future reference if necessary:
        //(this is needed here in case rememberMe principals were resolved and they need to be stored in the
        //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
        //Added in 1.2:
        
        save(subject);

        return subject;
    }


重点跟踪resolveSession()方法

  
  protected SubjectContext resolveSession(SubjectContext context) {
        //第一次进来肯定没有缓存值,会进入下面的resolveContextSession
        if (context.resolveSession() != null) {
            log.debug("Context already contains a session.  Returning.");
            return context;
        }
        try {
            //Context couldn't resolve it directly, let's see if we can since we have direct access to 
            //the session manager:
            Session session = resolveContextSession(context);
            if (session != null) {
                context.setSession(session);
            }
        } catch (InvalidSessionException e) {
            log.debug("Resolved SubjectContext context session is invalid.  Ignoring and creating an anonymous " +
                    "(session-less) Subject instance.", e);
        }
        return context;
    }
   //DefaultSecurityManager
   protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
   //这里判断一下是http请求的话,会获取本身session的绘画id
        SessionKey key = getSessionKey(context);
        if (key != null) {
            return getSession(key);
        }
        return null;
    }
  //一直跟踪getSession方法到AbstractNativeSessionManager
 public Session getSession(SessionKey key) throws SessionException {
        Session session = lookupSession(key);
        return session != null ? createExposedSession(session, key) : null;
    }
    
     private Session lookupSession(SessionKey key) throws SessionException {
        if (key == null) {
            throw new NullPointerException("SessionKey argument cannot be null.");
        }
        return doGetSession(key);
    }
    
    
//上面doGetSession调用了了AbstractValidatingSessionManager
 protected final Session doGetSession(final SessionKey key) throws InvalidSessionException {
        enableSessionValidationIfNecessary();

        log.trace("Attempting to retrieve session with key {}", key);

        Session s = retrieveSession(key);
        if (s != null) {
            validate(s, key);
        }
        return s;
    }

//DefaultSessionManager.retrieveSession
 protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
        Serializable sessionId = getSessionId(sessionKey);
        if (sessionId == null) {
            log.debug("Unable to resolve session ID from SessionKey [{}].  Returning null to indicate a " +
                    "session could not be found.", sessionKey);
            return null;
        }
        Session s = retrieveSessionFromDataSource(sessionId);
        if (s == null) {
            //session ID was provided, meaning one is expected to be found, but we couldn't find one:
            String msg = "Could not find session with ID [" + sessionId + "]";
            throw new UnknownSessionException(msg);
        }
        return s;
    }

//DefaultSessionManager.retrieveSessionFromDataSource
 protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {
        return sessionDAO.readSession(sessionId);
    }

一直跟踪到sessionDAO.readSession,这里,从方法字面意思也知道是从缓存或者数据库读取session信息了。就是上面提到的redis缓存,会吧session信息存储。

接下来回头看,在buildWebSubject的方法里面,获取到了缓存的session信息或者是一个全新的session信息,

之后的doCreateSubject方法


//DefaultWebSubjectFactory

public Subject createSubject(SubjectContext context) {
        //SHIRO-646
        //Check if the existing subject is NOT a WebSubject. If it isn't, then call super.createSubject instead.
        //Creating a WebSubject from a non-web Subject will cause the ServletRequest and ServletResponse to be null, which wil fail when creating a session.
        boolean isNotBasedOnWebSubject = context.getSubject() != null && !(context.getSubject() instanceof WebSubject);
        if (!(context instanceof WebSubjectContext) || isNotBasedOnWebSubject) {
            return super.createSubject(context);
        }
        WebSubjectContext wsc = (WebSubjectContext) context;
        SecurityManager securityManager = wsc.resolveSecurityManager();
        //取到session信息
        Session session = wsc.resolveSession();
        boolean sessionEnabled = wsc.isSessionCreationEnabled();
        //如果缓存没有,获取session中的身份信息
        PrincipalCollection principals = wsc.resolvePrincipals();
        //如果缓存没有,获取session中的认证信息
        boolean authenticated = wsc.resolveAuthenticated();
        String host = wsc.resolveHost();
        ServletRequest request = wsc.resolveServletRequest();
        ServletResponse response = wsc.resolveServletResponse();
        //创建新的subject
        return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled,
                request, response, securityManager);
    }


创建完毕以后,接下来就更新缓存内的subject的状态,直至请求结束

到这里每次请求到达filter基本上流程就完毕了,如果已经登录了,无论是从本身的map缓存还是外部的redis缓存,都应该能拿到session信息,如果没有登录,是拿不到的。也证明了分布式部署时,有了session的分布式缓存,应该就基本搭建成功了吧。

参考信息:

https://blog.csdn.net/quanaianzj/article/details/83858575 描述subject

https://blog.csdn.net/u010399009/article/details/78308322 自定义缓存

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容