วิธีตั้งค่าผู้ใช้ที่พิสูจน์ตัวตนด้วยตนเองใน Spring Security / SpringMVC


108

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

หน้าล็อกอินแบบฟอร์มปกติที่ผ่านตัวสกัดกั้นการรักษาความปลอดภัยแบบสปริงทำงานได้ดี

ในตัวควบคุมรูปแบบบัญชีใหม่ฉันกำลังสร้าง UsernamePasswordAuthenticationToken และตั้งค่าใน SecurityContext ด้วยตนเอง:

SecurityContextHolder.getContext().setAuthentication(authentication);

ในหน้าเดียวกันนั้นฉันตรวจสอบในภายหลังว่าผู้ใช้เข้าสู่ระบบด้วย:

SecurityContextHolder.getContext().getAuthentication().getAuthorities();

สิ่งนี้ส่งคืนหน่วยงานที่ฉันตั้งไว้ก่อนหน้านี้ในการตรวจสอบสิทธิ์ ทั้งหมดเป็นอย่างดี.

แต่เมื่อมีการเรียกรหัสเดียวกันนี้ในหน้าถัดไปที่ฉันโหลดโทเค็นการตรวจสอบความถูกต้องจะเป็นเพียง UserAnonymous

ฉันไม่ชัดเจนว่าเหตุใดจึงไม่เก็บการรับรองความถูกต้องที่ฉันตั้งไว้ในคำขอก่อนหน้านี้ ความคิดใด ๆ ?

  • เป็นไปได้ไหมว่ารหัสเซสชันไม่ได้รับการตั้งค่าอย่างถูกต้อง?
  • มีบางอย่างที่อาจเขียนทับการรับรองความถูกต้องของฉันหรือไม่?
  • บางทีฉันอาจต้องการขั้นตอนอื่นเพื่อบันทึกการรับรองความถูกต้อง?
  • หรือมีบางอย่างที่ฉันต้องทำเพื่อประกาศการรับรองความถูกต้องทั่วทั้งเซสชันแทนที่จะร้องขอเพียงครั้งเดียว

แค่มองหาความคิดบางอย่างที่อาจช่วยให้ฉันเห็นว่าเกิดอะไรขึ้นที่นี่


1
คุณสามารถติดตามคำตอบของฉันได้ที่stackoverflow.com/questions/4824395/…
AlexK

2
SecurityContextHolder.getContext().setAuthentication(authentication)ผู้อ่านระวังคำตอบสำหรับคำถามนี้หากพวกเขาบอกให้คุณทำ: มันใช้งานได้และเป็นเรื่องธรรมดา แต่มีข้อบกพร่องร้ายแรงเกี่ยวกับฟังก์ชันการทำงานที่คุณจะพบหากคุณทำเช่นนั้น สำหรับข้อมูลเพิ่มเติมโปรดดูคำถามของฉันและคำตอบ: stackoverflow.com/questions/47233187/…
แพะ

คำตอบ:


62

ฉันมีปัญหาเดียวกันกับคุณอยู่พักหนึ่ง ฉันจำรายละเอียดไม่ได้ แต่รหัสต่อไปนี้มีประโยชน์สำหรับฉัน รหัสนี้ใช้ภายในโฟลว์ Spring Webflow ดังนั้นคลาส RequestContext และ ExternalContext แต่ส่วนที่เกี่ยวข้องกับคุณมากที่สุดคือวิธี doAutoLogin

public String registerUser(UserRegistrationFormBean userRegistrationFormBean,
                           RequestContext requestContext,
                           ExternalContext externalContext) {

    try {
        Locale userLocale = requestContext.getExternalContext().getLocale();
        this.userService.createNewUser(userRegistrationFormBean, userLocale, Constants.SYSTEM_USER_ID);
        String emailAddress = userRegistrationFormBean.getChooseEmailAddressFormBean().getEmailAddress();
        String password = userRegistrationFormBean.getChoosePasswordFormBean().getPassword();
        doAutoLogin(emailAddress, password, (HttpServletRequest) externalContext.getNativeRequest());
        return "success";

    } catch (EmailAddressNotUniqueException e) {
        MessageResolver messageResolvable 
                = new MessageBuilder().error()
                                      .source(UserRegistrationFormBean.PROPERTYNAME_EMAIL_ADDRESS)
                                      .code("userRegistration.emailAddress.not.unique")
                                      .build();
        requestContext.getMessageContext().addMessage(messageResolvable);
        return "error";
    }

}


