2013-03-04 41 views
49

Tôi đã cố gắng tìm ra cách kiểm tra đơn vị nếu các URL của các bộ điều khiển của tôi được bảo mật đúng cách. Chỉ trong trường hợp ai đó thay đổi mọi thứ xung quanh và vô tình xóa cài đặt bảo mật.Kiểm tra và bảo mật mùa xuân: Cách xác thực giả?

phương pháp điều khiển của tôi trông như thế này:

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

tôi thiết lập một WebTestEnvironment như vậy:

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

Trong thử nghiệm thực tế của tôi, tôi đã cố gắng để làm một cái gì đó như thế này:

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

} 

Tôi đã chọn thẻ này tại đây:

Tuy nhiên, nếu nhìn kỹ này chỉ giúp khi không gửi các yêu cầu thực tế để URL, nhưng chỉ khi thử nghiệm các dịch vụ trên mức chức năng. Trong trường hợp của tôi là "access denied" ngoại lệ được ném:

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] 
     ... 

Hai thông điệp log sau đây là đáng chú ý về cơ bản nói rằng không có người dùng được xác nhận chỉ ra rằng thiết lập Principal đã không làm việc, hoặc rằng nó đã được ghi đè.

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.sprin[email protected]9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.sprin[email protected]957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS 
+0

tên công ty của bạn, eu.ubicon, được hiển thị trong nhập khẩu của bạn . Đó không phải là một nguy cơ bảo mật? –

+0

Xin chào, cảm ơn vì đã bình luận! Tôi không thể hiểu tại sao. Đó là phần mềm nguồn mở anyway. Nếu bạn quan tâm, hãy xem https://bitbucket.org/ubicon/ubicon (hoặc https://bitbucket.org/dmir_wue/everyaware cho ngã ba mới nhất). Hãy cho tôi biết nếu tôi bỏ lỡ một cái gì đó. –

+0

Kiểm tra giải pháp này (câu trả lời dành cho mùa xuân 4): https://stackoverflow.com/questions/14308341/how-to-login-a-user-with-spring-3-2-new-mvc-testing/47069613 # 47069613 –

Trả lời

39

Hóa ra là các SecurityContextPersistenceFilter, là một phần của chuỗi lọc Xuân An, luôn reset SecurityContext của tôi, mà tôi thiết kêu gọi SecurityContextHolder.getContext().setAuthentication(principal) (hoặc bằng cách sử dụng phương pháp .principal(principal)). Bộ lọc này đặt SecurityContext trong số SecurityContextHolder với sốtừ một số SecurityContextRepositoryOVERWRITING cái tôi đã đặt trước đó. Kho theo mặc định là HttpSessionSecurityContextRepository. HttpSessionSecurityContextRepository kiểm tra HttpRequest nhất định và cố gắng truy cập vào HttpSession tương ứng. Nếu nó tồn tại, nó sẽ cố gắng đọc SecurityContext từ HttpSession. Nếu điều này không thành công, kho lưu trữ sẽ tạo ra SecurityContext trống.

Vì vậy, giải pháp của tôi là để vượt qua một HttpSession cùng với yêu cầu, mà giữ 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()); 
    } 
} 
+3

Tác phẩm này hoạt động, nhưng đây có phải là phương pháp hay nhất này không? Một cách dễ dàng hơn? –

+2

Chúng tôi chưa thêm hỗ trợ chính thức cho Spring Security. Xem https://jira.springsource.org/browse/SEC-2015 Một phác thảo cho nó trông như thế nào được chỉ định trong https://github.com/SpringSource/spring-test-mvc/blob/master/src/test /java/org/springframework/test/web/server/samples/context/SpringSecurityTests.java –

+0

Tôi không nghĩ rằng việc tạo một đối tượng Authentication và thêm một session với thuộc tính tương ứng là xấu. Bạn có nghĩ đây là một "công việc xung quanh" hợp lệ không? Hỗ trợ trực tiếp, mặt khác sẽ là tuyệt vời của khóa học. Trông khá gọn gàng. Cảm ơn các liên kết! –

