Spring Test & Security:如何模拟身份验证?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/15203485/
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-09-08 05:46:47  来源:igfitidea点击:

Spring Test & Security: How to mock authentication?

springsecuritymodel-view-controllertestingjunit

提问by Martin Becker

I was trying to figure out how to unit test if my the URLs of my controllers are properly secured. Just in case someone changes things around and accidentally removes security settings.

我试图弄清楚如何对我的控制器的 URL 进行单元测试是否得到了适当的保护。以防万一有人改变了事情并意外删除了安全设置。

My controller method looks like this:

我的控制器方法如下所示:

@RequestMapping("/api/v1/resource/test") 
@Secured("ROLE_USER")
public @ResonseBody String test() {
    return "test";
}

I set up a WebTestEnvironment like so:

我像这样设置了一个 WebTestEnvironment:

import javax.annotation.Resource;
import javax.naming.NamingException;
import javax.sql.DataSource;

import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({ 
        "file:src/main/webapp/WEB-INF/spring/security.xml",
        "file:src/main/webapp/WEB-INF/spring/applicationContext.xml",
        "file:src/main/webapp/WEB-INF/spring/servlet-context.xml" })
public class WebappTestEnvironment2 {

    @Resource
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    @Qualifier("databaseUserService")
    protected UserDetailsService userDetailsService;

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    protected DataSource dataSource;

    protected MockMvc mockMvc;

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    protected UsernamePasswordAuthenticationToken getPrincipal(String username) {

        UserDetails user = this.userDetailsService.loadUserByUsername(username);

        UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(
                        user, 
                        user.getPassword(), 
                        user.getAuthorities());

        return authentication;
    }

    @Before
    public void setupMockMvc() throws NamingException {

        // setup mock MVC
        this.mockMvc = MockMvcBuilders
                .webAppContextSetup(this.wac)
                .addFilters(this.springSecurityFilterChain)
                .build();
    }
}

In my actual test I tried to do something like this:

在我的实际测试中,我尝试做这样的事情:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;

import eu.ubicon.webapp.test.WebappTestEnvironment;

public class CopyOfClaimTest extends WebappTestEnvironment {

    @Test
    public void signedIn() throws Exception {

        UsernamePasswordAuthenticationToken principal = 
                this.getPrincipal("test1");

        SecurityContextHolder.getContext().setAuthentication(principal);        

        super.mockMvc
            .perform(
                    get("/api/v1/resource/test")
//                    .principal(principal)
                    .session(session))
            .andExpect(status().isOk());
    }

}

I picked this up here:

我在这里捡到了这个:

Yet if one looks closely this only helps when not sending actual requests to URLs, but only when testing services on a function level. In my case an "access denied" exception was thrown:

然而,如果仔细观察,这只会在不向 URL 发送实际请求时有所帮助,而仅在功能级别上测试服务时才有帮助。在我的情况下,抛出了“访问被拒绝”异常:

org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:83) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:206) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:60) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) ~[spring-aop-3.2.1.RELEASE.jar:3.2.1.RELEASE]
        ...

The following two log messages are noteworthy basically saying that no user was authenticated indicating that setting the Principaldid not work, or that it was overwritten.

以下两条日志消息值得注意,基本上是说没有用户通过身份验证,表明设置Principal不起作用,或者它被覆盖了。

14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Secure object: ReflectiveMethodInvocation: public java.util.List test.TestController.test(); target is of class [test.TestController]; Attributes: [ROLE_USER]
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS

采纳答案by Martin Becker

It turned out that the SecurityContextPersistenceFilter, which is part of the Spring Security filter chain, always resets my SecurityContext, which I set calling SecurityContextHolder.getContext().setAuthentication(principal)(or by using the .principal(principal)method). This filter sets the SecurityContextin the SecurityContextHolderwith a SecurityContextfrom a SecurityContextRepositoryOVERWRITINGthe one I set earlier. The repository is a HttpSessionSecurityContextRepositoryby default. The HttpSessionSecurityContextRepositoryinspects the given HttpRequestand tries to access the corresponding HttpSession. If it exists, it will try to read the SecurityContextfrom the HttpSession. If this fails, the repository generates an empty SecurityContext.

