2008-12-11 69 views
94

Công ty của tôi đã đánh giá Spring MVC để xác định xem chúng tôi có nên sử dụng nó trong một trong các dự án tiếp theo của chúng tôi hay không. Cho đến nay tôi yêu những gì tôi đã thấy, và ngay bây giờ tôi đang xem xét mô-đun bảo mật mùa xuân để xác định xem đó là một cái gì đó chúng ta có thể/nên sử dụng.Thử nghiệm đơn vị với Spring Security

Yêu cầu bảo mật của chúng tôi khá cơ bản; người dùng chỉ cần có thể cung cấp tên người dùng và mật khẩu để có thể truy cập một số phần nhất định của trang web (chẳng hạn như để nhận thông tin về tài khoản của họ); và có một số ít trang trên trang web (FAQs, Support, v.v.) nơi người dùng ẩn danh sẽ được cấp quyền truy cập.

Trong nguyên mẫu tôi đã tạo, tôi đã lưu trữ một đối tượng "LoginCredentials" (chỉ chứa tên người dùng và mật khẩu) trong phiên cho người dùng được xác thực; một số điều khiển kiểm tra để xem nếu đối tượng này là trong phiên để có được một tham chiếu đến tên người dùng đăng nhập, ví dụ. Tôi đang tìm cách thay thế logic gia đình này bằng Spring Security để thay thế, điều này sẽ có lợi ích to lớn khi xóa bất kỳ loại "làm cách nào chúng tôi theo dõi người dùng đã đăng nhập?" và "cách chúng tôi xác thực người dùng?" từ bộ điều khiển/mã kinh doanh của tôi.

Nó có vẻ như mùa xuân Security cung cấp một "bối cảnh" đối tượng (mỗi chủ đề) để có thể truy cập vào username/thông tin chủ yếu từ bất cứ nơi nào trong ứng dụng của bạn ...

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 

... mà dường như rất un-Spring giống như đối tượng này là một singleton (toàn cầu), theo một cách nào đó. Câu hỏi của tôi là: nếu đây là cách tiêu chuẩn để truy cập thông tin về người dùng đã được xác thực trong Spring Security, cách được chấp nhận để đưa một đối tượng Authentication vào SecurityContext để nó có sẵn cho các bài kiểm tra đơn vị của tôi là gì khi kiểm tra đơn vị yêu cầu người dùng đã được xác thực?

Tôi có cần phải thiết lập dây này theo phương pháp khởi tạo của từng trường hợp thử nghiệm không?

protected void setUp() throws Exception { 
    ... 
    SecurityContextHolder.getContext().setAuthentication(
     new UsernamePasswordAuthenticationToken(testUser.getLogin(), testUser.getPassword())); 
    ... 
} 

Điều này có vẻ quá chi tiết. Có cách nào dễ hơn không?

Đối tượng SecurityContextHolder chính nó có vẻ rất un-Xuân-like ...

Trả lời

31

Vấn đề là bảo mật mùa xuân không làm cho đối tượng xác thực có sẵn dưới dạng hạt trong vùng chứa, do đó không có cách nào để dễ dàng chèn hoặc tự động loại bỏ nó ra khỏi hộp.

Trước khi bắt đầu sử dụng Spring Security, chúng tôi sẽ tạo một bean có phạm vi phiên trong vùng chứa để lưu trữ Hiệu trưởng, đưa nó vào "AuthenticationService" (singleton) và sau đó tiêm bean này vào các dịch vụ khác cần kiến ​​thức về Hiệu trưởng hiện tại.

Nếu bạn đang triển khai dịch vụ xác thực của riêng mình, về cơ bản bạn có thể làm điều tương tự: tạo bean phiên có thuộc tính "chính", đưa mã này vào dịch vụ xác thực của bạn, dịch vụ xác thực đặt thuộc tính thành công auth, và sau đó làm cho dịch vụ auth có sẵn cho các bean khác khi bạn cần nó.

