Spring MockMvc模拟shiro登录

news/2024/5/18 11:03:47 标签: spring, java, 后端, 前端, log4j

背景

最近在使用SpringBoot MockMvc进行controller层的单元测试,在测试的场景中需要用户先进行登录,用户登录使用的安全框架是apache shiro,在使用的过程中发现,使用MockHttpSession无法再用户登录后获取到shiro的session。

解决过程

对于需要模拟用户登录的场景,我们一般的做法是先调用用户的登录接口,然后获取到session,然后使用同样的session进行操作,那么,自然而然想到的就是MockHttpSession,这是一个常规方案,不做详细的追溯,然后这个方案无效。因此,还是要回到源码去解决问题。

在shiro中,我们是其实是先获取到Subject,然后再从Subject中获取session的,代码如下:

 Subject subject = SecurityUtils.getSubject();

getSubject()方法实际是从一个本地线程上下文中获取Subject,

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

ThreadContext其实是ThreadLocal的一层包装,有兴趣的读者可以自行再继续看源代码。这里我们可以看到,如果获取不到Subject,会新建一个进行绑定,但是,在我们的场景里面,用户是先进行登录的,那么,登录之后的操作肯定是从已经绑定到的subject中获取,问题指向了Subject在何时绑定?

在考虑的时候,去shiro官网看了下单元测试的解法,这边也贴一下。shiro官网中定义了一个基类如下:

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.UnavailableSecurityManagerException;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.SubjectThreadState;
import org.apache.shiro.util.LifecycleUtils;
import org.apache.shiro.util.ThreadState;
import org.junit.AfterClass;

/**
 * Abstract test case enabling Shiro in test environments.
 */
public abstract class AbstractShiroTest {

    private static ThreadState subjectThreadState;

    public AbstractShiroTest() {
    }

    /**
     * Allows subclasses to set the currently executing {@link Subject} instance.
     *
     * @param subject the Subject instance
     */
    protected void setSubject(Subject subject) {
        clearSubject();
        subjectThreadState = createThreadState(subject);
        subjectThreadState.bind();
    }

    protected Subject getSubject() {
        return SecurityUtils.getSubject();
    }

    protected ThreadState createThreadState(Subject subject) {
        return new SubjectThreadState(subject);
    }

    /**
     * Clears Shiro's thread state, ensuring the thread remains clean for future test execution.
     */
    protected void clearSubject() {
        doClearSubject();
    }

    private static void doClearSubject() {
        if (subjectThreadState != null) {
            subjectThreadState.clear();
            subjectThreadState = null;
        }
    }

    protected static void setSecurityManager(SecurityManager securityManager) {
        SecurityUtils.setSecurityManager(securityManager);
    }

    protected static SecurityManager getSecurityManager() {
        return SecurityUtils.getSecurityManager();
    }

    @AfterClass
    public static void tearDownShiro() {
        doClearSubject();
        try {
            SecurityManager securityManager = getSecurityManager();
            LifecycleUtils.destroy(securityManager);
        } catch (UnavailableSecurityManagerException e) {
            //we don't care about this when cleaning up the test environment
            //(for example, maybe the subclass is a unit test and it didn't
            // need a SecurityManager instance because it was using only
            // mock Subject instances)
        }
        setSecurityManager(null);
    }
}

这个基类主要的就是设置SecurityManager和Subject,在使用时,使用如下方式:

import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;

public class ExampleShiroIntegrationTest extends AbstractShiroTest {

    @BeforeClass
    public static void beforeClass() {
        //0.  Build and set the SecurityManager used to build Subject instances used in your tests
        //    This typically only needs to be done once per class if your shiro.ini doesn't change,
        //    otherwise, you'll need to do this logic in each test that is different
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:test.shiro.ini");
        setSecurityManager(factory.getInstance());
    }

    @Test
    public void testSimple() {
        //1.  Build the Subject instance for the test to run:
        Subject subjectUnderTest = new Subject.Builder(getSecurityManager()).buildSubject();

        //2. Bind the subject to the current thread:
        setSubject(subjectUnderTest);

        //perform test logic here.  Any call to
        //SecurityUtils.getSubject() directly (or nested in the
        //call stack) will work properly.
    }

    @AfterClass
    public void tearDownSubject() {
        //3. Unbind the subject from the current thread:
        clearSubject();
    }
} 

这个用例在spring环境下,不需要从ini中获取SecurityManager,直接注入即可。参考这段代码,实际我们要做的是在setSubject之后,使用Subject登录,然后进行MockMvc的操作。那么testSimple函数可以这么写:

        //1.  Build the Subject instance for the test to run:
        Subject subjectUnderTest = new Subject.Builder(getSecurityManager()).buildSubject();

        //2. Bind the subject to the current thread:
        setSubject(subjectUnderTest);
        Subject subject = SecurityUtils.getSubject();
        UserPasswordToken token = new UserPasswordToken("admin", "test");
        subject.login(token);
        mockMvc.perform(post("/public/auth/getVerifyCode")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
                .param("phoneNumber", "18812345678")).andDo(print())
                .andExpect(status().isOk());