事实证明,作为SecurityContextPersistenceFilterSpring Security 过滤器链一部分的 总是重置 my SecurityContext,我设置了调用SecurityContextHolder.getContext().setAuthentication(principal)(或使用.principal(principal)方法)。该过滤器设置SecurityContextSecurityContextHolder同一个SecurityContextSecurityContextRepository覆盖一个我先前设置。HttpSessionSecurityContextRepository默认情况下,存储库是一个。在HttpSessionSecurityContextRepository检查给定的HttpRequest,并试图访问相应HttpSession。如果存在,它将尝试SecurityContextHttpSession. 如果失败,存储库会生成一个空的SecurityContext.

Thus, my solution is to pass a HttpSessionalong with the request, which holds the SecurityContext:

因此,我的解决方案是将 aHttpSession与请求一起传递,其中包含SecurityContext

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;

import eu.ubicon.webapp.test.WebappTestEnvironment;

public class Test extends WebappTestEnvironment {

    public static class MockSecurityContext implements SecurityContext {

        private static final long serialVersionUID = -1386535243513362694L;

        private Authentication authentication;

        public MockSecurityContext(Authentication authentication) {
            this.authentication = authentication;
        }

        @Override
        public Authentication getAuthentication() {
            return this.authentication;
        }

        @Override
        public void setAuthentication(Authentication authentication) {
            this.authentication = authentication;
        }
    }

    @Test
    public void signedIn() throws Exception {

        UsernamePasswordAuthenticationToken principal = 
                this.getPrincipal("test1");

        MockHttpSession session = new MockHttpSession();
        session.setAttribute(
                HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, 
                new MockSecurityContext(principal));


        super.mockMvc
            .perform(
                    get("/api/v1/resource/test")
                    .session(session))
            .andExpect(status().isOk());
    }
}

回答by EliuX

Seaching for answer I couldn't find any to be easy and flexible at the same time, then I found the Spring Security Referenceand I realized there are near to perfect solutions. AOP solutions often are the greatest ones for testing, and Spring provides it with @WithMockUser, @WithUserDetailsand @WithSecurityContext, in this artifact:

寻找答案我找不到任何既简单又灵活的答案,然后我找到了Spring Security Reference,我意识到有接近完美的解决方案。AOP 解决方案通常是最适合测试的解决方案,Spring 为它提供了@WithMockUser,@WithUserDetails@WithSecurityContext, 在这个工件中:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>4.2.2.RELEASE</version>
    <scope>test</scope>
</dependency>

In most cases, @WithUserDetailsgathers the flexibility and power I need.

在大多数情况下,@WithUserDetails集合了我需要的灵活性和功能。

How @WithUserDetails works?

@WithUserDetails 如何工作?

Basically you just need to create a custom UserDetailsServicewith all the possible users profiles you want to test. E.g

基本上,您只需要UserDetailsService使用您想要测试的所有可能的用户配置文件创建一个自定义。例如

@TestConfiguration
public class SpringSecurityWebAuxTestConfig {

    @Bean
    @Primary
    public UserDetailsService userDetailsService() {
        User basicUser = new UserImpl("Basic User", "[email protected]", "password");
        UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList(
                new SimpleGrantedAuthority("ROLE_USER"),
                new SimpleGrantedAuthority("PERM_FOO_READ")
        ));

        User managerUser = new UserImpl("Manager User", "[email protected]", "password");
        UserActive managerActiveUser = new UserActive(managerUser, Arrays.asList(
                new SimpleGrantedAuthority("ROLE_MANAGER"),
                new SimpleGrantedAuthority("PERM_FOO_READ"),
                new SimpleGrantedAuthority("PERM_FOO_WRITE"),
                new SimpleGrantedAuthority("PERM_FOO_MANAGE")
        ));

        return new InMemoryUserDetailsManager(Arrays.asList(
                basicActiveUser, managerActiveUser
        ));
    }
}

Now we have our users ready, so imagine we want to test the access control to this controller function:

