如果我的控制器的网址得到妥善保护,我试图弄清楚如何进行单元测试。以防有人改变现状并意外删除安全设置。
我的控制器方法如下所示:
@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
答案 0 :(得分:58)
寻找答案我无法在同一时间找到任何容易和灵活的东西,然后我找到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
会收集我需要的灵活性和力量。
基本上,您只需创建一个自定义UserDetailsService
,其中包含您要测试的所有可能的用户个人资料。 E.g
@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());
}
}
这里我们有一个获取映射函数到路径 / foo / salute ,我们正在使用@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
以便为我们的用户提供测试。每一个都使用相应的测试用例,只需使用简单的注释,减少代码和复杂性。
如您所见@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";
}
注释定义了非常基本的用户的默认值。在我们的案例中,我们正在测试的路由只需要经过身份验证的用户是管理员,我们可以使用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")));
}
请注意,我们现在取代用户 manager@company.com ,我们将获得@WithMockUser
提供的默认值:用户;但它不重要,因为我们真正关心的是他的角色:ROLE_MANAGER
。
正如您所看到的@WithUserDetails
和@WithMockUser
这样的注释,我们可以在不同的经过身份验证的用户场景之间切换,而无需构建与我们的架构疏远的类,只是为了进行简单的测试。它还建议您了解@WithSecurityContext如何工作以获得更大的灵活性。
答案 1 :(得分:47)
事实证明,作为Spring Security过滤器链一部分的SecurityContextPersistenceFilter
始终重置我设置为SecurityContext
的{{1}}(或使用{{1} }} 方法)。此过滤器将SecurityContextHolder.getContext().setAuthentication(principal)
中的.principal(principal)
与SecurityContext
OVERWRITING 中的SecurityContextHolder
设置为我之前设置的SecurityContext
。默认情况下,存储库为SecurityContextRepository
。 HttpSessionSecurityContextRepository
检查给定的HttpSessionSecurityContextRepository
并尝试访问相应的HttpRequest
。如果存在,它将尝试从HttpSession
读取SecurityContext
。如果失败,则存储库将生成空HttpSession
。
因此,我的解决方案是将SecurityContext
与请求一起传递,该请求包含HttpSession
:
SecurityContext
答案 2 :(得分: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-blog处的示例用法
(https://jira.spring.io/browse/SEC-2592)。
更新
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,版本是相同的)。
设置已更改: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();
}
答案 3 :(得分:30)
从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'
答案 4 :(得分: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>
答案 5 :(得分: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;
}
从Spring安全测试中执行formLogin
后,您的每个请求都将自动调用为登录用户。
答案很长:
检查此解决方案(答案是针对春季4):How to login a user with spring 3.2 new mvc testing
答案 6 :(得分:2)
避免在测试中使用SecurityContextHolder的选项:
SecurityContextHolder
使用一些模拟库 - 例如 EasyMock SecurityContextHolder.get...
- 例如在SecurityServiceImpl
中使用实现getCurrentPrincipal
接口的方法SecurityService
然后在测试中,您可以简单地创建此接口的模拟实现,该实现返回所需的主体,而无需访问SecurityContextHolder
。 答案 7 :(得分:0)
虽然很晚才回答。但这对我有用,并且可能有用。
在使用Spring Security和mockMvc时,您需要使用的是@WithMockUser注释,就像其他提到的一样。
Spring安全性还提供了另一个名为 @WithAnonymousUser
的注释,用于测试未经身份验证的请求。但是,在这里您应该小心。您可能期望401,但默认情况下却收到403禁止错误。在实际场景中,当您运行实际服务时,将其重定向并最终获得正确的401响应代码。将此注释用于匿名请求。
您可能还会想到省略注释,并只是对其进行未经授权的操作。但这通常会引发正确的异常(例如AuthenticationException),但是如果正确处理(如果使用自定义处理程序),则会获得正确的状态代码。我曾经为此获得500。因此,请查找调试器中引发的异常,并检查是否正确处理了该异常并返回正确的状态代码。
答案 8 :(得分:0)
在您的测试包上创建一个类 TestUserDetailsImpl
:
@Service
@Primary
@Profile("test")
public class TestUserDetailsImpl implements UserDetailsService {
public static final String API_USER = "apiuser@example.com";
private User getAdminUser() {
User user = new User();
user.setUsername(API_USER);
SimpleGrantedAuthority role = new SimpleGrantedAuthority("ROLE_API_USER");
user.setAuthorities(Collections.singletonList(role));
return user;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
if (Objects.equals(username, ADMIN_USERNAME))
return getAdminUser();
throw new UsernameNotFoundException(username);
}
}
休息端点:
@GetMapping("/invoice")
@Secured("ROLE_API_USER")
public Page<InvoiceDTO> getInvoices(){
...
}
测试端点:
@Test
@WithUserDetails("apiuser@example.com")
public void testApi() throws Exception {
...
}