Shiro源码剖析——Subject的创建与获取(一次完整的请求执行流程-2

简介: 三、subject.execute()

三、subject.execute()

// 将该subject绑定到当前线程,并更新会话的上次访问时间以及分发合适的过滤器
subject.execute(new Callable() {
    public Object call() throws Exception {
        AbstractShiroFilter.this.updateSessionLastAccessTime(request, response);
        AbstractShiroFilter.this.executeChain(request, response, chain);
        return null;
    }
});

1、execute()

追踪execute(),来到DelegatingSubject类

public <V> V execute(Callable<V> callable) throws ExecutionException {
    Callable associated = this.associateWith(callable);
    try {
        return associated.call();
    } catch (Throwable var4) {
        throw new ExecutionException(var4);
    }
}

this.associateWith(callable)通过参数callable创建了一个SubjectCallable对象,所以我们查看SubjectCallable类中的call方法,如下:

public V call() throws Exception {
    Object var1;
    try {
        this.threadState.bind();
        var1 = this.doCall(this.callable);
    } finally {
        this.threadState.restore();
    }
    return var1;
}

this.threadState.bind();方法将subject和securityManager绑定到当前线程的resources(一个map),如下

public void bind() {
    SecurityManager securityManager = this.securityManager;
    if (securityManager == null) {
        securityManager = ThreadContext.getSecurityManager();
    }
    this.originalResources = ThreadContext.getResources();
    ThreadContext.remove();
    ThreadContext.bind(this.subject);
    if (securityManager != null) {
        ThreadContext.bind(securityManager);
    }
}

doCall(this.callable)调用回调方法

2、updateSessionLastAccessTime(request, response)

更新会话上次访问时间

protected void updateSessionLastAccessTime(ServletRequest request, ServletResponse response) {
    if (!this.isHttpSessions()) {
        Subject subject = SecurityUtils.getSubject();
        if (subject != null) {
            Session session = subject.getSession(false);
            if (session != null) {
                try {
                    session.touch();
                } catch (Throwable var6) {
                    log.error("session.touch() method invocation has failed.  Unable to update the corresponding session's last access time based on the incoming request.", var6);
                }
            }
        }
    }
}

3、executeChain(request, response, chain)

将请求分发给合适的过滤器

此部分不做详解

四、认证时的createSubject()

基本认证流程如下:

Subject subject = SecurityUtils.getSubject();
subject.login(new UsernamePasswordToken("cyh", "123"));

1、getSubject()

追踪源码发现getSubject最终执行的是SecurityUtils类中的这么一个方法

public static Subject getSubject() {
    Subject subject = ThreadContext.getSubject();
    if (subject == null) {
        subject = (new Builder()).buildSubject();
        ThreadContext.bind(subject);
    }
    return subject;
}

从上文的分析中我们知道了每一次访问都会重新建立subject然后绑定到当前线程,所以当前线程中获得subject不会是null。只有在没有配置shirofilter的应用中才会出现当前线程中subject==null的情况,所以这里就可以拿到前面过滤过程中创建的subject

2、subject.login()

随后通过当前的subject和token调用login函数

public void login(AuthenticationToken token) throws AuthenticationException {
    clearRunAsIdentitiesInternal();
    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;
    }
    Session session = subject.getSession(false);
    if (session != null) {
        this.session = decorate(session);
    } else {
        this.session = null;
    }
}

其在最终会通过调用realm中的认证方法获得当前用户的info

Subject loggedIn = createSubject(token, info, subject);

随后在此处又创建了一次Subject

那么两次创建subject有什么区别呢

protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
    SubjectContext context = createSubjectContext();
    // 保存登陆状态
    context.setAuthenticated(true);
    // 保存token
    context.setAuthenticationToken(token);
    // 保存认证的返回信息
    context.setAuthenticationInfo(info);
    context.setSecurityManager(this);
    if (existing != null) {
        // 保存subject的信息到上下文
        context.setSubject(existing);
    }
    // 开始创建subject
    return createSubject(context);
}


可以看到此处的创建其实就是在上下文中额外增加了一些信息,之后也是通过调用上面过滤时用的那个函数createSubject(SubjectContext subjectContext)创建的subject

public Subject createSubject(SubjectContext subjectContext) {
    SubjectContext context = this.copy(subjectContext);
    context = this.ensureSecurityManager(context);
    context = this.resolveSession(context);
    context = this.resolvePrincipals(context);
    Subject subject = this.doCreateSubject(context);
    this.save(subject);
    return subject;
}

这里着重看save函数,前面我们进入到

public Subject save(Subject subject) {
    if (isSessionStorageEnabled(subject)) {
        saveToSession(subject);
    } else {
        log.trace("Session storage of subject state for Subject [{}] has been disabled: identity and " +
                "authentication state are expected to be initialized on every request or invocation.", subject);
    }
    return subject;
}

如果开启session,则将subject保存至session中,这里我们接着saveToSession(subject)往下追踪,来到

protected void saveToSession(Subject subject) {
    mergePrincipals(subject);
    mergeAuthenticationState(subject);
}

先看mergePrincipals(subject)函数