2

Tùy chọn để tránh sử dụng SecurityContextHolder trong các thử nghiệm:

  • Lựa chọn 1: sử dụng mocks - Tôi có nghĩa là giả SecurityContextHolder sử dụng một số thư viện giả - EasyMock ví dụ
  • Lựa chọn 2: quấn gọi SecurityContextHolder.get... trong mã của bạn trong một số dịch vụ - ví dụ như trong SecurityServiceImpl với phương pháp getCurrentPrincipal mà thực hiện giao diện SecurityService và sau đó trong các thử nghiệm của bạn, bạn có thể dễ dàng tạo thực hiện mô hình của giao diện này trả về hiệu trưởng mong muốn mà không cần truy cập vào SecurityContextHolder .
+0

Mh, có lẽ tôi không nhận được toàn bộ bức tranh. Vấn đề của tôi là SecurityContextPersistenceFilter thay thế SecurityContext bằng SecurityContext từ một HttpSessionSecurityContextRepository, mà lần lượt đọc SecurityContext từ HttpSession tương ứng. Vì vậy, giải pháp sử dụng phiên. Về cuộc gọi đến SecurityContextHolder: Tôi đã chỉnh sửa câu trả lời của mình để tôi không sử dụng cuộc gọi đến SecurityContextHolder nữa. Nhưng cũng không giới thiệu bất kỳ thư viện gói hoặc thêm mocking. Bạn có nghĩ rằng, đây là một giải pháp tốt hơn? –

+0

Xin lỗi tôi đã không hiểu chính xác những gì bạn đang tìm kiếm và tôi không thể cung cấp câu trả lời tốt hơn so với giải pháp bạn đã đưa ra và - nó có vẻ là một lựa chọn tốt. –

+0

Được rồi, cảm ơn. Tôi sẽ chấp nhận đề nghị của tôi như là một giải pháp cho bây giờ. –

5

Dưới đây là ví dụ cho những ai muốn Kiểm tra cấu hình bảo mật Spring MockMvc bằng cách sử dụng xác thực cơ sở Base64.

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 phụ thuộc

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

Thêm vào pom.xml:

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

và sử dụng org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors cho yêu cầu cấp phép. Xem cách sử dụng mẫu tại https://github.com/rwinch/spring-security-test-blog (https://jira.spring.io/browse/SEC-2592).

Cập nhật:

4.0.0.RC2 hoạt động để bảo mật mùa xuân 3.x. Để bảo vệ mùa xuân 4 mùa xuân-an ninh-kiểm tra trở thành một phần của an ninh mùa xuân (http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test, phiên bản là như nhau).

Thiết lập được thay đổi: 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(); 
} 

mẫu cho cơ bản-authentication: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication.

+0

Đoán tính năng này chỉ hoạt động cho các ứng dụng Mùa xuân 4? –

+0

Điều này cũng khắc phục được sự cố của tôi khi nhận được 404 khi cố gắng đăng nhập qua bộ lọc bảo mật đăng nhập. Cảm ơn! –

+0

Xin chào, trong khi thử nghiệm như đã đề cập bởi GKislin. Tôi nhận được lỗi sau "Xác thực không thành công UserDetailsService trả về null, mà là một vi phạm hợp đồng giao diện". Bất kỳ đề nghị xin vui lòng. final AuthenticationRequest auth = new AuthenticationRequest(); \t \t auth.setUsername (userId); \t \t auth.setPassword (mật khẩu); mockMvc.perform (nội dung bài ("/ api/auth /"). (json (auth)). contentType (MediaType.APPLICATION_JSON)); – Sanjeev

16

Tìm kiếm câu trả lời Tôi không thể tìm thấy bất kỳ sự dễ dàng và linh hoạt nào cùng một lúc, sau đó tôi tìm thấy số Spring Security Reference và tôi nhận thấy có những giải pháp hoàn hảo gần. giải pháp AOP thường là những người vĩ đại nhất để thử nghiệm, và Spring cung cấp nó với @WithMockUser, @WithUserDetails@WithSecurityContext, trong hiện tượng này:

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

