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 自定义缓存

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容