การทดสอบหน่วยด้วย Spring Security


140

บริษัท ของฉันกำลังประเมิน Spring MVC เพื่อพิจารณาว่าเราควรใช้ในโครงการต่อไปของเราหรือไม่ จนถึงตอนนี้ฉันชอบสิ่งที่ฉันเห็นและตอนนี้ฉันกำลังดูโมดูลความปลอดภัยของสปริงเพื่อตรวจสอบว่าเป็นสิ่งที่เราสามารถ / ควรใช้

ข้อกำหนดด้านความปลอดภัยของเราค่อนข้างพื้นฐาน ผู้ใช้เพียงแค่ต้องสามารถให้ชื่อผู้ใช้และรหัสผ่านเพื่อให้สามารถเข้าถึงบางส่วนของเว็บไซต์ (เช่นเพื่อรับข้อมูลเกี่ยวกับบัญชีของพวกเขา); และมีหน้าเว็บจำนวนหนึ่งบนไซต์ (คำถามที่พบบ่อยการสนับสนุน ฯลฯ ) ซึ่งผู้ใช้ที่ไม่ระบุชื่อควรได้รับสิทธิ์เข้าถึง

ในต้นแบบที่ฉันสร้างฉันได้จัดเก็บวัตถุ "LoginCredentials" (ซึ่งเพิ่งมีชื่อผู้ใช้และรหัสผ่าน) ในเซสชันสำหรับผู้ใช้ที่ได้รับการรับรองความถูกต้อง คอนโทรลเลอร์บางตัวตรวจสอบว่าวัตถุนี้อยู่ในเซสชันเพื่อรับการอ้างอิงถึงชื่อผู้ใช้ที่เข้าสู่ระบบหรือไม่ ฉันต้องการแทนที่ตรรกะที่พัฒนาขึ้นเองด้วย Spring Security แทนซึ่งจะมีประโยชน์อย่างมากในการลบ "เราจะติดตามผู้ใช้ที่เข้าสู่ระบบได้อย่างไร" และ "เราจะตรวจสอบผู้ใช้อย่างไร" จากตัวควบคุม / รหัสธุรกิจของฉัน

ดูเหมือนว่า Spring Security จะมีวัตถุ "บริบท" เพื่อให้สามารถเข้าถึงชื่อผู้ใช้ / ข้อมูลหลักจากที่ใดก็ได้ในแอปของคุณ ...

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

... ซึ่งดูเหมือนว่าจะไม่มีสปริงเหมือนวัตถุนี้เป็นซิงเกิล (ทั่วโลก) ในทางใดทางหนึ่ง

คำถามของฉันคือ: หากนี่เป็นวิธีมาตรฐานในการเข้าถึงข้อมูลเกี่ยวกับผู้ใช้ที่ได้รับการรับรองความถูกต้องใน Spring Security วิธีที่ยอมรับในการฉีดออบเจ็กต์การรับรองความถูกต้องลงใน SecurityContext เพื่อให้สามารถทดสอบหน่วยของฉันได้ ผู้ใช้รับรองความถูกต้อง?

ฉันต้องต่อสายนี้ในวิธีการเริ่มต้นของแต่ละกรณีทดสอบหรือไม่?

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

ดูเหมือนว่า verbose มากเกินไป มีวิธีที่ง่ายกว่านี้ไหม?

SecurityContextHolderวัตถุตัวเองดูเหมือนยกเลิกฤดูใบไม้ผลิเหมือนมาก ...

คำตอบ:


48

ปัญหาคือ Spring Security ไม่ได้ทำให้วัตถุการรับรองความถูกต้องเป็นถั่วในภาชนะดังนั้นจึงไม่มีวิธีที่จะฉีดหรือออกจากกล่องอัตโนมัติ

ก่อนที่เราจะเริ่มใช้ Spring Security เราจะสร้าง bean ที่ถูกกำหนดขอบเขตเซสชันไว้ในคอนเทนเนอร์เพื่อเก็บเงินต้นแล้วอัดมันเข้าไปใน "AuthenticationService" (singleton) แล้วฉีดถั่วนี้ไปยังบริการอื่น ๆ ที่ต้องการความรู้เกี่ยวกับหลักการปัจจุบัน

หากคุณกำลังใช้บริการการตรวจสอบความถูกต้องของคุณเองคุณสามารถทำสิ่งเดียวกันโดยทั่วไป: สร้างถั่วที่มีการกำหนดขอบเขตด้วยคุณสมบัติ "ตัวหลัก" แทรกสิ่งนี้ลงในบริการการตรวจสอบความถูกต้องของคุณให้บริการรับรองความถูกต้อง ทำให้บริการ auth พร้อมใช้งานสำหรับถั่วอื่น ๆ ตามที่คุณต้องการ

