MockMVC登录后测试SpringBoot项目包含Shiro Subject的控制层方法

UnavailableSecurityManagerException

在常规SpringBoot项目中,我们往往在单元测试类中直接使用@Autowired注解注入Bean实例,并在Test方法中调用实例方法。但如果该项目加入了Shiro安全框架,并且在某个被测试的实例方法中存在获取当前Shiro Subject对象的方法:

package com.jake.manager.controller;

import com.jake.manager.constant.LoginConstants;
import com.jake.manager.exception.NoEmployeeException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import static com.jake.manager.constant.ExceptionConstants.*;

@RestController
@Api("登录相关接口")
public class LoginController {

    private static final Logger logger = LoggerFactory.getLogger(LoginController.class);

    @GetMapping("authentication")
    @ApiOperation(value = "用户名密码校验", notes = "基于Shiro")
    public String authenticate(String account, String password, String rememberMe) {
        UsernamePasswordToken token = new UsernamePasswordToken(account, password,
                StringUtils.equals(rememberMe, LoginConstants.REMEMBER_ME));
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
        } catch (NoEmployeeException e) {
            logger.error(NO_EMPLOYEE_EXCEPTION);
            return NO_EMPLOYEE_EXCEPTION;
        } catch (AuthenticationException e) {
            logger.error(ERROR_PSWD_EXCEPTION);
            return ERROR_PSWD_EXCEPTION;
        }

        return LoginConstants.REDIRECT_TO_INDEX;
    }

}

那么很有可能会抛出以下异常:
Spring integration test with Shiro cause UnavailableSecurityManagerException

MockMVC先行登录

需要使用JUnit的@Before注解MockMVC的登录方法,即该登录方法在每个单元测试方法执行前都需要执行一遍。

package com.jake.manager.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.web.subject.WebSubject;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static com.jake.manager.constant.LoginConstants.CORRECT_ACCOUNT;
import static com.jake.manager.constant.LoginConstants.CORRECT_PSWD;

@RunWith(SpringRunner.class)
@SpringBootTest
public abstract class BaseMockBeforeTests {

    @Autowired
    private SecurityManager securityManager;

    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @Before
    public void loginByMock() {
        MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest(webApplicationContext.getServletContext());
        MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
        MockHttpSession mockHttpSession = new MockHttpSession(webApplicationContext.getServletContext());
        mockHttpServletRequest.setSession(mockHttpSession);
        SecurityUtils.setSecurityManager(securityManager);
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
        Subject subject = new WebSubject
                .Builder(mockHttpServletRequest, mockHttpServletResponse)
                .buildWebSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(CORRECT_ACCOUNT, CORRECT_PSWD);
        subject.login(token);
        ThreadContext.bind(subject);
    }

    String getReturnValue(String uri) throws Exception {
        return mockMvc.perform(MockMvcRequestBuilders.get(uri))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andReturn()
                .getResponse()
                .getContentAsString();
    }
}

此处将Mock登录类抽取为一个基类,注意需要将该类声明为抽象类。否则会报“No Runnable Methods”,这是因为BaseMockBeforeTests中没有单元测试方法,所以产生异常:已加载至SpringBootTest容器中的单元测试类中没有可运行的单元测试方法。若将该类声明为抽象类,则该类不会被加载进SpringBootTest容器,而是根据多态,加载其子类对象。
登录代码完成后,对LoginController的单元测试类代码如下:

package com.jake.manager.controller;

import org.junit.Test;

import static com.jake.manager.constant.ExceptionConstants.*;
import static com.jake.manager.constant.LoginConstants.*;
import static org.junit.Assert.*;

public class LoginControllerTests extends BaseMockBeforeTests {

    @Test
    public void authenticateByCorrectAccountAndPassword() throws Exception {
        assertEquals(REDIRECT_TO_INDEX,
                getReturnValue(getBuiltUri(CORRECT_ACCOUNT, CORRECT_PSWD)));
    }

    @Test
    public void authenticateByWrongAccount() throws Exception {
        assertEquals(NO_EMPLOYEE_EXCEPTION,
                getReturnValue(getBuiltUri(WRONG_ACCOUNT, CORRECT_PSWD)));
    }

    @Test
    public void authenticateByWrongPassword() throws Exception {
        assertEquals(ERROR_PSWD_EXCEPTION,
                getReturnValue(getBuiltUri(CORRECT_ACCOUNT, WRONG_PSWD)));
    }

    private String getBuiltUri(String account, String password) {
        return "/authentication?account=" + account + "&password=" + password;
    }

}
发布了79 篇原创文章 · 获赞 322 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/qq_15329947/article/details/93503523