private void doAutoLogin(String username, String password, HttpServletRequest request) {

    try {
        // Must be called from request filtered by Spring Security, otherwise SecurityContextHolder is not updated
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
        token.setDetails(new WebAuthenticationDetails(request));
        Authentication authentication = this.authenticationProvider.authenticate(token);
        logger.debug("Logging in with [{}]", authentication.getPrincipal());
        SecurityContextHolder.getContext().setAuthentication(authentication);
    } catch (Exception e) {
        SecurityContextHolder.getContext().setAuthentication(null);
        logger.error("Failure in autoLogin", e);
    }

}

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

4
ใครก็ตามที่ปฏิบัติตามคำแนะนำนี้ควรดูปัญหาที่เกี่ยวข้องนี้ด้วย: stackoverflow.com/questions/4824395/…
David Parks

14
คุณช่วยอธิบายได้ไหมว่าคุณรับการพิสูจน์ตัวตน
Provider ได้อย่างไร

1
@ s1moner3d คุณควรจะฉีดผ่าน IoC -> \ @Autowired
Hartmut

1
@Configuration public class WebConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationProvider() throws Exception { return super.authenticationManagerBean(); } }
slisnychyi

67

ฉันไม่พบโซลูชันอื่น ๆ ทั้งหมดดังนั้นฉันจึงคิดว่าจะโพสต์ของฉัน นี่อาจเป็นการแฮ็กเล็กน้อย แต่สามารถแก้ไขปัญหาดังกล่าวได้:

public void login(HttpServletRequest request, String userName, String password)
{

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, password);

    // Authenticate the user
    Authentication authentication = authenticationManager.authenticate(authRequest);
    SecurityContext securityContext = SecurityContextHolder.getContext();
    securityContext.setAuthentication(authentication);

    // Create a new session and add the security context.
    HttpSession session = request.getSession(true);
    session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
}

7
+1 - สิ่งนี้ช่วยฉันได้! ฉันไม่มีการอัปเดต SPRING_SECURITY_CONTEXT ... แต่นี่มัน "สกปรก" แค่ไหน?
l3dx

13
คุณได้รับauthenticationManagerจากที่ไหน?
Isaac

2
authenticationManager เป็นระบบอัตโนมัติในคลาสของคุณเช่นนี้ @Autowired AuthenticationServiceImpl authenticationManager และต้องมีการฉีดถั่วในการกำหนดค่า xml ของคุณด้วยดังนั้น Spring จึงรู้ว่าต้องฉีดอะไร

1
การใช้งาน AuthenticationServiceImpl อยู่ที่ไหน คลาสนี้เก็บอะไร?
Pra_A

3
เหตุใดจึงต้องสร้างเซสชันใหม่ SecurityContext จัดการไม่ได้หรือ
Vlad Manuel Mureșan

17

ในที่สุดก็พบต้นตอของปัญหา

เมื่อฉันสร้างบริบทความปลอดภัยด้วยตนเองจะไม่มีการสร้างวัตถุเซสชัน เฉพาะเมื่อการร้องขอเสร็จสิ้นการประมวลผลกลไก Spring Security จะตระหนักว่าวัตถุเซสชันเป็นโมฆะ (เมื่อพยายามจัดเก็บบริบทการรักษาความปลอดภัยไปยังเซสชันหลังจากประมวลผลคำขอแล้ว)

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


4
จริงๆแล้วสิ่งนี้ให้ความรู้สึกเหมือนข้อบกพร่องด้านการออกแบบในระบบความปลอดภัยของสปริงมากกว่าสิ่งใด ๆ มีเฟรมเวิร์กมากมายที่เขียนในภาษาอื่น ๆ ที่จะไม่มีปัญหากับสิ่งนี้ แต่ Spring Security เพิ่งหยุดทำงาน
chubbsondubs

3
และทางออกคือ?
s1moner3d

2
แล้วทางแก้คืออะไร?
Thiago

6

เปิดการบันทึกการแก้ไขข้อบกพร่องเพื่อให้ได้ภาพที่ดีขึ้นว่าเกิดอะไรขึ้น

คุณสามารถทราบได้ว่ามีการตั้งค่าคุกกี้เซสชันหรือไม่โดยใช้ดีบักเกอร์ฝั่งเบราว์เซอร์เพื่อดูส่วนหัวที่ส่งคืนในการตอบกลับ HTTP (มีวิธีอื่นด้วย)

ความเป็นไปได้อย่างหนึ่งคือ SpringSecurity กำลังตั้งค่าคุกกี้เซสชันที่ปลอดภัยและหน้าถัดไปของคุณที่ร้องขอมี URL "http" แทนที่จะเป็น URL "https" (เบราว์เซอร์จะไม่ส่งคุกกี้ที่ปลอดภัยสำหรับ URL "http")


ขอบคุณคำแนะนำที่เป็นประโยชน์และเกี่ยวข้องทั้งหมด!
David Parks

