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操作)


相关文章
|
8月前
|
数据采集 Web App开发 数据可视化
Python爬虫分析B站番剧播放量趋势:从数据采集到可视化分析
Python爬虫分析B站番剧播放量趋势:从数据采集到可视化分析b
|
安全 存储
Shiro源码剖析——Subject的创建与获取(一次完整的请求执行流程-1
本文可能较长,但是通读一定能让你对整个shiro请求的执行流程有清晰的了解 总体流程: 1、在过滤的过程中创建subject doFilter -&gt; SecurityManager -&gt; SubjectContext -&gt; 创建subject -&gt; 解析各种信息并赋值 2、若该subject未认证则进行认证并在认证时再次创建subject 调用realm中的doAuthenticationInfo()获得返回的信息重新创建subject并保存到session
|
存储 安全 API
OpenStack的块存储卷管理快照 (Snapshot)
【8月更文挑战第26天】
880 13
|
机器学习/深度学习 存储 并行计算
Differential Transformer: 通过差分注意力机制提升大语言模型性能
《Differential Transformer》论文提出了一种新的差分注意力机制,旨在解决传统Transformer模型过分关注不相关信息的问题。该机制通过计算两个独立的注意力图谱之差来消除注意力噪声,提高模型性能。实验结果显示,DIFF Transformer在减少参数量和训练token数量的同时,显著提升了多目标检索任务的准确率。
838 11
Differential Transformer: 通过差分注意力机制提升大语言模型性能
|
前端开发 JavaScript Java
Java打包jar运行时分离lib和jar
在`pom.xml`的`build`节点中,设置`packaging`为`jar`,并配置插件分离依赖库到`lib`目录和资源文件到`resources`目录。这样可以在运行时通过`-Dloader.path=lib,resources`加载外部依赖和资源文件,便于独立升级依赖库和修改资源文件,而无需重新打包程序。具体插件包括`maven-dependency-plugin`、`maven-resources-plugin`和`spring-boot-maven-plugin`等。
856 2
|
安全 Java 程序员
shiro学习三:shiro的源码分析
这篇文章是关于Apache Shiro安全框架的源码分析,主要探讨了Shiro的认证流程和自定义Realm的实现细节。
318 0
shiro学习三:shiro的源码分析
|
存储 缓存 数据库
【万字长文】微服务整合Shiro+Jwt,源码分析鉴权实战
介绍如何整合Spring Boot、Shiro和Jwt,以实现一个支持RBAC的无状态认证系统。通过生成JWT token,实现用户无状态登录,并能根据用户角色动态鉴权,而非使用Shiro提供的注解,将角色和权限信息硬编码。此外,文章还探讨了如何对Shiro的异常进行统一捕获和处理。作为应届生,笔者在学习Shiro的过程中进行了一些源码分析,尽管可能存在不足和Bug,但希望能为同样需要实现权限管理的开发者提供参考,并欢迎各位大佬指正完善。
807 65
【万字长文】微服务整合Shiro+Jwt,源码分析鉴权实战
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
540 0
|
存储 XML Java
【Maven技术专题】「入门到精通」教你如何使用Maven中引用依赖本地Jar包,并进行打包输出
【Maven技术专题】「入门到精通」教你如何使用Maven中引用依赖本地Jar包,并进行打包输出
2973 0

热门文章

最新文章