java - वसंत सुरक्षा के साथ यूनिट परीक्षण




security unit-testing (8)

मेरी कंपनी स्प्रिंग एमवीसी का मूल्यांकन कर रही है यह निर्धारित करने के लिए कि क्या हमें इसे हमारी अगली परियोजनाओं में से एक में उपयोग करना चाहिए। अब तक मैंने जो देखा है उससे प्यार करता हूं, और अभी मैं वसंत सुरक्षा मॉड्यूल पर एक नज़र डाल रहा हूं यह निर्धारित करने के लिए कि क्या ऐसा कुछ है जिसे हम उपयोग / कर सकते हैं।

हमारी सुरक्षा आवश्यकताओं बहुत बुनियादी हैं; किसी उपयोगकर्ता को साइट के कुछ हिस्सों तक पहुंचने में सक्षम होने के लिए उपयोगकर्ता नाम और पासवर्ड प्रदान करने में सक्षम होना चाहिए (जैसे कि उनके खाते के बारे में जानकारी प्राप्त करना); और साइट पर कुछ हद तक पेज हैं (अक्सर पूछे जाने वाले प्रश्न, समर्थन, आदि) जहां एक अज्ञात उपयोगकर्ता को पहुंच दी जानी चाहिए।

प्रोटोटाइप में मैं बना रहा हूं, मैं प्रमाणित उपयोगकर्ता के लिए सत्र में "लॉगिन प्रमाण-पत्र" ऑब्जेक्ट (जिसमें केवल उपयोगकर्ता नाम और पासवर्ड शामिल है) संग्रहीत कर रहा है; कुछ नियंत्रक यह देखने के लिए जांच करते हैं कि लॉग इन-इन उपयोगकर्ता नाम का संदर्भ प्राप्त करने के लिए यह ऑब्जेक्ट सत्र में है या नहीं। मैं इस घर के उगाए गए तर्क को बदले में स्प्रिंग सिक्योरिटी के साथ बदलना चाहता हूं, जिसके किसी भी प्रकार के "हम उपयोगकर्ताओं में लॉग इन कैसे ट्रैक करते हैं?" को हटाने का अच्छा लाभ होगा। और "हम उपयोगकर्ताओं को कैसे प्रमाणित करते हैं?" मेरे नियंत्रक / व्यापार कोड से।

ऐसा लगता है जैसे स्प्रिंग सिक्योरिटी एक (प्रति-थ्रेड) "संदर्भ" ऑब्जेक्ट प्रदान करती है ताकि आप अपने ऐप में कहीं से भी उपयोगकर्ता नाम / मूल जानकारी तक पहुंच सकें ...

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

... जो बहुत ही वसंत लगता है जैसे कि यह वस्तु एक (वैश्विक) सिंगलटन है, एक तरह से।

मेरा सवाल यह है: यदि स्प्रिंग सिक्योरिटी में प्रमाणीकृत उपयोगकर्ता के बारे में जानकारी तक पहुंचने का यह मानक तरीका है, तो सुरक्षा कॉन्टेक्स्ट में प्रमाणीकरण ऑब्जेक्ट को इंजेक्ट करने का स्वीकार्य तरीका क्या है ताकि यूनिट परीक्षणों की आवश्यकता होने पर यह मेरे यूनिट परीक्षणों के लिए उपलब्ध हो प्रमाणीकृत उपयोगकर्ता?

क्या मुझे प्रत्येक टेस्ट केस की आरंभिक विधि में इसे तार करने की ज़रूरत है?

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

यह अत्यधिक वर्बोज़ लगता है। क्या कोई आसान तरीका है?

SecurityContextHolder ऑब्जेक्ट स्वयं बहुत ही वसंत-जैसा लगता है ...


सामान्य

इस बीच (संस्करण 3.2 के बाद, वर्ष 2013 में, SEC-2298 @AuthenticationPrincipal 9 SEC-2298 लिए धन्यवाद) प्रमाणीकरण एनोटेशन @AuthenticationPrincipal का उपयोग कर एमवीसी विधियों में इंजेक्शन दिया जा सकता है:

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

टेस्ट

