![](/img/trans.png)
[英]How do I unit test spring security @PreAuthorize custom expression
[英]How do I unit test spring security @PreAuthorize(hasRole)?
为了在控制器方法上对 PreAuthorize 注释的 hasRole 部分进行单元测试,我需要什么?
我的测试应该会成功,因为登录用户只有两个角色之一,但它会失败并出现以下断言错误:
java.lang.AssertionError: 状态
预期:401
实际:200
我在 MyController 中有以下方法:
@PreAuthorize(value = "hasRole('MY_ROLE') and hasRole('MY_SECOND_ROLE')")
@RequestMapping(value = "/myurl", method = RequestMethod.GET)
public String loadPage(Model model, Authentication authentication, HttpSession session) {
...stuff to do...
}
我创建了以下 abstract-security-test.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd">
<security:global-method-security secured-annotations="enabled" />
<security:authentication-manager alias="authManager">
<security:authentication-provider>
<security:user-service>
<security:user name="missingsecondrole" password="user" authorities="MY_ROLE" />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
在我的单元测试中,我有这个:
@ContextConfiguration("classpath:/spring/abstract-security-test.xml")
public class MyTest {
private final MyController myController = new MyController();
@Autowired
private AuthenticationManager manager;
@Test
public void testValidUserWithInvalidRoleFails() throws Exception {
MockMvc mockMvc = standaloneSetup(myController).setViewResolvers(viewResolver()).build();
Authentication auth = login("missingsecondrole", "user");
mockMvc.perform(get("/myurl")
.session(session)
.flashAttr(MODEL_ATTRIBUTE_NAME, new ModelMap())
.principal(auth)).andExpect(status().isUnauthorized());
}
protected Authentication login(String name, String password) {
Authentication auth = new UsernamePasswordAuthenticationToken(name, password);
SecurityContextHolder.getContext().setAuthentication(manager.authenticate(auth));
return auth;
}
private ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("WEB-INF/views");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
Spring Security 4 为与 MockMvc 集成提供了全面的支持。 例如:
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class SecurityMockMvcTests {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
@Test
public void withUserRequestPostProcessor() {
mvc
.perform(get("/admin").with(user("admin").roles("USER","ADMIN")))
...
}
@WithMockUser(roles="ADMIN")
@Test
public void withMockUser() {
mvc
.perform(get("/admin"))
...
}
...
问题
问题是在这种情况下设置 SecurityContextHolder 不起作用。 原因是 SecurityContextPersistenceFilter 将使用 SecurityContextRepository 尝试从 HttpServletRequest 中找出 SecurityContext(默认情况下它使用 HttpSession)。 它找到(或未找到)的 SecurityContext 将覆盖您在 SecurityContextHolder 上设置的 SecurityContext。
解决方案
为确保请求经过身份验证,您需要使用您正在利用的 SecurityContextRepository 关联您的 SecurityContext。 默认值为 HttpSessionSecurityContextRepository。 下面是一个允许您模拟用户登录的示例方法:
private SecurityContextRepository repository =
new HttpSessionSecurityContextRepository();
private void login(SecurityContext securityContext, HttpServletRequest request) {
HttpServletResponse response = new MockHttpServletResponse();
HttpRequestResponseHolder requestResponseHolder =
new HttpRequestResponseHolder(request, response);
repository.loadContext(requestResponseHolder);
request = requestResponseHolder.getRequest();
response = requestResponseHolder.getResponse();
repository.saveContext(securityContext, request, response);
}
如何使用它的细节可能仍然有点模糊,因为您可能不知道如何访问 MockMvc 中的 HttpServletRequest,但请继续阅读,因为有更好的解决方案。
让它更容易
如果你想让这个和其他与 MockMvc 的安全相关的交互更容易,你可以参考 gs-spring-security-3.2 示例应用程序。 在项目中,您会发现一些用于使用 Spring Security 和 MockMvc 的实用程序,称为SecurityRequestPostProcessors 。 要使用它们,您可以将前面提到的类复制到您的项目中。 使用此实用程序将允许您编写类似这样的内容:
RequestBuilder request = get("/110")
.with(user(rob).roles("USER"));
mvc
.perform(request)
.andExpect(status().isUnAuthorized());
注意:不需要在请求上设置主体,因为只要用户经过身份验证,Spring Security 就会为您建立主体。
您可以在SecurityTests中找到更多示例。 该项目还将协助 MockMvc 和 Spring Security 之间的其他集成(即在执行 POST 时使用 CSRF 令牌设置请求)。
默认不包含?
您可能会问为什么默认情况下不包括在内。 答案是我们根本没有时间看 3.2 的时间线。 示例中的所有代码都可以正常工作,但是我们对命名约定以及它如何集成以发布它的信心不足。 您可以跟踪计划与 Spring Security 4.0.0.M1 一起发布的 SEC-2015 。
更新
您的 MockMvc 实例还需要包含 springSecurityFilterChain。 为此,您可以使用以下内容:
@Autowired
private Filter springSecurityFilterChain;
@Test
public void testValidUserWithInvalidRoleFails() throws Exception {
MockMvc mockMvc = standaloneSetup(myController)
.addFilters(springSecurityFilterChain)
.setViewResolvers(viewResolver())
.build();
...
要使@Autowired
工作,您需要确保在您的@ContextConfiguration
中包含使 springSecurityFilterChain 的安全配置。 对于您当前的设置,这意味着“classpath:/spring/abstract-security-test.xml”应该包含您的安全配置的<http ..>
部分(以及所有依赖的 bean)。 或者,您可以在@ContextConfiguration
中包含第二个文件,其中包含您的安全配置的<http ..>
部分(以及所有依赖的 bean)。
只是为了添加到上述 Rob 的解决方案中,截至 2014 年 12 月 20 日,Rob 上面的回答中的 master 分支上的SecurityRequestPostProcessors
类中存在一个错误,该错误阻止了分配的角色被填充。
一个快速的解决方法是注释掉SecurityRequestPostProcessors
的UserRequestPostProcessor
内部静态类的roles(String... roles)
方法中的以下代码行(当前为第 181 行):
// List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(roles.length);
.
您需要注释掉局部变量,而不是成员变量。
或者,您可以在从方法返回之前插入此行:
this.authorities = authorities;
PS如果我有足够的声誉,我会将此添加为评论。
MockMvcBuilders.standaloneSetup
手动实例化MyController
(没有 Spring,因此没有 AOP)。 因此 PreAuthorize 不会被拦截并且安全检查被跳过。 因此,您可以 @Autowire 您的控制器并将其传递给MockMvcBuilders.standaloneSetup
以模拟传递给控制器的任何服务(有时需要)使用@MockBean
以便服务的每个实例都被 Mock 替换。
我遇到了同样的问题,我花了 1 周的时间几乎解决了这个问题,所以我想在这里分享我的知识,也许以后会有所帮助。 接受的答案或多或少是正确的,但重点是你必须在你的abstract-security-test.xml
中声明所有注入的 bean,当你有很多注入并且你不想复制时,这可能会很痛苦一切。 所以我使用了一个autoBeanMocker
来模拟所有的 bean。 类是这样的:
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import static org.mockito.Mockito.mock;
public class AutoBeanMocker implements BeanDefinitionRegistryPostProcessor
{
private Collection<String> mockedDefinitions;
public AutoBeanMocker()
{
mockedDefinitions = new ArrayList<String>();
}
private Iterable<Field> findAllAutoWired(Class targetBean)
{
List<Field> declaredFields = Arrays.asList(targetBean.getDeclaredFields());
return declaredFields.stream().filter(input -> input.isAnnotationPresent(Autowired.class) || input.isAnnotationPresent(Resource.class))
.collect(Collectors.toList());
}
private void registerOn(final BeanDefinitionRegistry registry, final String beanName, final Class type)
{
RootBeanDefinition definition = new RootBeanDefinition();
MutablePropertyValues values = new MutablePropertyValues();
values.addPropertyValue(new PropertyValue("type", type));
definition.setPropertyValues(values);
((DefaultListableBeanFactory) registry).registerSingleton(beanName, mock(type));
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException
{
for (String beanName : registry.getBeanDefinitionNames())
{
BeanDefinition beanDefinition = registry.getBeanDefinition(beanName);
String beanClassName = beanDefinition.getBeanClassName();
try
{
Class beanClass = Class.forName(beanClassName);
while (true)
{
for (final Field field : findAllAutoWired(beanClass))
{
String fieldName = field.getName();
boolean invalidType = field.getType().isArray() || field.getType().isPrimitive();
if (invalidType)
{
continue;
}
if (!registry.isBeanNameInUse(fieldName))
{
registerOn(registry, fieldName, field.getType());
mockedDefinitions.add(fieldName);
// Now field will be available for autowiring.
}
}
if (beanClass.getSuperclass() != null)
beanClass = beanClass.getSuperclass();
else
break;
}
}
catch (Exception ex)
{
Logger.getLogger(AutoBeanMocker.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
{
for (String beanName : mockedDefinitions)
{
if (!beanFactory.containsBean(beanName))
{
Logger.getLogger(AutoBeanMocker.class.getName()).log(Level.SEVERE, "Missing definition %s", beanName);
}
}
}
}
不要忘记将它添加到您的上下文配置 xml 文件中。
所以现在你需要在你的测试中自动连接你的控制器:
@InjectMocks
@Autowire
private MyController myController;
因为我想模拟一些 bean,所以我还在控制器顶部使用了@InjectMocks
并使用MockitoAnnotations.initMocks(this);
在我的setup()
方法中。 现在你应该知道的最后一点是,如果你将一些 bean 自动装配到你的控制器中,你需要为它们创建 setter 方法,否则 InjectMocks 将无法工作。
此外,我不需要将 SpringSecurityFilterChain 添加到我的控制器中,所以我只是像这样定义了我的 mockMvc:
mockMvc = standaloneSetup(myController).build();
这是一个示例测试方法:
@Test
public void someTest_expectAccessDeniedException() throws Exception
{
when(someBean.someMethod(someParameter)).thenReturn(someReturn);
mockMvc.perform(get("somePath"))
.andExpect(result -> assertTrue(result.getResolvedException() instanceof AccessDeniedException));
}
添加@WithMockUser(authorities = ["YOUR_ROLE"])
对我有用。 使用 MockMcv 时,此自动设置 Spring 安全上下文中的角色。
@Test
@WithMockUser(authorities = ["YOUR_ROLE"])
void test_role() {
}
只是另一种测试方式,只需使用 PreAuthorize 注释过滤类方法。 我制作了简单的测试生成器
@Test
public void shouldCheckMethodsPreAuthorizeAnnotationValue() {
List<Method> methods = Arrays.stream(YourControllerOrService.class.getDeclaredMethods())
.filter(method -> AnnotationUtils.getAnnotation(method, PreAuthorize.class) != null)
.collect(Collectors.toList());
System.out.println(format("assertEquals(methodsMap.size(), %s);", methods.size()));
Map<String, String> methodsMap = methods.stream()
.map(method -> {
PreAuthorize annotation = AnnotationUtils.getAnnotation(method, PreAuthorize.class);
System.out.println(format("assertEquals(methodsMap.get(\"%s\"), \"%s\");", method.getName(), annotation.value()));
return method;
})
.collect(Collectors.toMap(
Method::getName,
method -> Objects.requireNonNull(AnnotationUtils.getAnnotation(method, PreAuthorize.class)).value()
));
assertEquals(methodsMap.size(), 2);
assertEquals(methodsMap.get("getMethod1"), "hasRole('ROLE_BLA_1')");
assertEquals(methodsMap.get("getMethod2"), "hasRole('ROLE_BLA_2')");
}
该测试涵盖所有@PreAuthorize 注释。 生成断言后,只需删除不必要的 System.out 即可。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.