Spring测试和安全:如何模拟身份验证?

15 浏览
0 Comments

Spring测试和安全:如何模拟身份验证?

我试图找出如何单元测试我的控制器的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

admin 更改状态以发布 2023年5月21日
0
0 Comments

从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'

0
0 Comments

在寻找易用和灵活性之间的答案时,我找到了Spring Security参考,我意识到那里有近乎完美的解决方案。在测试中,AOP解决方案通常是最好的解决方案,Spring提供了@WithMockUser@WithUserDetails@WithSecurityContext,以实现这个工件:\n


    org.springframework.security
    spring-security-test
    4.2.2.RELEASE
    test

\n在大多数情况下,@WithUserDetails提供了我所需的灵活性和功能。\n

如何使用@WithUserDetails?

\n基本上,你只需要创建一个自定义的UserDetailsService,其中包含你想要测试的所有可能的用户配置文件。例如:\n

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

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

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

\n在这里,我们有一个映射到路由/foo/salute的get函数,并且我们正在使用@Secured注释进行角色基础安全测试,尽管你也可以进行@PreAuthorize@PostAuthorize测试。\n让我们创建两个测试,一个用于检查有效用户是否可以看到这个敬礼响应,另一个用于检查它是否真的被禁止。\n

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

\n正如你所看到的,我们导入了 SpringSecurityWebAuxTestConfig ,为测试提供我们的用户。每个测试案例使用其对应的简单注释即可。这样可以减少代码和复杂性。\n

更好的使用@WithMockUser进行简单的基于角色的安全性

\n正如你所看到的,@WithUserDetails具有大多数应用程序所需的所有灵活性。它允许你使用任何GrantedAuthority,如角色或权限的自定义用户。但是如果你只使用角色进行工作,那么测试就会变得更加简单,可以避免构建自定义 UserDetailsService 。在这种情况下,使用@WithMockUser指定简单的用户、密码和角色组合即可。\n

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

\n该注释为一个非常基本的用户定义默认值。由于我们正在测试的路由只要求已认证的用户是经理,因此我们可以放弃使用 SpringSecurityWebAuxTestConfig ,并且可以这样做。\n

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

\n请注意,现在我们不再使用用户 manager@company.com ,而是使用@WithMockUser提供的默认用户: user ;然而,这并不重要,因为我们真正关心的是他的角色: ROLE_MANAGER 。\n

结论

\n如你所看到的,使用注释@WithUserDetails@WithMockUser,我们可以在不构建与我们的架构疏远的类的情况下,在不同的认证用户场景之间切换,以进行简单的测试。还建议您查看@WithSecurityContext,以获得更灵活的功能。

0