Kiểm tra đơn vị với Spring Security


140

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 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 mô-đun Spring Security để xác định xem đó có phải là thứ chúng ta có thể / nên sử dụng hay không.

Yêu cầu bảo mật của chúng tôi là 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 vào một số phần nhất định của trang web (chẳng hạn như để có được thông tin về tài khoản của họ); và có một số trang trên trang web (Câu hỏi thường gặp, Hỗ trợ, 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ố bộ điều khiển kiểm tra xem liệu đối tượng này có trong phiên để lấy tham chiếu đến tên người dùng đã đăng nhập không, chẳng hạn. Thay vào đó, tôi đang tìm cách thay thế logic được trồng tại nhà này bằng Spring Security, điều này sẽ có lợi ích tốt khi loại bỏ bất kỳ loại "làm thế nào để chúng tôi theo dõi người dùng đã đăng nhập?" và "làm thế nào để chúng tôi xác thực người dùng?" từ bộ điều khiển / mã doanh nghiệp của tôi.

Có vẻ như Spring Security cung cấp một đối tượng "ngữ cảnh" (trên mỗi luồng) để có thể truy cập tên người dùng / thông tin chính từ bất kỳ đâu trong ứng dụng của bạn ...

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

... có vẻ rất không giống mùa xuân vì đối tượng này là một đơn vị (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, thì cách nào được chấp nhận để đưa đối tượng Xác thực vào SecurityContext để nó có sẵn cho các bài kiểm tra đơn vị của tôi khi kiểm tra đơn vị yêu cầu Người dùng đã được chứng thực?

Tôi có cần nối dây này trong phương thức 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á dài dòng. Có cách nào dễ hơn không?

Bản SecurityContextHolderthân vật thể có vẻ rất không giống mùa xuân ...

Câu trả lời:


48

Vấn đề là Spring Security không làm cho đối tượng Xác thực có sẵn dưới dạng một bean trong container, vì vậy không có cách nào để dễ dàng tiêm hoặc tự động lấy nó ra khỏi hộp.

Trước khi chúng tôi 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 container để lưu trữ Hiệu trưởng, đưa phần này vào một "Dịch vụ xác thực" (singleton) và sau đó đưa bean này vào các dịch vụ khác cần có 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 một bean có phạm vi phiên với thuộc tính "chính", đưa dịch vụ 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 trên auth thành công, và sau đó làm cho dịch vụ xác thực có sẵn cho các loại đậu khác khi bạn cầ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à tĩnh / Singleton và Spring không khuyến khích sử dụng những thứ đó nhưng việc triển khai chúng cần được xử lý phù hợp tùy thuộc vào môi trường: phiên được đặt trong một thùng chứa Servlet, được phân luồng trong thử nghiệm JUnit, v.v. 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.


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

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 cách nhận biết nếu nó hoạt động trong môi trường máy chủ web - nó sử dụng ThreadLocal trừ khi bạn định cấu hình nó để sử dụng một cái gì đó khác (chỉ có hai chế độ dựng sẵn khác là InheritableThreadLocal và toàn cầu)
matt b

Hạn chế duy nhất đối với việc sử dụng đậu trong phạm vi phiên / yêu cầu trong Spring là chúng sẽ thất bại trong thử nghiệm JUnit. Những gì bạn có thể làm là triển khai một phạm vi tùy chỉnh sẽ sử dụng phiên / yêu cầu nếu có sẵn và quay lại chủ đề là cần thiết. Tôi đoán là Spring Security đang làm một cái gì đó tương tự ...
cliff.meyers

Mục tiêu của tôi là xây dựng một api Rest mà không cần phiên. Có lẽ với một mã thông báo mới. Trong khi điều này không trả lời câu hỏi của tôi, nó giúp. Cảm ơn
Pomagranite

166

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

Điều khiển:

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

Kiểm tra:

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

2
@Leonardo nên Authentication athêm cái này vào bộ điều khiển ở đâu? Như tôi có thể hiểu trong mỗi lần gọi phương thức? Có ổn không khi "con đường mùa xuân" chỉ cần thêm nó, thay vì tiêm?
Oleg Kuts

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 bài kiểm tra ...
Łukasz Woźniczka

Làm điều đó trong @BeforeEach(JUnit5) hoặc @Before(JUnit 4). Tốt và đơn giản.
WesternGun

30

Không trả lời câu hỏi về cách tạo và tiêm các đối tượng Xác thực, Spring Security 4.0 cung cấp một số lựa chọn thay thế đáng hoan nghênh khi thử nghiệm. Các @WithMockUserchú 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ó tùy chọn sử dụng @WithUserDetailsđể mô phỏng UserDetailstrả về từ UserDetailsService, vd

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

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


29

Bạn hoàn toàn đúng khi lo ngại - 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 chế giễu các phụ thuộc của mình. Những gì tôi sẽ chỉ cho bạn là làm thế nào để bộ chứa Spring IoC thực hiện công việc bẩn thỉu cho bạn, để lại cho bạn mã gọn gàng, có thể kiểm tra được. SecurityContextHolder là một lớp khung và mặc dù có thể ổn khi 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ị giao diện gọn gàng hơn cho các thành phần UI (ví dụ như bộ điều khiển).

cliff.meyers đã đề cập một cách xung quanh nó - tạo ra loại "chính" của riêng bạn và đưa một thể hiện 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 nhà máy có thể là vé cho mã dễ đọc nhất.

Nó có thể hoạt động 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ế có lẽ bạn đã phải làm hầu hết điều này rồi. Tiếp theo, trong ngữ cảnh bean của bạn, hãy xác định một bean có phạm vi yêu cầu để 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ờ vào sự kỳ diệu của thẻ aop: scoped-proxy, phương thức tĩnh getUserDetails sẽ được gọi mỗi khi có yêu cầu HTTP mới xuất hiện và mọi tham chiếu đến thuộc tính currentUser sẽ được giải quyết chính xác. Bây giờ kiểm thử đơ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);
}