在调用这个测试方法的时候,我们发现实际在应用中获取到的Subject并不是我们在单元测试方法中绑定的Subject,这说明MockMvc在模拟请求时另行绑定了Subject,那么现在我们就考虑到shiro是什么时候对应用进行操作的?

答案是ShiroFilter。 ShiroFilter承担着shiro框架最核心的工作,它对servlet的filterchain进行了代理,在接受到请求时,会先执行shiro自己的filter链,再执行servlet容器的filter链。ShiroFilter继承自AbstractShiroFilter,在AbstractShiroFilter的doFilterInternal方法中,我们看到了如下的一段代码:

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

        final Subject subject = createSubject(request, response);

熟悉的味道,CreateSubject!在shiro执行自己的链的时候,它会去创建一个Subject,这就解释了MockMvc在模拟请求时另行绑定Subject的现象。那么,createSubject具体是如何执行的呢?用户登录之后又是如何获取到对应的session呢?是这一段代码:

    //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);

在resolveSession方法中会根据上下文中request的cookie获取到JSESSIONID,从而读取存储的session,进行subject的关联,最终获取sessionid的方法是在org.apache.shiro.web.session.mgt.DefaultWebSessionManager#getSessionIdCookieValue。

经过上面的分析,我们可以知道,要绑定对应的Subject,其实就是要能关联到登录的session,也就是说在MockMvc中要带上对应的Cookie,那代码可以如下:

mockMvc.perform(post("/public/auth/getVerifyCode")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
                .param("phoneNumber", "18812345678").cookie(cookie)).andDo(print())
                .andExpect(status().isOk());

至于cookie如何获取,mockMvc将result print出来之后应该就一目了然了,读者可以自行尝试。

有问题,可联系wgy@live.cn。


http://www.niftyadmin.cn/n/1276486.html

相关文章

Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway at this time

我已经把父工程中以下依赖移除掉了&#xff0c;但是她还是报Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway at this time. 说我依赖重复了&#xff0c;重复就重复了呗&#xff0c;为啥还报错还就搞不明白了 <dependency><groupId…

密码等级:至少包含字母、大小写数字、特殊字符 JS

前言 密码&#xff0c;如果设置的太简单&#xff0c;很容易就被攻破&#xff0c;所以很多网站将密码设置的要求设置的挺严格&#xff0c;一般是字母、数字、字符3选2&#xff0c;区分大小写。对于设置得太简单的密码&#xff0c;予以错误提示。或者予以密码等级&#xff08;低…

SpringBoot 部署Dubbo3.0相关2021版本

SpringBoot 部署Dubbo3.0 部署2021年dubbo3.0相关的版本 SpringBoot 2.4.3安装 zookeeper-3.7.0 单机引入依赖 dubbo-spring-boot-starter 3.0.4、org.apache.curator 5.2.0 Curator 框架提供了一套高级的 API&#xff0c;简化了 ZooKeeper 的操作。它增加了很多使用 ZooKeep…

浅析ListView实现原理

为什么80%的码农都做不了架构师&#xff1f;>>> 浅析ListView实现原理 --为什么要使用getview&#xff08;&#xff09;中的convertView 2009的Google Io大会中有一个专门培训ListView使用的课程说到&#xff0c;使用ListView 最快最优化&#xff08;Fast Way&…

Springboot 配置H2数据库

1. 配置文件 spring.thymeleaf.cachefalsespring.resources.cache-period0server.port80?spring.datasource.urljdbc:h2:file:./db/test01#spring.datasource.urljdbc:h2:mem:test#spring.datasource.urljdbc:h2:mem:test;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSEspring.datas…

kindeditor.net应用

1.网址&#xff1a;http://kindeditor.net/docs/usage.html转载于:https://www.cnblogs.com/ChineseMoonGod/p/5018818.html

web安全 应用程序错误威胁

描述 如果攻击者通过伪造包含非应用程序预期的参数或参数值的请求&#xff0c;来探测应用程序&#xff0c;那么应用程序可能会进入易受攻击的未定义状态。攻击者可以从应用程序对该请求的响应中获取有用的信息&#xff0c;且可利用该信息&#xff0c;以找出应用程序的弱点。 …

spring 基础

Spring 是一个开源的框架&#xff0c;为了解决企业应用开发的复杂性而创建的&#xff0c;但是现在已经不止应用于企业。spring是一个轻量级的控制反转(Ioc)和面向切面(AOP)的容器框架&#xff0c;从大小和开销两个方面而言spirng是轻量级的通过控制反转(Ioc)的技术达到松耦合的…