春季测试和安全性:如何模拟身份验证?


124

我试图弄清楚如何对我的控制器的URL进行正确保护的单元测试。以防万一有人更改周围的内容并意外删除安全设置。

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

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

我像这样设置一个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();
    }
}

在我的实际测试中,我尝试执行以下操作:

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());
    }

}

我在这里捡到的:

但是,如果仔细观察,这只会在不向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]
        ...

以下两个日志消息是值得注意的,基本上表明没有用户经过身份验证,表明该设置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

您的导入中将显示您的公司名称eu.ubicon。那不是安全隐患吗?
凯尔·布​​莱恩汀汀

2
嗨,谢谢你的评论!我不明白为什么。无论如何,它是开源软件。如果你有兴趣,请参阅bitbucket.org/ubicon/ubicon(或bitbucket.org/dmir_wue/everyaware最新叉)。让我知道我是否想念什么。
马丁·贝克尔

检查该解决方案(答案是春季4):stackoverflow.com/questions/14308341/...
纳吉阿提拉

Answers:


101

搜寻答案我无法同时找到任何既简单又灵活的方法,所以我找到了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>

在大多数情况下,可以@WithUserDetails收集我所需的灵活性和功能。

@WithUserDetails如何工作?

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

@TestConfiguration
public class SpringSecurityWebAuxTestConfig {

    @Bean
    @Primary
    public UserDetailsService userDetailsService() {
        User basicUser = new UserImpl("Basic User", "user@company.com", "password");
        UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList(
                new SimpleGrantedAuthority("ROLE_USER"),
                new SimpleGrantedAuthority("PERM_FOO_READ")
        ));

        User managerUser = new UserImpl("Manager User", "manager@company.com", "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
        ));
    }
}

现在我们已经为用户准备好了,所以想象一下我们想测试对该控制器功能的访问控制:

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

在这里,我们有一个被映射功能的路线/富/敬礼,我们正在测试与基于角色的安全性@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("manager@company.com")
    public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("manager@company.com")));
    }

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

如您所见,我们导入是SpringSecurityWebAuxTestConfig为了为我们的用户提供测试。每个人仅通过使用简单的注释就可以在其相应的测试用例上使用,从而减少了代码和复杂性。

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

如您所见,它@WithUserDetails具有大多数应用程序所需的所有灵活性。它允许您使用具有任何GrantedAuthority(例如角色或权限)的自定义用户。但是,如果您只是使用角色,则测试会更加容易,并且可以避免构造一个custom 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";
}

注释为非常基本的用户定义了默认值。就像在我们的案例中,我们正在测试的路线仅要求经过身份验证的用户成为管理员,我们可以退出使用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")));
}

注意,现在不是用户user@manager.company,而是由@WithMockUser以下用户提供的默认值:user ; 但这并不重要,因为我们真正关心的是他的角色:ROLE_MANAGER

结论

如您所见,使用注释这样@WithUserDetails@WithMockUser我们可以在不同的经过身份验证的用户场景之间切换,而无需构建与我们的体系结构无关的类,而只是为了进行简单的测试。它还建议您查看@WithSecurityContext如何工作以提供更大的灵活性。


如何模拟多个用户?例如,第一个请求是由发送的tom,而第二个是通过jerry
ch271828n

您可以使用tom创建一个测试所在的函数,并使用相同的逻辑创建另一个测试,然后使用Jerry对其进行测试。每个测试都会有一个特定的结果,因此会有不同的断言,如果测试失败,它将通过名称告诉您哪个用户/角色不起作用。请记住,在一个请求中,用户只能是一个,因此在一个请求中指定多个用户是没有意义的。
EliuX

抱歉,我的意思是这样的示例场景:我们测试过,汤姆创建了一篇秘密文章,然后杰瑞尝试阅读该文章,而杰瑞不应该看到它(因为这是秘密的)。因此,在这种情况下,它是一个单元测试...
ch271828n

它看起来很像答案中的BasicUserManager User场景。关键概念是,我们实际上不在乎用户,而是在乎他们的角色,但是位于同一单元测试中的每个测试实际上代表不同的查询。由不同用户(具有不同角色)对同一个端点完成。
EliuX

61

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

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

请记住将以下依赖项添加到您的项目中

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

1
春天很棒。谢谢
TuGordoBello

好答案。此外-您不需要使用嘲笑Mvc,但是如果您使用的是例如springframework.data中的PagingAndSortingRepository-您可以直接从存储库中调用方法(用EL @PreAuthorize(......)注释)
supertramp

50

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

因此,我的解决方案是将HttpSession连同请求一起传递,其中包含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());
    }
}


我认为创建Authentication对象并添加具有相应属性的会话并不那么糟糕。您认为这是有效的“解决方法”吗?另一方面,直接支持当然会很棒。看起来很整洁。感谢您的链接!
Martin Becker 2013年

很好的解决方案。为我工作!受保护方法的命名只是一个小问题,getPrincipal()我认为这有点误导。理想情况下,它应该被命名getAuthentication()。同样,在您的signedIn()测试中,应将局部变量命名为authauthentication代替principal
Tanvir

什么是“ getPrincipal(“ test1”)” ??您能解释一下它在哪里吗?在此先感谢
user2992476

@ user2992476它可能返回UsernamePasswordAuthenticationToken类型的对象。或者,您可以创建GrantedAuthority并构造此对象。
bluelurker

31

添加pom.xml:

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

org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors用于授权请求。看到在样品使用https://github.com/rwinch/spring-security-test-bloghttps://jira.spring.io/browse/SEC-2592)。

更新:

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

设置已更改: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();
}

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


这也解决了我尝试通过登录安全过滤器登录时得到404的问题。谢谢!
伊恩·纽兰德

嗨,正如GKislin提到的那样进行测试。我收到以下错误“验证失败的UserDetailsS​​ervice返回null,这是违反接口合同的行为”。请提出任何建议。最终AuthenticationRequest auth = new AuthenticationRequest(); auth.setUsername(userId); auth.setPassword(密码); mockMvc.perform(post(“ / api / auth /”)。content(json(auth))。contentType(MediaType.APPLICATION_JSON));
桑耶夫

7

对于那些想要使用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>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.3</version>
    </dependency>

3

简短答案:

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

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

长答案:

检查此解决方案(答案是针对Spring 4的):如何使用Spring 3.2新的MVC测试登录用户


2

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

  • 选项1:使用模拟-我的意思是SecurityContextHolder使用一些模拟库进行模拟- 例如EasyMock
  • 选项2SecurityContextHolder.get...在某些服务中将调用包装在代码中-例如,SecurityServiceImpl使用getCurrentPrincipal实现SecurityService接口的方法,然后在测试中,您可以简单地创建此接口的模拟实现,该实现将返回所需的主体而无需访问SecurityContextHolder

嗯,也许我不了解整个情况。我的问题是,SecurityContextPersistenceFilter使用HttpSessionSecurityContextRepository中的SecurityContext替换了SecurityContext,后者又从相应的HttpSession中读取了SecurityContext。因此使用会话解决方案。关于对SecurityContextHolder的调用:我编辑了答案,以便不再使用对SecurityContextHolder的调用。而且也无需引入任何包装或额外的模拟库。您认为这是一个更好的解决方案吗?
马丁·贝克尔

抱歉,我不完全了解您要查找的内容,无法提供比您想出的解决方案更好的答案,而且-这似乎是个不错的选择。
2013年

好的,谢谢。我现在将接受我的建议作为解决方案。
Martin Becker 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.