आपके यूनिट परीक्षण में आप स्पष्ट रूप से इस विधि को सीधे कॉल कर सकते हैं। org.springframework.test.web.servlet.MockMvc का उपयोग करके एकीकरण परीक्षण में आप org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user() का उपयोग इस तरह उपयोगकर्ता को इंजेक्ट करने के लिए कर सकते हैं:

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

हालांकि यह केवल सुरक्षा कॉन्टेक्स्ट को भर देगा। यदि आप यह सुनिश्चित करना चाहते हैं कि उपयोगकर्ता आपके परीक्षण में किसी सत्र से लोड हो, तो आप इसका उपयोग कर सकते हैं:

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

आप चिंतित होने का काफी अधिकार हैं - स्थैतिक विधि कॉल यूनिट परीक्षण के लिए विशेष रूप से समस्याग्रस्त हैं क्योंकि आप आसानी से अपनी निर्भरताओं का नकल नहीं कर सकते हैं। मैं आपको दिखाने के लिए जा रहा हूं कि स्प्रिंग आईओसी कंटेनर आपके लिए गंदे काम कैसे करे, जिससे आपको साफ, टेस्टेबल कोड मिल जाए। SecurityContextHolder एक ढांचा वर्ग है और यह आपके निम्न-स्तरीय सुरक्षा कोड से जुड़ा हुआ हो सकता है, लेकिन शायद आप अपने यूआई घटकों (यानी नियंत्रक) में एक नीदर इंटरफ़ेस का पर्दाफाश करना चाहते हैं।

क्लिफ.मेयर्स ने इसके चारों ओर एक तरह का उल्लेख किया - अपना खुद का "प्रिंसिपल" प्रकार बनाएं और उपभोक्ताओं में एक उदाहरण डालें। स्प्रिंग < aop:scoped-proxy /> टैग 2.x में एक अनुरोध स्कोप बीन परिभाषा के साथ संयुक्त किया गया है, और फैक्ट्री-विधि समर्थन सबसे अधिक पढ़ने योग्य कोड का टिकट हो सकता है।

यह निम्नलिखित की तरह काम कर सकता है:

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
}

अभी तक कुछ भी जटिल नहीं है, है ना? वास्तव में आपको शायद पहले से ही ऐसा करना होगा। इसके बाद, अपने बीन संदर्भ में प्रिंसिपल रखने के लिए एक अनुरोध-स्कोप्ड बीन परिभाषित करें:

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

एओपी के जादू के लिए धन्यवाद: स्कॉप्ड-प्रॉक्सी टैग, स्थैतिक विधि getUserDetails को हर बार एक नया HTTP अनुरोध आने पर बुलाया जाएगा और वर्तमान उपयोगकर्ता संपत्ति के किसी भी संदर्भ को सही ढंग से हल किया जाएगा। अब इकाई परीक्षण तुच्छ हो जाता है:

protected void setUp() {
    // existing init code

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

उम्मीद है की यह मदद करेगा!


काफी काम करने के बाद मैं वांछित व्यवहार को पुन: उत्पन्न करने में सक्षम था। मैंने MockMvc के माध्यम से लॉगिन नकल किया था। अधिकांश यूनिट परीक्षणों के लिए यह बहुत भारी है लेकिन एकीकरण परीक्षणों के लिए सहायक है।

बेशक मैं स्प्रिंग सिक्योरिटी 4.0 में उन नई सुविधाओं को देखने के लिए तैयार हूं जो हमारे परीक्षण को आसान बना देंगे।

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 
}

प्रमाणीकरण ऑब्जेक्ट्स को बनाने और इंजेक्ट करने के बारे में सवाल का जवाब दिए बिना, स्प्रिंग सिक्योरिटी 4.0 परीक्षण के समय कुछ स्वागत विकल्प प्रदान करता है। @WithMockUser एनोटेशन डेवलपर को एक नकली उपयोगकर्ता (वैकल्पिक प्राधिकरण, उपयोगकर्ता नाम, पासवर्ड और भूमिकाओं के साथ) को एक साफ तरीके से निर्दिष्ट करने में सक्षम बनाता है:

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

@WithUserDetails से @WithUserDetails गए @WithUserDetails का अनुकरण करने के लिए @WithUserDetails का उपयोग करने का विकल्प भी है, उदाहरण के लिए

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