Tôi sẽ không cảm thấy quá tệ khi sử dụng SecurityContextHolder. Tuy nhiên.Tôi biết rằng đó là một tĩnh/Singleton và Spring không khuyến khích sử dụng những thứ như vậy nhưng việc triển khai của chúng sẽ cẩn thận để hành xử một cách thích hợp tùy thuộc vào môi trường: session-scoped trong một container Servlet, thread-scoped trong một bài kiểm tra JUnit, vv. của Singleton là khi nó cung cấp một triển khai không linh hoạt với các môi trường khác nhau.

+0

Cảm ơn, đây là lời khuyên hữu ích. Những gì tôi đã làm cho đến nay là về cơ bản để tiến hành với gọi SecurityContextHolder.getContext() (thông qua một vài phương pháp wrapper của riêng tôi, vì vậy ít nhất nó chỉ được gọi là từ một lớp). –

+2

Mặc dù chỉ là một lưu ý - Tôi không nghĩ rằng ServletContextHolder có bất kỳ khái niệm nào về HttpSession hoặc một cách để biết liệu nó có hoạt động trong môi trường máy chủ web hay không. InheritableThreadLocal và Global) –

+0

Hạn chế duy nhất đối với việc sử dụng bean/session-scoped trong Spring là chúng sẽ thất bại trong một bài kiểm tra JUnit. Những gì bạn có thể làm là triển khai phạm vi tùy chỉnh sẽ sử dụng phiên/yêu cầu nếu có và quay trở lại chuỗi là cần thiết. Tôi đoán là Spring Security đang làm một cái gì đó tương tự ... –

2

tôi sẽ có một cái nhìn tại các lớp học thử nghiệm trừu tượng mùa xuân và các đối tượng giả được nói về here. Chúng cung cấp một cách mạnh mẽ để tự động kết nối các đối tượng được quản lý Spring của bạn, giúp việc kiểm tra đơn vị và tích hợp dễ dàng hơn.

+0

Trong khi những lớp học thử nghiệm rất hữu ích, tôi không chắc chắn nếu họ áp dụng ở đây.Các bài kiểm tra của tôi không có khái niệm về ApplicationContext - chúng không cần một. Tất cả những gì tôi cần là đảm bảo rằng SecurityContext được điền trước khi phương thức test chạy - nó chỉ cảm thấy bẩn khi phải đặt nó trong một ThreadLocal đầu tiên –

26

Bạn hoàn toàn đúng khi quan tâm - các cuộc gọi phương thức tĩnh đặc biệt có vấn đề đối với thử nghiệm đơn vị vì bạn không thể dễ dàng giả lập các phụ thuộc của bạn. Những gì tôi sẽ chỉ cho bạn là làm thế nào để cho container Spring IoC thực hiện công việc bẩn thỉu cho bạn, để lại cho bạn một đoạn code gọn gàng, có thể kiểm thử. SecurityContextHolder là một lớp khung và trong khi nó có thể được ok cho mã bảo mật cấp thấp của bạn được gắn với nó, bạn có thể muốn hiển thị một giao diện neater cho các thành phần giao diện người dùng của bạn (tức là bộ điều khiển).

cliff.meyers được đề cập một cách chung quanh - tạo loại "chính" của riêng bạn và đưa một cá thể vào người tiêu dùng. Thẻ Spring < aop:scoped-proxy /> được giới thiệu trong 2.x kết hợp với định nghĩa bean phạm vi yêu cầu và hỗ trợ phương thức gốc có thể là vé cho mã dễ đọc nhất.

Nó có thể làm việc như sau:

public class MyUserDetails implements UserDetails { 
    // this is your custom UserDetails implementation to serve as a principal 
    // implement the Spring methods and add your own methods as appropriate 
} 

