如何对 Spring Security @PreAuthorize(hasRole) 进行单元测试?

Posted

技术标签:

【中文标题】如何对 Spring Security @PreAuthorize(hasRole) 进行单元测试?【英文标题】:How do I unit test spring security @PreAuthorize(hasRole)? 【发布时间】:2014-04-27 00:04:56 【问题描述】:

为了在控制器方法上对 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;
    

【问题讨论】:

【参考方案1】:

更新

Spring Security 4 提供了comprehensive support 用于与 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”应该包含您的安全配置的&lt;http ..&gt; 部分(以及所有依赖的bean)。或者,您可以在 @ContextConfiguration 中包含第二个文件,其中包含您的安全配置的 &lt;http ..&gt; 部分(以及所有依赖的 bean)。

【讨论】:

感谢罗布的完整解释。然而下载 gs-spring-security-3.2 3 后 SecurityTests 失败:inboxShowsOnlyRobsMessages、validUsernamePassword、robCannotAccessLukesMessage。这一切似乎都基于用户和角色。同样,实施您的建议并没有改变问题,我仍然没有得到我期望的 401 状态,因此测试失败。 (注意:我在 IntelliJ 13.1 中针对 java 6 和 7 运行了这些测试,结果相同。但我认为这不应该影响测试的成功率。)。还有其他想法吗? 你是正确的,测试没有通过。我不小心从 master 中删除了一些安全性,而不是将其推送到演示分支(即,我有一个分支是演示的开始,而 master 应该是完整的版本)。请尝试使用 master 的更新。 IntelliJ 应该不会引起任何问题,但让我们先尝试让我提供的示例工作,因为我确切知道那里有什么代码。 所以测试现在可以工作了。但是我仍然无法解决我的问题。无论我以任何角色传递给哪个用户,它总是会成功登录。您的测试的哪一部分是检查这些角色的关键?是 mockMvc 构建器上的 springSecurityFilterChain 吗?因为我在 WebMvcConfiguration 中找不到匹配的类,所以它是从哪里自动装配的。【参考方案2】:

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

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

// List&lt;GrantedAuthority&gt; authorities = new ArrayList&lt;GrantedAuthority&gt;(roles.length);

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

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

this.authorities = authorities;

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

【讨论】:

【参考方案3】:

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

【讨论】:

为什么否定......这个答案对我来说很有意义 @Rafael 让我大吃一惊 :),它确实有效,请点赞 :)

以上是关于如何对 Spring Security @PreAuthorize(hasRole) 进行单元测试?的主要内容,如果未能解决你的问题,请参考以下文章

如何对 Spring Security @PreAuthorize(hasRole) 进行单元测试?

如何使用spring security根据角色限制对html页面的访问?

Spring Security对Android客户端如何实现权限管理

如何使用 spring security 和 spring boot 对 Google 用户进行身份验证,将 mongoDB 作为存储库?

如何使用 DaoAuthenticationProvider 以编程方式使用 Spring Security 对用户进行身份验证

Spring Security OAUTH2 - 如何根据客户端 ID 限制对 API 的访问?