ฉันจะไม่รู้สึกแย่เกี่ยวกับการใช้ SecurityContextHolder แม้ ฉันรู้ว่ามันเป็นแบบสแตติก / ซิงเกิลตันและฤดูใบไม้ผลิไม่สนับสนุนการใช้สิ่งต่าง ๆ แต่การใช้งานของพวกเขาจะทำหน้าที่อย่างเหมาะสมขึ้นอยู่กับสภาพแวดล้อม: เซสชัน - ขอบเขตในภาชนะ Servlet, เธรดที่กำหนดขอบเขตในการทดสอบ JUnit เป็นต้น ของ Singleton คือเมื่อมันให้การปรับใช้ที่ไม่ยืดหยุ่นกับสภาพแวดล้อมที่แตกต่างกัน


ขอบคุณนี่เป็นคำแนะนำที่มีประโยชน์ สิ่งที่ฉันได้ทำไปนั้นเป็นพื้นเพื่อดำเนินการโทร SecurityContextHolder.getContext () (ผ่านวิธี wrapper ไม่กี่วิธีของตัวเองอย่างน้อยก็อย่างน้อยก็เรียกจากชั้นหนึ่ง)
matt b

2
แม้ว่าจะมีเพียงหนึ่งโน้ต - ฉันไม่คิดว่า ServletContextHolder มีแนวคิดใด ๆ ของ HttpSession หรือวิธีการที่รู้ว่ามันทำงานอยู่ในสภาพแวดล้อมของเว็บเซิร์ฟเวอร์หรือไม่ - มันใช้ ThreadLocal เว้นแต่ว่าคุณจะกำหนดค่าให้ใช้อย่างอื่น และทั่วโลก)
matt b

ข้อเสียเปรียบเพียงอย่างเดียวในการใช้ session / request-scoped beans ใน Spring คือว่าจะล้มเหลวในการทดสอบ JUnit สิ่งที่คุณสามารถทำได้คือใช้ขอบเขตที่กำหนดเองที่จะใช้เซสชัน / คำขอหากมีและจำเป็นต้องกลับไปใช้เธรด ฉันเดาว่าฤดูใบไม้ผลิการรักษาความปลอดภัยที่จะทำบางสิ่งบางอย่างที่คล้ายกัน ...
cliff.meyers

เป้าหมายของฉันคือการสร้างส่วนที่เหลือโดยไม่ต้องประชุม บางทีด้วยโทเค็นที่รีเฟรชได้ ขณะนี้ไม่ได้ตอบคำถามของฉันมันช่วย ขอบคุณ
Pomagranite

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
@ Leonardo ที่ควรAuthentication aเพิ่มสิ่งนี้ในตัวควบคุม? ที่ฉันสามารถเข้าใจในแต่ละวิธีการภาวนา? มันเป็นเรื่องที่ดีสำหรับ "วิธีการสปริง" เพียงเพื่อเพิ่มแทนที่จะฉีด?
Oleg Kuts

แต่จำไว้ว่ามันจะไม่ทำงานกับ TestNG เพราะ SecurityContextHolder เก็บไว้ที่เธรดตัวแปรดังนั้นคุณจึงแชร์ตัวแปรนี้ระหว่างการทดสอบ ...
ŁukaszWoźniczka

ทำใน@BeforeEach(JUnit5) หรือ@Before(JUnit 4) ดีและเรียบง่าย
WesternGun

30

โดยไม่ต้องตอบคำถามเกี่ยวกับวิธีการสร้างและฉีดออบเจ็กต์การรับรองความถูกต้อง Spring Security 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บทในเอกสารอ้างอิง Spring Security (ซึ่งคัดลอกตัวอย่างด้านบน)


29

คุณค่อนข้างถูกต้องที่จะต้องกังวล - การเรียกใช้เมธอดแบบสแตติกเป็นปัญหาอย่างยิ่งสำหรับการทดสอบหน่วยเนื่องจากคุณไม่สามารถเลียนแบบการพึ่งพาของคุณได้อย่างง่ายดาย สิ่งที่ฉันจะแสดงให้คุณเห็นคือวิธีปล่อยภาชนะสปริง IoC ทำงานสกปรกให้คุณทิ้งโค้ดที่เรียบร้อยและทดสอบได้ให้คุณ SecurityContextHolder เป็นคลาสเฟรมเวิร์กและในขณะที่มันอาจจะโอเคสำหรับรหัสความปลอดภัยระดับต่ำของคุณที่จะเชื่อมโยงกับมันคุณอาจต้องการที่จะเปิดเผยอินเตอร์เฟซแบบ neater กับองค์ประกอบ UI ของคุณ (เช่นตัวควบคุม)