protected void mergePrincipals(Subject subject) {
    PrincipalCollection currentPrincipals = null;
    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 = subject.getSession(false);
    if (session == null) {
        if (!isEmpty(currentPrincipals)) {
            session = subject.getSession();
            session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
        }
    } else {
        PrincipalCollection existingPrincipals =
                (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
        if (isEmpty(currentPrincipals)) {
            if (!isEmpty(existingPrincipals)) {
                session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            }
        } else {
            if (!currentPrincipals.equals(existingPrincipals)) {
                session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
            }
        }
    }
}

其中先通过反射获取subject中的principals属性

try {
       Field field = DelegatingSubject.class.getDeclaredField("principals");
       field.setAccessible(true);
       currentPrincipals = (PrincipalCollection)field.get(subject);
}

显然当我们在过滤时创建的subject是没有这个属性的(除非得到了session,通过session获取保存的principals),只有在登陆认证的时候之后才有这个属性

if (!isEmpty(currentPrincipals)) {
       session = subject.getSession();
       session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
}


以下都是针对第一次请求,没有能够获取到cookie中的session的情况


这里表示如果currentPrincipals不为空则获取session,获取不到则创建session并保存principals,过滤时的save函数不会进去这个代码段,也就没有创建session。


到这我们就清晰了,在最开始过滤时,显然是没有principals的,虽然前面执行了resolvePrincipals(context)函数,但是此时context上下文中是没有principals的信息的,所以过滤操作时处我们并不会去创建一个session


而在认证时再次执行save操作的时候,此时已经调用了认证获得了对应的info,info中解析出了principals信息在调用resolvePrincipals(context)时就保存了该信息,所以save时就会因此创建一个session,随后存放principals信息在session中


下方的mergeAuthenticationState(subject)函数其实也同理,在认证之后会将该状态保存到session中,然后 SessionId 写到 Cookies 中(DefaultWebSessionManager的onstart方法)


subject 创建出来之后,暂且叫内部subject,就是把认证通过的内部subject的信息和session复制给我们外界使用的subject.login(token)的subject中,这个subject暂且叫外部subject,看下session的赋值,又进行了一次装饰(decorate),这次装饰则把session(类型为StoppingAwareProxiedSession,即是内部subject和session的合体)和外部subject绑定到一起。

五、用户认证后的后续请求

除了手动调用login会进入认证,其他请求过来时也会先检测当前用户是否进行了认证(即subject.authenticated是否为true),没有则先认证,有则不需要。


现在我们已经知道使用了全局的Session为我们保存了认证状态, 并且每次请求就会从这个全局Session中取出认证状态并保存到新创建的Subject中即可,不需要再执行login,也不需调用doGetAuthenticationInfo()方法


现在, 我们只需要保证前后两次请求是从同一个Session中取的认证状态即可(即两次请求在同一个会话中),这只要通过sessionId便可以实现,即


当用户访问其他接口时,经过过滤器时会创建Subject,在其中执行resovleSession时便会调用方法获取请求携带的cookie,随后通过Cookies 中的SessionId 获取到Session,并将 Session 中的信息合并到当前Subject中,此时当前线程的 Subject.authenticated = true, Subject.principals 保存了用户信息。


随后经过过滤器验证,如指定的是 authc 过滤器,则认为是 FormAuthenticationFilter 来执行此处请求的验证。而 FormAuthenticationFilter 的判断条件是 Subject.authenticated 是否为true,因为已经合并了session中保存的subject的认证信息,故校验通过。(如果没有session,则在此处需要重新执行login操作)


相关文章
|
6月前
|
缓存 前端开发 Java
【二十八】springboot之通过threadLocal+参数解析器实现同session一样保存当前登录信息的功能
【二十八】springboot之通过threadLocal+参数解析器实现同session一样保存当前登录信息的功能
162 1
|
22天前
|
缓存 Java Spring
servlet和SpringBoot两种方式分别获取Cookie和Session方式比较(带源码) —— 图文并茂 两种方式获取Header
文章比较了在Servlet和Spring Boot中获取Cookie、Session和Header的方法,并提供了相应的代码实例,展示了两种方式在实际应用中的异同。
96 3
servlet和SpringBoot两种方式分别获取Cookie和Session方式比较(带源码) —— 图文并茂 两种方式获取Header
|
6月前
|
Java Spring
SpringBoot+async异步调用接口以及几个任务同时完成和异步接口实现和调用
SpringBoot+async异步调用接口以及几个任务同时完成和异步接口实现和调用
128 0
|
安全 存储
Shiro源码剖析——Subject的创建与获取(一次完整的请求执行流程-1
本文可能较长,但是通读一定能让你对整个shiro请求的执行流程有清晰的了解 总体流程: 1、在过滤的过程中创建subject doFilter -&gt; SecurityManager -&gt; SubjectContext -&gt; 创建subject -&gt; 解析各种信息并赋值 2、若该subject未认证则进行认证并在认证时再次创建subject 调用realm中的doAuthenticationInfo()获得返回的信息重新创建subject并保存到session
|
6月前
|
JSON 前端开发 Java
【JavaEE进阶】 Spring请求如何传递参数详解
【JavaEE进阶】 Spring请求如何传递参数详解
|
消息中间件 设计模式 Java
Spring 四种方式教你异步接口返回结果
Spring 四种方式教你异步接口返回结果
Spring 四种方式教你异步接口返回结果
|
安全 Java UED
SpringBoot 如何使用 @Async 注解处理异步事件
SpringBoot 如何使用 @Async 注解处理异步事件
|
Java Spring
SpringMVC源码分析:一个request请求的完整流程和各组件介绍
SpringMVC源码分析:一个request请求的完整流程和各组件介绍
46 0
|
存储 XML Java
【spring源码系列-06】refresh中obtainFreshBeanFactory方法的执行流程
【spring源码系列-06】refresh中obtainFreshBeanFactory方法的执行流程
101 0
|
负载均衡 Java 数据处理
案例03-fegin调用报404问题
fegin调用报404问题
148 0