public class MyUserHolder { 
    public static MyUserDetails getUserDetails() { 
     Authentication a = SecurityContextHolder.getContext().getAuthentication(); 
     if (a == null) { 
      return null; 
     } else { 
      return (MyUserDetails) a.getPrincipal(); 
     } 
    } 
} 

public class MyUserAwareController {   
    MyUserDetails currentUser; 

    public void setCurrentUser(MyUserDetails currentUser) { 
     this.currentUser = currentUser; 
    } 

    // controller code 
} 

Không có gì phức tạp cho đến nay, phải không? Trong thực tế, bạn có lẽ đã phải làm hầu hết điều này rồi. Tiếp theo, trong bối cảnh đậu của bạn xác định một bean request-scoped để giữ hiệu trưởng:

<bean id="userDetails" class="MyUserHolder" factory-method="getUserDetails" scope="request"> 
    <aop:scoped-proxy/> 
</bean> 

<bean id="controller" class="MyUserAwareController"> 
    <property name="currentUser" ref="userDetails"/> 
    <!-- other props --> 
</bean> 

Nhờ sự kỳ diệu của aop: thẻ scoped-proxy, phương pháp getUserDetails tĩnh sẽ được gọi mỗi khi một yêu cầu HTTP mới đến và bất kỳ tham chiếu nào đến thuộc tính currentUser sẽ được giải quyết chính xác. Bây giờ thử nghiệm đơn vị trở nên tầm thường:

protected void setUp() { 
    // existing init code 

    MyUserDetails user = new MyUserDetails(); 
    // set up user as you wish 
    controller.setCurrentUser(user); 
} 

Hy vọng điều này sẽ hữu ích!

+3

Đây là gợi ý tốt nhất mà tôi đã xem để giải quyết vấn đề này, tôi không thể tin rằng Spring đã không xây dựng một cái gì đó như thế này trong. –

3

Tôi đã tự hỏi mình câu hỏi tương tự trên here và vừa đăng câu trả lời mà tôi vừa tìm thấy gần đây. Câu trả lời ngắn gọn là: tiêm SecurityContext và chỉ tham chiếu SecurityContextHolder trong cấu hình Spring của bạn để có được SecurityContext

5

Sử dụng tĩnh trong trường hợp này là cách tốt nhất để viết mã bảo mật.

Có, thống kê nói chung là xấu - nói chung, nhưng trong trường hợp này, tĩnh là những gì bạn muốn. Vì ngữ cảnh bảo mật liên kết một Hiệu trưởng với luồng hiện đang chạy, mã bảo mật nhất sẽ truy cập tĩnh từ luồng càng trực tiếp càng tốt. Ẩn quyền truy cập đằng sau lớp trình bao bọc được cung cấp cho kẻ tấn công có nhiều điểm hơn để tấn công. Họ sẽ không cần truy cập vào mã (mà họ sẽ gặp khó khăn khi thay đổi nếu jar đã được ký), họ chỉ cần một cách để ghi đè cấu hình, có thể được thực hiện khi chạy hoặc trượt một số XML lên đường dẫn lớp. Ngay cả việc sử dụng tính năng chèn chú thích cũng có thể được ghi đè bằng XML bên ngoài. XML như vậy có thể tiêm hệ thống đang chạy với một hiệu trưởng giả mạo.

8

Cá nhân tôi sẽ chỉ sử dụng Powermock cùng với Mockito hoặc Easymock để giả lập static SecurityContextHolder.getSecurityContext() trong bài kiểm tra đơn vị/tích hợp của bạn, ví dụ:

@RunWith(PowerMockRunner.class) 
@PrepareForTest(SecurityContextHolder.class) 
public class YourTestCase { 

    @Mock SecurityContext mockSecurityContext; 