cliff.meyers พูดถึงวิธีหนึ่งในนั้น - สร้างประเภท "อาจารย์ใหญ่" ของคุณเองและฉีดอินสแตนซ์เข้าสู่ผู้บริโภค แท็กSpring < 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 ของคุณให้กำหนดถั่วที่ร้องขอตามขอบเขตเพื่อเก็บหลักการ:

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

ด้วยความมหัศจรรย์ของแท็ก aop: scoped-proxy วิธีการคงที่ getUserDetails จะถูกเรียกทุกครั้งที่มีการร้องขอ HTTP ใหม่เข้ามาและการอ้างอิงใด ๆ กับคุณสมบัติ currentUser จะได้รับการแก้ไขอย่างถูกต้อง ตอนนี้การทดสอบหน่วยกลายเป็นเรื่องเล็กน้อย:

protected void setUp() {
    // existing init code

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

หวังว่านี่จะช่วยได้!


9

โดยส่วนตัวแล้วฉันจะใช้ Powermock ร่วมกับ Mockito หรือ Easymock เพื่อเยาะเย้ย 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

การใช้สแตติกในกรณีนี้เป็นวิธีที่ดีที่สุดในการเขียนรหัสที่ปลอดภัย

ใช่สถิตยศาสตร์มักไม่ดี - โดยทั่วไป แต่ในกรณีนี้สเตติกคือสิ่งที่คุณต้องการ เนื่องจากบริบทความปลอดภัยจะเชื่อมโยงหลักการกับเธรดที่รันอยู่ในปัจจุบันรหัสที่ปลอดภัยที่สุดจะเข้าถึงสแตติกจากเธรดให้ได้โดยตรงที่สุด การซ่อนการเข้าถึงด้านหลังคลาส wrapper ที่ถูกฉีดจะช่วยให้ผู้โจมตีมีคะแนนมากขึ้นในการโจมตี พวกเขาไม่ต้องการการเข้าถึงโค้ด (ซึ่งพวกเขาจะมีการเปลี่ยนแปลงยากหากลงนาม jar) พวกเขาต้องการวิธีที่จะแทนที่การกำหนดค่าซึ่งสามารถทำได้ในขณะรันไทม์หรือวาง XML ลงบน classpath แม้กระทั่งการใช้การฉีดหมายเหตุประกอบจะสามารถ overridable กับ XML ภายนอก XML ดังกล่าวสามารถฉีดระบบที่กำลังรันด้วยหลักการโกง


4

ฉันถามคำถามเดียวกันกับตัวเองที่นี่และเพิ่งโพสต์คำตอบที่ฉันเพิ่งพบ คำตอบสั้น ๆ คือ: ฉีด a SecurityContextและอ้างถึงSecurityContextHolderเฉพาะในการกำหนดค่าสปริงของคุณเพื่อรับSecurityContext


3

ทั่วไป

ในระหว่างนี้ (ตั้งแต่รุ่น 3.2 ในปี 2013 ขอบคุณSEC-2298 ) การตรวจสอบสิทธิ์สามารถถูกฉีดเข้าไปในวิธีการ MVC โดยใช้คำอธิบายประกอบ@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 บรรจุก่อนที่วิธีการทดสอบจะทำงาน - เพียงแค่รู้สึกสกปรกที่ต้องตั้งค่าใน ThreadLocal ก่อน
matt b

1

การตรวจสอบความถูกต้องเป็นคุณสมบัติของเธรดในสภาพแวดล้อมเซิร์ฟเวอร์ในลักษณะเดียวกับที่เป็นคุณสมบัติของกระบวนการในระบบปฏิบัติการ การมีอินสแตนซ์ bean สำหรับการเข้าถึงข้อมูลการรับรองความถูกต้องจะเป็นการกำหนดค่าที่ไม่สะดวกและค่าใช้จ่ายในการเดินสายโดยไม่มีประโยชน์

เกี่ยวกับการรับรองความถูกต้องของการทดสอบมีหลายวิธีที่คุณสามารถทำให้ชีวิตของคุณง่ายขึ้น สิ่งที่ฉันชอบคือการสร้างคำอธิบายประกอบที่กำหนดเอง@Authenticatedและทดสอบการฟังการดำเนินการซึ่งจัดการมัน ตรวจสอบDirtiesContextTestExecutionListenerแรงบันดาลใจ


0

หลังจากทำงานค่อนข้างมากฉันก็สามารถทำซ้ำพฤติกรรมที่ต้องการ ฉันจำลองการเข้าสู่ระบบผ่าน MockMvc มันหนักเกินไปสำหรับการทดสอบหน่วยส่วนใหญ่ แต่มีประโยชน์สำหรับการทดสอบการรวม

แน่นอนฉันยินดีที่จะเห็นคุณสมบัติใหม่เหล่านั้นใน Spring Security 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 
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.