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;
}
}