如何单元测试弹簧安全@PreAuthorize(hasRole)?更新

2022-09-01 06:10:57

为了对控制器方法上的预授权注释的 hasRole 部分进行单元测试,我需要什么?

我的测试应该会成功,因为登录用户只有两个角色中的一个,但它会失败,并出现以下断言错误:

java.lang.AssertionError: Status

预期 :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...
}

我创建了以下抽象安全测试.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;
    }
}

答案 1

更新

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 就会为您建立主体。

您可以在安全测试中找到其他示例。该项目还将帮助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();
    ...

要使 工作,您需要确保在 .对于您当前的设置,这意味着“classpath:/spring/abstract-security-test.xml”应包含您的部分安全配置(以及所有依赖的bean)。或者,您可以在 中包含第二个文件,其中包含您的部分安全配置(以及所有从属 Bean)。@Autowired@ContextConfiguration<http ..>@ContextConfiguration<http ..>


答案 2

为了补充上面的 Rob 解决方案,截至 2014 年 12 月 20 日,上面的 Rob 答案中的主分支上的类中存在一个错误,该错误阻止了分配的角色被填充。SecurityRequestPostProcessors

一个快速修复方法是在内部静态类的方法中注释掉以下代码行(当前为第 181 行):roles(String... roles)UserRequestPostProcessorSecurityRequestPostProcessors

// List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(roles.length);.

您需要注释掉局部变量,而不是成员变量。

或者,您可以在从该方法返回之前插入以下行:

this.authorities = authorities;

P.S如果我有足够的声誉,我会添加这个作为评论。