繁体   English   中英

我如何对 spring security @PreAuthorize(hasRole) 进行单元测试?

[英]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类中存在一个错误,该错误阻止了分配的角色被填充。

一个快速的解决方法是注释掉SecurityRequestPostProcessorsUserRequestPostProcessor内部静态类的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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM