स्प्रिंग सुरक्षा के साथ यूनिट परीक्षण


140

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

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

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

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

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

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

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

क्या मुझे प्रत्येक परीक्षण मामले के प्रारंभ विधि में इसे तार करने की आवश्यकता है?

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

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

SecurityContextHolderवस्तु ही बहुत अन-वसंत-तरह लगता है ...

जवाबों:


48

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

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

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

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


धन्यवाद, यह उपयोगी सलाह है। मैंने अब तक जो भी किया है वह मूल रूप से SecurityContextHolder.getContext () के साथ आगे बढ़ने के लिए है (मेरे खुद के कुछ आवरण विधियों के माध्यम से, इसलिए कम से कम इसे केवल एक वर्ग से कहा जाता है)।
मैट बी

2
हालांकि सिर्फ एक नोट - मुझे नहीं लगता है कि ServletContextHolder में HttpSession या किसी वेब सर्वर के वातावरण में काम करने के तरीके को जानने का कोई तरीका है - यह ThreadLocal का उपयोग करता है जब तक कि आप इसे किसी और चीज का उपयोग करने के लिए कॉन्फ़िगर नहीं करते हैं (केवल दो दो अंतर्निहित मोड InheritableThreadLocal हैं और ग्लोबल)
मैट बी

स्प्रिंग में सत्र / अनुरोध-स्कॉप्ड बीन्स का उपयोग करने का एकमात्र दोष यह है कि वे एक जेयूनिट परीक्षा में असफल हो जाएंगे। आप जो कर सकते हैं वह एक कस्टम स्कोप लागू करना है जो सत्र / अनुरोध का उपयोग करेगा यदि उपलब्ध हो और थ्रेड पर वापस आना आवश्यक हो। मेरा अनुमान है कि स्प्रिंग सिक्योरिटी कुछ ऐसा ही कर रही है ...
cliff.meyers

मेरा लक्ष्य सत्रों के बिना रेस्ट एपी का निर्माण करना है। शायद एक ताज़ा टोकन के साथ। जबकि इसने मेरे सवाल का जवाब नहीं दिया इससे मदद मिली। धन्यवाद
पोमग्रैनाइट

166

बस इसे सामान्य तरीके से करें और फिर इसे 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);

2
@ लियोनार्डो यह Authentication aनियंत्रक में कहां जोड़ा जाना चाहिए ? जैसा कि मैं प्रत्येक विधि आह्वान में समझ सकता हूं? क्या इंजेक्शन लगाने के बजाय इसे जोड़ना "स्प्रिंग वे" के लिए ठीक है?
ओलेग कुट्स

लेकिन याद रखें कि TestNG के साथ काम नहीं किया जा रहा है क्योंकि SecurityContextHolder स्थानीय थ्रेड वैरिएबल को पकड़ता है, इसलिए आप इस वैरिएबल को परीक्षणों के बीच साझा करते हैं ...
zukasz Woźniczka

इसे @BeforeEach(JUnit5) या @Before(JUnit 4) में करें। अच्छा और सरल।
पश्चिमीगुन

30

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

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

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

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

अधिक जानकारी @WithMockUser और @WithUserDetails अध्यायों में स्प्रिंग सुरक्षा संदर्भ डॉक्स (जिसमें से उपरोक्त उदाहरणों की प्रतिलिपि बनाई गई थी) में मिल सकती है।


29

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

cliff.meyers ने इसके चारों ओर एक तरह से उल्लेख किया - अपना स्वयं का "प्रिंसिपल" प्रकार बनाएं और उपभोक्ताओं में एक उदाहरण इंजेक्ट करें। स्प्रिंग < aop: scoped -xy /> टैग को 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 अनुरोध आने पर कहा जाएगा और currentUser संपत्ति के किसी भी संदर्भ को सही ढंग से हल किया जाएगा। अब इकाई परीक्षण तुच्छ हो जाता है:

protected void setUp() {
    // existing init code

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

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


9

व्यक्तिगत रूप से मैं सिर्फ आपकी यूनिट / एकीकरण परीक्षण जैसे स्थैतिक 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);
        ...
    }
}

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


7

इस मामले में एक स्टेटिक का उपयोग करना सुरक्षित कोड लिखने का सबसे अच्छा तरीका है।

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


4

मैंने खुद से यहाँ वही सवाल पूछा , और बस एक जवाब पोस्ट किया जो मुझे हाल ही में मिला। संक्षिप्त उत्तर है: एक इंजेक्षन SecurityContext, और SecurityContextHolderकेवल अपने स्प्रिंग कॉन्फिगर में देखेंSecurityContext


3

सामान्य

इस बीच, (संस्करण 3.2 के बाद से, वर्ष 2013 में, एसईसी -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)));

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

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

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


जबकि वे परीक्षण कक्षाएं सहायक हैं, मुझे यकीन नहीं है कि वे यहां आवेदन करेंगे। मेरे परीक्षणों में ApplicationContext की कोई अवधारणा नहीं है - उन्हें एक की आवश्यकता नहीं है। मुझे यह सुनिश्चित करने की ज़रूरत है कि SecurityContext परीक्षण विधि के चलने से पहले ही आबाद हो जाए - इसे थ्रेडलोक में पहले सेट करने के लिए गंदा महसूस होता है
मैट b

1

प्रमाणीकरण सर्वर वातावरण में उसी तरह से थ्रेड की एक संपत्ति है जिस तरह से यह ओएस में एक प्रक्रिया की संपत्ति है। प्रमाणीकरण जानकारी तक पहुंचने के लिए सेम उदाहरण होने से असुविधाजनक कॉन्फ़िगरेशन और ओवरहेड वायरिंग बिना किसी लाभ के होगा।

परीक्षण प्रमाणीकरण के बारे में कई तरीके हैं जिनसे आप अपने जीवन को आसान बना सकते हैं। मेरा पसंदीदा एक कस्टम एनोटेशन @Authenticatedऔर परीक्षण निष्पादन श्रोता बनाना है, जो इसे प्रबंधित करता है। DirtiesContextTestExecutionListenerप्रेरणा के लिए जाँच करें ।


0

काफी काम के बाद मैं वांछित व्यवहार को पुन: पेश करने में सक्षम था। मैंने 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 
}
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.