现在我们已经准备好了我们的用户,所以假设我们要测试对这个控制器函数的访问控制:

@RestController
@RequestMapping("/foo")
public class FooController {

    @Secured("ROLE_MANAGER")
    @GetMapping("/salute")
    public String saluteYourManager(@AuthenticationPrincipal User activeUser)
    {
        return String.format("Hi %s. Foo salutes you!", activeUser.getUsername());
    }
}

Here we have a get mapped functionto the route /foo/saluteand we are testing a role based security with the @Securedannotation, although you can test @PreAuthorizeand @PostAuthorizeas well. Let's create two tests, one to check if a valid user can see this salute response and the other to check if it's actually forbidden.

在这里,我们有一个被映射功能的路线/富/敬礼,我们正在测试与基于角色的安全性@Secured注解,但你可以测试@PreAuthorize@PostAuthorize也。让我们创建两个测试,一个是检查有效用户是否可以看到此致敬响应,另一个是检查它是否实际上被禁止。

@RunWith(SpringRunner.class)
@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        classes = SpringSecurityWebAuxTestConfig.class
)
@AutoConfigureMockMvc
public class WebApplicationSecurityTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithUserDetails("[email protected]")
    public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("[email protected]")));
    }

    @Test
    @WithUserDetails("[email protected]")
    public void givenBasicUser_whenGetFooSalute_thenForbidden() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isForbidden());
    }
}

As you see we imported SpringSecurityWebAuxTestConfigto provide our users for testing. Each one used on its corresponding test case just by using a straightforward annotation, reducing code and complexity.

如您所见,我们导入SpringSecurityWebAuxTestConfig以提供给我们的用户进行测试。每一个都用于其相应的测试用例,只需使用一个简单的注释,减少代码和复杂性。

Better use @WithMockUser for simpler Role Based Security

更好地使用 @WithMockUser 以获得更简单的基于角色的安全性

As you see @WithUserDetailshas all the flexibility you need for most of your applications. It allows you to use custom users with any GrantedAuthority, like roles or permissions. But if you are just working with roles, testing can be even easier and you could avoid constructing a custom UserDetailsService. In such cases, specify a simple combination of user, password and roles with @WithMockUser.

如您所见,@WithUserDetails拥有大多数应用程序所需的所有灵活性。它允许您使用具有任何 GrantedAuthority 的自定义用户,例如角色或权限。但是,如果您只是使用角色,测试会更容易,并且您可以避免构建自定义UserDetailsService. 在这种情况下,请使用@WithMockUser指定用户、密码和角色的简单组合。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(
    factory = WithMockUserSecurityContextFactory.class
)
public @interface WithMockUser {
    String value() default "user";

    String username() default "";

    String[] roles() default {"USER"};

    String password() default "password";
}

The annotation defines default values for a very basic user. As in our case the route we are testing just requires that the authenticated user be a manager, we can quit using SpringSecurityWebAuxTestConfigand do this.

注释为非常基本的用户定义了默认值。在我们的例子中,我们正在测试的路由只需要经过身份验证的用户是管理员,我们可以退出使用SpringSecurityWebAuxTestConfig并执行此操作。

@Test
@WithMockUser(roles = "MANAGER")
public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
{
    mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
            .accept(MediaType.ALL))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("user")));
}

Notice that now instead of the user [email protected]we are getting the default provided by @WithMockUser: user; yet it won't matter because what we really care about is his role: ROLE_MANAGER.

请注意,现在我们得到的不是用户[email protected],而是由@WithMockUser以下用户提供的默认值:user; 不过没关系,因为我们真正关心的是他的角色:ROLE_MANAGER

Conclusions

结论

As you see with annotations like @WithUserDetailsand @WithMockUserwe can switch between different authenticated users scenarioswithout building classes alienated from our architecture just for making simple tests. Its also recommended you to see how @WithSecurityContextworks for even more flexibility.

正如你看到的注释@WithUserDetails@WithMockUser我们可以在不同的经过身份验证的用户场景之间切换,而无需为了进行简单的测试而构建与我们的架构疏远的类。它还建议您查看@WithSecurityContext如何工作以获得更大的灵活性。