    @Test 
    public void testMethodThatCallsStaticMethod() { 
     // Set mock behaviour/expectations on the mockSecurityContext 
     when(mockSecurityContext.getAuthentication()).thenReturn(...) 
     ... 
     // Tell mockito to use Powermock to mock the SecurityContextHolder 
     PowerMockito.mockStatic(SecurityContextHolder.class); 

     // use Mockito to set up your expectation on SecurityContextHolder.getSecurityContext() 
     Mockito.when(SecurityContextHolder.getSecurityContext()).thenReturn(mockSecurityContext); 
     ... 
    } 
} 

Phải thừa nhận rằng có khá nhiều mã tấm nồi hơi ở đây tức là chế nhạo một đối tượng xác thực, nhạo báng một SecurityContext để trả lại xác thực và cuối cùng là chế nhạo các SecurityContextHolder để có được những SecurityContext, tuy nhiên nó rất linh hoạt và cho phép bạn đơn vị kiểm tra các kịch bản như đối tượng Xác thực null, v.v.mà không cần phải thay đổi (không kiểm tra) của bạn đang

97

Chỉ cần làm điều đó theo cách thông thường và sau đó chèn nó bằng cách sử SecurityContextHolder.setContext() trong lớp thử nghiệm của bạn, ví dụ:

Bộ điều khiển:

Authentication a = SecurityContextHolder.getContext().getAuthentication(); 

Test:

Authentication authentication = Mockito.mock(Authentication.class); 
// Mockito.whens() for your authorization object 
SecurityContext securityContext = Mockito.mock(SecurityContext.class); 
Mockito.when(securityContext.getAuthentication()).thenReturn(authentication); 
SecurityContextHolder.setContext(securityContext); 
+1

@Leonardo, nơi này nên "Xác thực a' được thêm vào bộ điều khiển? Như tôi có thể hiểu trong mỗi lời kêu gọi phương pháp? Liệu có ổn cho "con đường mùa xuân" chỉ để thêm nó, thay vì tiêm? –

+0

Nhưng hãy nhớ rằng nó sẽ không hoạt động với TestNG vì SecurityContextHolder giữ biến chủ đề cục bộ để bạn chia sẻ biến này giữa các lần kiểm tra ... –

1

Xác thực là thuộc tính của chuỗi trong môi trường máy chủ giống như thuộc tính của quy trình trong hệ điều hành. Có một cá thể đậu để truy cập thông tin xác thực sẽ là cấu hình bất tiện và dây dẫn trên không có bất kỳ lợi ích nào.

Về xác thực kiểm tra, có một số cách giúp bạn có thể làm cho cuộc sống của mình dễ dàng hơn. Yêu thích của tôi là tạo chú thích tùy chỉnh @Authenticated và trình xử lý thực thi kiểm tra, quản lý nó. Kiểm tra DirtiesContextTestExecutionListener để lấy cảm hứng.

0

Sau khá nhiều công việc tôi đã có thể tạo lại hành vi mong muốn. Tôi đã mô phỏng thông tin đăng nhập thông qua MockMvc. Nó quá nặng đối với hầu hết các bài kiểm tra đơn vị nhưng hữu ích cho các bài kiểm tra tích hợp.

Tất nhiên tôi sẵn sàng xem các tính năng mới trong Spring Security 4.0 sẽ giúp cho thử nghiệm của chúng tôi dễ dàng hơn.

package [myPackage] 

import static org.junit.Assert.*; 

import javax.inject.Inject; 
import javax.servlet.http.HttpSession; 

import org.junit.Before; 
import org.junit.Test; 
import org.junit.experimental.runners.Enclosed; 
import org.junit.runner.RunWith; 
import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.mock.web.MockHttpServletRequest; 
import org.springframework.security.core.context.SecurityContext; 
import org.springframework.security.core.context.SecurityContextHolder; 
import org.springframework.security.web.FilterChainProxy; 
import org.springframework.security.web.context.HttpSessionSecurityContextRepository; 
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; 

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 

@ContextConfiguration(locations={[my config file locations]}) 
@WebAppConfiguration 
@RunWith(SpringJUnit4ClassRunner.class) 
public static class getUserConfigurationTester{ 

