spring-security 如何在PreAuthorize(hasRole)对Spring安全性进行单元测试?

yftpprvb  于 2022-11-11  发布在  Spring
关注(0)|答案(5)|浏览(151)

为了对控制器方法上的PreAuthorize注解的hasRole部分进行单元测试,我需要什么?
我的测试应该成功,因为登录的用户只有两个角色中的一个,但是它失败了,并出现以下Assert错误:
java.lang.AssertionError:状态
预期值:401
实际值:200
下面的代码是一个例子:

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

r9f1avp51#

更新

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示例应用程序。在这个项目中,你会发现一些名为SecurityRequestPostProcessors的实用程序,用于处理Spring Security和MockMvc。要使用它们,你可以将前面提到的类复制到你的项目中。使用这个实用程序将允许你编写类似如下的代码:

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的时间线。示例中的所有代码都可以正常工作,但是我们对命名约定和集成的方式没有足够的信心来发布这个。您可以跟踪SEC-2015,它计划与Spring Security 4.0.0.M1一起发布。

更新

您的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)。

nfeuvbwi

nfeuvbwi2#

在上述Rob的解决方案中,截至2014年12月20日,在上述Rob的答案中,master分支上的SecurityRequestPostProcessors类中存在一个错误,该错误阻止填充分配的角色。
一种快速修复方法是注解掉SecurityRequestPostProcessorsUserRequestPostProcessor内部静态类的roles(String... roles)方法中的以下代码行(当前为第181行):
// List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(roles.length); .
您需要注解掉局部变量,而不是成员变量。
或者,您可以在从方法传回之前插入这一行:
this.authorities = authorities;
如果我有足够的声誉,我会把这句话作为评论加进去。

niwlg2el

niwlg2el3#

MockMvcBuilders.standaloneSetup手动示例化了一个MyController(没有Spring,因此没有AOP)。因此,PreAuthorize不会被拦截,安全检查也会被跳过。因此,您可以@Autowire您的控制器,并将其传递给MockMvcBuilders.standaloneSetup,以模拟传递给控制器的任何服务(有时候需要)。使用@MockBean,这样服务的每个示例都会被替换为Mock。

lymnna71

lymnna714#

我遇到了同样的问题,我花了一周的时间来解决这个问题,所以我想在这里分享我的知识,也许它会对以后的人有所帮助。公认的答案或多或少是正确的,但主要的一点是,你必须在你的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,我在我的setup()方法中使用了MockitoAnnotations.initMocks(this);。现在,你应该知道的最后一点是,如果你要将一些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));
}
unhi4e5o

unhi4e5o5#

添加@WithMockUser(authorities = ["YOUR_ROLE"])对我来说很有用。当使用MockMcv时,这个自动设置在Spring的安全上下文中的角色。

@Test
@WithMockUser(authorities = ["YOUR_ROLE"])
void test_role() {

  }

相关问题