Trong hầu hết các trường hợp, @WithUserDetails tập hợp sự linh hoạt và sức mạnh tôi cần.

Cách hoạt động của @WithUserDetails?

Về cơ bản, bạn chỉ cần tạo một tùy chỉnh UserDetailsService với tất cả các hồ sơ người dùng có thể bạn muốn kiểm tra. E.g

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

Bây giờ chúng ta có những người dùng của chúng tôi đã sẵn sàng, vì vậy tưởng tượng chúng ta muốn thử nghiệm kiểm soát quyền truy cập vào chức năng điều khiển này:

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

Ở đây chúng ta có một được chức năng ánh xạ tới các tuyến đường /foo/salute và chúng tôi đang thử nghiệm bảo mật dựa trên vai trò với chú thích @Secured, mặc dù bạn cũng có thể thử nghiệm @PreAuthorize@PostAuthorize. Hãy tạo hai bài kiểm tra, một để kiểm tra xem người dùng hợp lệ có thể xem phản hồi chào hàng này và câu trả lời khác để kiểm tra xem nó có bị cấm hay không.

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

Như bạn thấy, chúng tôi đã nhập SpringSecurityWebAuxTestConfig để cung cấp cho người dùng của chúng tôi để thử nghiệm. Mỗi loại được sử dụng trên trường hợp thử nghiệm tương ứng của nó chỉ bằng cách sử dụng chú thích đơn giản, giảm mã và độ phức tạp. sử dụng @WithMockUser

tốt hơn cho vai trò đơn giản Bảo mật dựa trên

Như bạn thấy @WithUserDetails có tất cả các linh hoạt cần thiết cho hầu hết các ứng dụng của bạn. Nó cho phép bạn sử dụng người dùng tùy chỉnh với bất kỳ GrantedAuthority nào, như vai trò hoặc quyền. Nhưng nếu bạn chỉ làm việc với vai trò, việc thử nghiệm có thể dễ dàng hơn và bạn có thể tránh việc xây dựng một tùy chỉnh UserDetailsService. Trong những trường hợp như vậy, hãy chỉ định kết hợp đơn giản của người dùng, mật khẩu và vai trò với @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"; 
} 

Chú thích xác định giá trị mặc định cho người dùng rất cơ bản. Như trong trường hợp của chúng tôi, tuyến đường mà chúng tôi đang thử nghiệm chỉ yêu cầu người dùng được xác thực là người quản lý, chúng tôi có thể thoát bằng cách sử dụng SpringSecurityWebAuxTestConfig và thực hiện việc này.

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

Chú ý rằng bây giờ thay vì người dùng [email protected] chúng tôi đang nhận được mặc định được cung cấp bởi @WithMockUser: dùng; nhưng nó sẽ không quan trọng bởi vì những gì chúng tôi thực sự quan tâm là vai trò của mình: ROLE_MANAGER.

Kết luận

Như bạn thấy với các chú thích như @WithUserDetails@WithMockUser chúng ta có thể chuyển đổi giữa kịch bản người dùng xác thực khác nhau mà không xây dựng lớp xa lánh từ kiến ​​trúc của chúng tôi chỉ cho làm các xét nghiệm đơn giản. Nó cũng khuyên bạn xem cách @WithSecurityContext hoạt động để linh hoạt hơn nữa.

2

Câu trả lời ngắn:

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

Sau khi thực hiện formLogin từ kiểm tra an ninh mùa xuân mỗi yêu cầu của bạn sẽ được tự động gọi là đăng nhập người dùng.

Long trả lời:

Kiểm tra giải pháp này (câu trả lời là cho mùa xuân 4): How to login a user with spring 3.2 new mvc testing

4

Kể từ mùa xuân 4.0+, giải pháp tốt nhất là để chú thích các phương pháp thử nghiệm với @WithMockUser

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

Hãy nhớ thêm phụ thuộc sau đây để dự án của bạn

'org.springframework.security:spring-security-test:4.2.3.RELEASE' 
Các vấn đề liên quan