Java 如何对 spring 安全性 @PreAuthorize(hasRole) 进行单元测试?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/22527978/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 16:16:38  来源:igfitidea点击:

How do I unit test spring security @PreAuthorize(hasRole)?

javatestingspring-securitymocking

提问by edwardmlyte

What do I need in order to unit test the hasRole part of a PreAuthorize annotation on a controller method?

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

My test should succeed because the logged in user only has one of the two roles, but instead it fails with the following assertion error:

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

java.lang.AssertionError: Status

Expected :401

Actual :200

java.lang.AssertionError: 状态

预期:401

实际:200

I have the following method in MyController:

我在 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...
}

I created the following abstract-security-test.xml:

我创建了以下 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>

And in my unit test I have this:

在我的单元测试中,我有这个:

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

采纳答案by Rob Winch

UPDATE

更新

Spring Security 4 provides comprehensive supportfor integrating with MockMvc. For example:

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"))
            ...
    }

 ...

The Problem

问题

The problem is that setting the SecurityContextHolder does not work in this instance. The reason is that the SecurityContextPersistenceFilter will use the SecurityContextRepository to try and figure out the SecurityContext from the HttpServletRequest (by default it uses the HttpSession). The SecurityContext it finds (or doesn't find) will override the SecurityContext you have set on the SecurityContextHolder.

问题是在这种情况下设置 SecurityContextHolder 不起作用。原因是 SecurityContextPersistenceFilter 将使用 SecurityContextRepository 尝试从 HttpServletRequest 中找出 SecurityContext(默认情况下它使用 HttpSession)。它找到(或未找到)的 SecurityContext 将覆盖您在 SecurityContextHolder 上设置的 SecurityContext。

The Solution

解决方案

To ensure the request is authenticated you need to associate your SecurityContext using the SecurityContextRepository that you are leveraging. The default is the HttpSessionSecurityContextRepository. An example method that will allow you to mock being logged in by a user is below:

为了确保请求经过身份验证,您需要使用您正在利用的 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);
}

The details of how to use this might still a bit vague since you might not know how to access the HttpServletRequest in MockMvc, but keep reading as there is a better solution.

如何使用它的细节可能仍然有点模糊,因为您可能不知道如何访问 MockMvc 中的 HttpServletRequest,但请继续阅读,因为有更好的解决方案。

Making it easier

让它更容易

If you want to make this and other Security related interactions with MockMvc easier, you can refer to the gs-spring-security-3.2 sample application. Within the project you will find some utilities for working with Spring Security and MockMvc called SecurityRequestPostProcessors. To use them you can copy that previously mentioned class into your project. Using this utility will allow you to write something like this instead:

如果你想让这个和其他与 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());

NOTE: There is no need to set the principal on the request as Spring Security establishes the Principal for you as long as a user is authenticated.

注意:不需要在请求上设置主体,因为只要用户经过身份验证,Spring Security 就会为您建立主体。

You can find additional examples in SecurityTests. This project will also assist in other integrations between MockMvc and Spring Security (i.e. setting up the request with the CSRF token when performing a POST).

您可以在SecurityTests 中找到其他示例。该项目还将协助 MockMvc 和 Spring Security 之间的其他集成(即在执行 POST 时使用 CSRF 令牌设置请求)。

Not included by default?

默认不包含?

You might ask why this is not included by default. The answer is that we simply did not have time for the 3.2 timeline. All the code in the sample will work fine, but we weren't confident enough on naming conventions and exactly how it integrated to release this. You can track SEC-2015which is scheduled to come out with Spring Security 4.0.0.M1.

您可能会问为什么默认情况下不包括在内。答案是我们根本没有时间进行 3.2 时间表。示例中的所有代码都可以正常工作,但我们对命名约定以及它如何集成以发布该约定并没有足够的信心。您可以跟踪计划与 Spring Security 4.0.0.M1 一起发布的SEC-2015

Update

更新

Your MockMvc instance needs to also contain the springSecurityFilterChain. To do so, you can use the following:

您的 MockMvc 实例还需要包含 springSecurityFilterChain。为此,您可以使用以下方法:

@Autowired
private Filter springSecurityFilterChain;

@Test
public void testValidUserWithInvalidRoleFails() throws Exception {
    MockMvc mockMvc = standaloneSetup(myController)
        .addFilters(springSecurityFilterChain)
        .setViewResolvers(viewResolver())
        .build();
    ...

For the @Autowiredto work, you need to ensure to include your security configuration that makes the springSecurityFilterChain in your @ContextConfiguration. For your current setup, this means "classpath:/spring/abstract-security-test.xml" should contain your <http ..>portion of your security configuration (and all the dependent beans). Alternatively, you can include a second file(s) in the @ContextConfigurationthat has your <http ..>portion of your security configuration (and all the dependent beans).

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

回答by Akio Hamasaki

Just to add to Rob's solution above, as of December 20, 2014, there is a bug in the SecurityRequestPostProcessorsclass on the master branch from Rob's answer above that prevents the assigned roles from being populated.

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

A quick fix is to comment out the following line of code (currently line 181) in the roles(String... roles)method of the UserRequestPostProcessorinner static class of SecurityRequestPostProcessors:

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

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

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

You need to comment out the local variable, NOT the member variable.

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

Alternatively, you may insert this line just before returning from the method:

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

this.authorities = authorities;

this.authorities = authorities;

P.S I would have added this as a comment had I had enough reputation.

PS 如果我有足够的声誉,我会将此添加为评论。

回答by user1928596

MockMvcBuilders.standaloneSetupgets a MyControllerinstantiated manually ( without Spring and therefore without AOP). Therefore the PreAuthorize is not intercepted and security check is skipped. You can therefore @Autowire your controller and pass it to MockMvcBuilders.standaloneSetupto mock any services passed to the controller (as it's sometimes needed) use @MockBeanso that every instance of the service gets replaced with the Mock.

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