Hi vọng điêu nay co ich!


9

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

@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, ví dụ như giả định một đối tượng Xác thực, giả định SecurityContext để trả về Xác thực và cuối cùng giả định SecurityContextHolder để lấy SecurityContext, tuy nhiên nó rất linh hoạt và cho phép bạn kiểm tra các kịch bản như các đối tượng xác thực null vv mà không phải thay đổi mã (không kiểm tra) của bạn


7

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

Vâng, 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ì bối 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 truy cập đằng sau một lớp bao bọc được tiêm 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 quyề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 trong thời gian chạy hoặc trượt một số XML vào đường dẫn lớp. Ngay cả việc sử dụng phép chú thích cũng sẽ bị quá tải với 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.


4

Tôi đã hỏi cùng một câu hỏi ở đây và chỉ đăng một câu trả lời mà tôi mới tìm thấy. Câu trả lời ngắn gọn là: tiêm a SecurityContextvà chỉ tham khảo SecurityContextHoldertrong cấu hình Spring của bạn để có đượcSecurityContext


3

Chung

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

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

Xét nghiệm

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

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

Tuy nhiên, điều này sẽ chỉ trực tiếp điền vào SecurityContext. Nếu bạn muốn đảm bảo 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 điều 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;
        }
    };
}

2

Tôi sẽ xem xét các lớp kiểm tra trừu tượng của Spring và các đối tượng giả được nói đến ở đây . Chúng cung cấp một cách mạnh mẽ để tự động nối dây cho các đối tượng được quản lý Spring của bạn làm cho việc kiểm tra đơn vị và tích hợp dễ dàng hơn.


Mặc dù các lớp kiểm tra này rất hữu ích, tôi không chắc chúng có áp dụng ở đây không. Các thử nghiệm của tôi không có khái niệm về ApplicationContext - chúng không cần. 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 thử nghiệm chạy - nó chỉ cảm thấy bẩn khi phải đặt nó trong ThreadLocal trước
matt b

1

Xác thực là một thuộc tính của một luồng trong môi trường máy chủ giống như nó là một thuộc tính của một tiến trình trong HĐH. Có một ví dụ bean để truy cập thông tin xác thực sẽ gây bất tiện cho cấu hình và kết nối dây mà không có bất kỳ lợi ích nào.

Về xác thực thử nghiệm, có một số cách 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 một chú thích tùy chỉnh @Authenticatedvà trình nghe thực thi kiểm tra, quản lý nó. Kiểm tra DirtiesContextTestExecutionListenercảm hứng.


0

Sau khá nhiều công việc tôi đã có thể tái tạo hành vi mong muốn. Tôi đã mô phỏng đă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 những tính năng mới trong Spring Security 4.0 sẽ giúp việc 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 
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.