    private MockMvc mockMvc; 

    @Autowired 
    private FilterChainProxy springSecurityFilterChain; 

    @Autowired 
    private MockHttpServletRequest request; 

    @Autowired 
    private WebApplicationContext webappContext; 

    @Before 
    public void init() { 
     mockMvc = MockMvcBuilders.webAppContextSetup(webappContext) 
        .addFilters(springSecurityFilterChain) 
        .build(); 
    } 


    @Test 
    public void testTwoReads() throws Exception{       

    HttpSession session = mockMvc.perform(post("/j_spring_security_check") 
         .param("j_username", "admin_001") 
         .param("j_password", "secret007")) 
         .andDo(print()) 
         .andExpect(status().isMovedTemporarily()) 
         .andExpect(redirectedUrl("/index")) 
         .andReturn() 
         .getRequest() 
         .getSession(); 

    request.setSession(session); 

    SecurityContext securityContext = (SecurityContext) session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); 

    SecurityContextHolder.setContext(securityContext); 

     // Your test goes here. User is logged with 
} 
2

chung

Trong khi đó (kể từ phiên bản 3.2, trong năm 2013, nhờ vào SEC-2298) xác thực có thể được tiêm vào phương pháp MVC sử dụng chú thích @AuthenticationPrincipal:

@Controller 
class Controller { 
    @RequestMapping("/somewhere") 
    public void doStuff(@AuthenticationPrincipal UserDetails myUser) { 
    } 
} 

thử nghiệm

Trong thử nghiệm đơn vị của bạn, bạn rõ ràng có thể gọi trực tiếp phương thức này. Trong các thử nghiệm tích hợp sử dụng org.springframework.test.web.servlet.MockMvc bạn có thể sử dụng org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user() để tiêm cho người sử dụng như thế này:

mockMvc.perform(get("/somewhere").with(user(myUserDetails))); 

này sẽ tuy nhiên chỉ trực tiếp điền vào SecurityContext. Nếu bạn muốn chắc chắn rằng người dùng được tải từ một phiên trong thử nghiệm của bạn, bạn có thể sử dụng này:

mockMvc.perform(get("/somewhere").with(sessionUser(myUserDetails))); 
/* ... */ 
private static RequestPostProcessor sessionUser(final UserDetails userDetails) { 
    return new RequestPostProcessor() { 
     @Override 
     public MockHttpServletRequest postProcessRequest(final MockHttpServletRequest request) { 
      final SecurityContext securityContext = new SecurityContextImpl(); 
      securityContext.setAuthentication(
       new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()) 
      ); 
      request.getSession().setAttribute(
       HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext 
      ); 
      return request; 
     } 
    }; 
} 
16

Nếu không có cách trả lời các câu hỏi về cách tạo và tiêm đối tượng xác thực, Spring Security 4.0 cung cấp một số lựa chọn thay thế hoan nghênh khi nói đến thử nghiệm. Các @WithMockUser chú thích cho phép các nhà phát triển để xác định một người sử dụng giả (với cơ quan chức năng bắt buộc, tên người dùng, mật khẩu và vai trò) theo một cách gọn gàng:

@Test 
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" }) 
public void getMessageWithMockUserCustomAuthorities() { 
    String message = messageService.getMessage(); 
    ... 
} 

Ngoài ra còn có các tùy chọn để sử dụng @WithUserDetails để mô phỏng một UserDetails trở về từ các UserDetailsService , ví dụ

@Test 
@WithUserDetails("customUsername") 
public void getMessageWithUserDetailsCustomUsername() { 
    String message = messageService.getMessage(); 
    ... 
} 

Thông tin chi tiết có thể được tìm thấy trong @WithMockUser@WithUserDetails chương trong tài liệu tham khảo Xuân An (mà từ đó các ví dụ trên, nơi sao chép)

Các vấn đề liên quan