स्प्रिंग सिक्योरिटी रेफरेंस डॉक्स में @WithMockUser और @WithUserDetails अध्यायों में अधिक जानकारी मिल सकती है (जिसमें से ऊपर दिए गए उदाहरण कॉपी किए गए हैं)


बस इसे सामान्य तरीका करें और फिर अपने टेस्ट क्लास में SecurityContextHolder.setContext() का उपयोग करके इसे सम्मिलित करें, उदाहरण के लिए:

नियंत्रक:

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

परीक्षा:

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

मैं वसंत के अमूर्त परीक्षण वर्गों और नकली वस्तुओं पर एक नज़र डालेगा जो here बारे में बात की जाती हैं। वे आपकी वसंत प्रबंधित वस्तुओं को इकाई बनाने और एकीकरण परीक्षण को आसान बनाने के लिए एक शक्तिशाली तरीका प्रदान करते हैं।


व्यक्तिगत रूप से मैं सिर्फ पॉकेटमॉक का उपयोग मॉकिटो या ईसिमॉक के साथ अपने यूनिट / एकीकरण परीक्षण में स्थैतिक SecurityContextHolder.getSecurityContext () को नकल करने के लिए करता हूं।

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

माना जाता है कि यहां बॉयलर प्लेट कोड का थोड़ा सा हिस्सा है यानी प्रमाणीकरण ऑब्जेक्ट का नकल करें, प्रमाणीकरण को वापस करने के लिए सुरक्षा कॉन्टेक्स्ट का मज़ाक उड़ाएं और आखिरकार सुरक्षा कॉन्टेक्स्टहोल्डर को सुरक्षा कॉन्टेक्स्ट प्राप्त करने के लिए नकल करें, हालांकि यह बहुत लचीला है और आपको नल प्रमाणीकरण ऑब्जेक्ट जैसे परिदृश्यों के लिए यूनिट परीक्षण करने की अनुमति देता है आदि अपने (गैर परीक्षण) कोड को बदलने के बिना


समस्या यह है कि स्प्रिंग सिक्योरिटी कंटेनर में बीन के रूप में प्रमाणीकरण ऑब्जेक्ट उपलब्ध नहीं कराती है, इसलिए बॉक्स से आसानी से इंजेक्ट या ऑटोवायर करने का कोई तरीका नहीं है।

स्प्रिंग सिक्योरिटी का उपयोग शुरू करने से पहले, हम प्रधानाचार्य को स्टोर करने के लिए कंटेनर में एक सत्र-स्कोप्ड बीन बनाते हैं, इसे "प्रमाणीकरण सेवा" (सिंगलटन) में इंजेक्ट करते हैं और फिर इस बीन को अन्य सेवाओं में इंजेक्ट करते हैं जिन्हें वर्तमान प्रिंसिपल के ज्ञान की आवश्यकता होती है।

यदि आप अपनी खुद की प्रमाणीकरण सेवा को कार्यान्वित कर रहे हैं, तो आप मूल रूप से वही काम कर सकते हैं: "प्रिंसिपल" संपत्ति के साथ सत्र-स्कोप्ड बीन बनाएं, इसे अपनी प्रमाणीकरण सेवा में इंजेक्ट करें, क्या ऑथ सेवा ने सफल ऑथ पर संपत्ति सेट की है, और फिर जैसा कि आपको इसकी आवश्यकता है, अन्य बीन्स के लिए ऑथ सेवा उपलब्ध कराएं।

मैं SecurityContextHolder का उपयोग करने के बारे में बहुत बुरा महसूस नहीं करता। हालांकि। मुझे पता है कि यह एक स्थिर / सिंगलटन है और वसंत ऐसी चीजों का उपयोग करके हतोत्साहित करता है लेकिन उनके कार्यान्वयन पर्यावरण के आधार पर उचित तरीके से व्यवहार करने का ख्याल रखता है: एक सर्वलेट कंटेनर में सत्र-स्कोप्ड, एक जुनीट टेस्ट में थ्रेड-स्कोप्ड आदि। असली सीमित कारक सिंगलटन का यह तब होता है जब यह एक कार्यान्वयन प्रदान करता है जो विभिन्न वातावरणों के लिए लचीला है।







spring-security