5

คุณสมบัติการกรองใหม่ใน Servlet 2.4 โดยพื้นฐานแล้วจะช่วยลดข้อ จำกัด ที่ตัวกรองสามารถทำงานได้เฉพาะในขั้นตอนการร้องขอก่อนและหลังการประมวลผลคำขอจริงโดยแอปพลิเคชันเซิร์ฟเวอร์ ขณะนี้ตัวกรอง Servlet 2.4 สามารถโต้ตอบกับผู้จัดส่งคำขอได้ในทุกจุดจัดส่ง ซึ่งหมายความว่าเมื่อทรัพยากรบนเว็บส่งต่อคำร้องขอไปยังทรัพยากรอื่น (ตัวอย่างเช่น servlet ส่งต่อคำร้องขอไปยังเพจ JSP ในแอ็พพลิเคชันเดียวกัน) ตัวกรองสามารถทำงานได้ก่อนที่จะจัดการคำขอโดยทรัพยากรเป้าหมาย นอกจากนี้ยังหมายความว่าทรัพยากรบนเว็บควรมีเอาต์พุตหรือฟังก์ชันจากทรัพยากรบนเว็บอื่น ๆ (ตัวอย่างเช่นเพจ JSP รวมเอาต์พุตจากเพจ JSP อื่น ๆ ) ตัวกรอง Servlet 2.4 สามารถทำงานก่อนและหลังทรัพยากรที่รวมอยู่ .

ในการเปิดคุณสมบัตินั้นคุณต้อง:

web.xml

<filter>   
    <filter-name>springSecurityFilterChain</filter-name>   
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
</filter>  
<filter-mapping>   
    <filter-name>springSecurityFilterChain</filter-name>   
    <url-pattern>/<strike>*</strike></url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>

ตัวควบคุมการลงทะเบียน

return "forward:/login?j_username=" + registrationModel.getUserEmail()
        + "&j_password=" + registrationModel.getPassword();

ข้อมูลที่ดี แต่การใส่ชื่อผู้ใช้และรหัสผ่านใน url นั้นไม่ดี 1) ไม่มีการหลบหนีดังนั้นชื่อผู้ใช้หรือรหัสผ่านที่มีอักขระพิเศษมีแนวโน้มที่จะแตกหรือแย่กว่านั้นคือถูกใช้เป็นเวกเตอร์ช่องโหว่ด้านความปลอดภัย 2) รหัสผ่านใน URL ไม่ถูกต้องเนื่องจาก URL มักจะถูกบันทึกลงในดิสก์ซึ่งค่อนข้างไม่ดีต่อความปลอดภัย - รหัสผ่านทั้งหมดของคุณเป็นข้อความธรรมดาเพียงแค่นั่งอยู่ที่นั่น
แพะ

1

ฉันพยายามทดสอบแอปพลิเคชัน extjs และหลังจากตั้งค่า testingAuthenticationToken สำเร็จสิ่งนี้ก็หยุดทำงานโดยไม่มีสาเหตุที่ชัดเจน

ฉันไม่สามารถหาคำตอบข้างต้นเพื่อใช้งานได้ดังนั้นวิธีแก้ปัญหาของฉันคือข้ามสปริงนี้ไปในสภาพแวดล้อมการทดสอบ ฉันแนะนำตะเข็บรอบฤดูใบไม้ผลิเช่นนี้:

public class SpringUserAccessor implements UserAccessor
{
    @Override
    public User getUser()
    {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        return (User) authentication.getPrincipal();
    }
}

ผู้ใช้เป็นประเภทกำหนดเองที่นี่

จากนั้นฉันจะห่อมันในชั้นเรียนซึ่งมีตัวเลือกสำหรับรหัสทดสอบเพื่อเปลี่ยนฤดูใบไม้ผลิ

public class CurrentUserAccessor
{
    private static UserAccessor _accessor;

    public CurrentUserAccessor()
    {
        _accessor = new SpringUserAccessor();
    }

    public User getUser()
    {
        return _accessor.getUser();
    }

    public static void UseTestingAccessor(User user)
    {
        _accessor = new TestUserAccessor(user);
    }
}

เวอร์ชันทดสอบมีลักษณะดังนี้:

public class TestUserAccessor implements UserAccessor
{
    private static User _user;

    public TestUserAccessor(User user)
    {
        _user = user;
    }

    @Override
    public User getUser()
    {
        return _user;
    }
}

ในรหัสการโทรฉันยังคงใช้ผู้ใช้ที่เหมาะสมที่โหลดจากฐานข้อมูล:

    User user = (User) _userService.loadUserByUsername(username);
    CurrentUserAccessor.UseTestingAccessor(user);

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

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