回答by GummyBear21

Since Spring 4.0+, the best solution is to annotate the test method with @WithMockUser

从 Spring 4.0+ 开始,最好的解决方案是使用 @WithMockUser 注释测试方法

@Test
@WithMockUser(username = "user1", password = "pwd", roles = "USER")
public void mytest1() throws Exception {
    mockMvc.perform(get("/someApi"))
        .andExpect(status().isOk());
}

Remember to add the following dependency to your project

记得在你的项目中添加以下依赖

'org.springframework.security:spring-security-test:4.2.3.RELEASE'

回答by Grigory Kislin

Add in pom.xml:

在 pom.xml 中添加:

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <version>4.0.0.RC2</version>
    </dependency>

and use org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessorsfor authorization request. See the sample usage at https://github.com/rwinch/spring-security-test-blog(https://jira.spring.io/browse/SEC-2592).

org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors用于授权请求。请参阅https://github.com/rwinch/spring-security-test-blog( https://jira.spring.io/browse/SEC-2592) 上的示例用法。

Update:

更新:

4.0.0.RC2 works for spring-security 3.x. For spring-security 4 spring-security-test become part of spring-security (http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test, version is the same).

4.0.0.RC2 适用于 spring-security 3.x。对于 spring-security 4 spring-security-test 成为 spring-security 的一部分(http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test,版本是一样的)。

Setting Up is changed: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc

设置已更改:http: //docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc

public void setup() {
    mvc = MockMvcBuilders
            .webAppContextSetup(context)
            .apply(springSecurity())  
            .build();
}

Sample for basic-authentication: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication.

基本身份验证示例:http: //docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication

回答by Jay

Here is an example for those who want to Test Spring MockMvc Security Config using Base64 basic authentication.

这是一个示例,适用于想要使用 Base64 基本身份验证测试 Spring MockMvc 安全配置的人。

String basicDigestHeaderValue = "Basic " + new String(Base64.encodeBase64(("<username>:<password>").getBytes()));
this.mockMvc.perform(get("</get/url>").header("Authorization", basicDigestHeaderValue).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());

Maven Dependency

Maven 依赖

    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.3</version>
    </dependency>

回答by Nagy Attila

Short answer:

简短的回答:

@Autowired
private WebApplicationContext webApplicationContext;

@Autowired
private Filter springSecurityFilterChain;

@Before
public void setUp() throws Exception {
    final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path");
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
            .defaultRequest(defaultRequestBuilder)
            .alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest()))
            .apply(springSecurity(springSecurityFilterChain))
            .build();
}

private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder,
                                                             final MockHttpServletRequest request) {
    requestBuilder.session((MockHttpSession) request.getSession());
    return request;
}

After perform formLoginfrom spring security test each of your requests will be automatically called as logged in user.

formLogin从 spring 安全测试执行后,您的每个请求都将自动以登录用户的身份调用。

Long answer:

长答案:

Check this solution (the answer is for spring 4): How to login a user with spring 3.2 new mvc testing

检查此解决方案(答案适用于 spring 4):How to login a user with spring 3.2 new mvc testing

回答by Pavla Nováková

Options to avoid using SecurityContextHolder in tests:

避免在测试中使用 SecurityContextHolder 的选项:

  • Option 1: use mocks - I mean mock SecurityContextHolderusing some mock library - EasyMockfor example
  • Option 2: wrap call SecurityContextHolder.get...in your code in some service - for example in SecurityServiceImplwith method getCurrentPrincipalthat implements SecurityServiceinterface and then in your tests you can simply create mock implementation of this interface that returns the desired principal without access to SecurityContextHolder.
  • 选项 1:使用模拟 - 我的意思是SecurityContextHolder使用一些模拟库模拟 -例如EasyMock
  • 选项 2:将调用包装SecurityContextHolder.get...在某些服务中的代码中 - 例如在实现接口的SecurityServiceImpl方法中getCurrentPrincipalSecurityService然后在您的测试中,您可以简单地创建此接口的模拟实现,该实现返回所需的主体而无需访问SecurityContextHolder.