Cách đặt thủ công người dùng được xác thực trong Spring Security / SpringMVC


107

Sau khi người dùng mới gửi biểu mẫu 'Tài khoản mới', tôi muốn đăng nhập thủ công người dùng đó để họ không phải đăng nhập trên trang tiếp theo.

Trang đăng nhập biểu mẫu bình thường thông qua bộ chặn bảo mật mùa xuân hoạt động tốt.

Trong bộ điều khiển biểu mẫu tài khoản mới, tôi đang tạo UsernamePasswordAuthenticationToken và đặt nó trong SecurityContext theo cách thủ công:

SecurityContextHolder.getContext().setAuthentication(authentication);

Trên cùng trang đó, sau đó, tôi kiểm tra xem người dùng đã đăng nhập bằng:

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

Điều này trả về các cơ quan mà tôi đã đặt trước đó trong xác thực. Tất cả đều tốt.

Nhưng khi mã tương tự này được gọi trên trang tiếp theo mà tôi tải, mã xác thực chỉ là UserAnonymous.

Tôi không rõ tại sao nó không giữ xác thực mà tôi đã đặt trong yêu cầu trước đó. Có suy nghĩ gì không?

  • Nó có thể liên quan đến phiên ID không được thiết lập đúng cách?
  • Có điều gì đó có thể đang ghi đè xác thực của tôi bằng cách nào đó không?
  • Có lẽ tôi chỉ cần một bước nữa để lưu xác thực?
  • Hoặc có điều gì đó tôi cần làm để khai báo xác thực trong toàn bộ phiên thay vì một yêu cầu duy nhất bằng cách nào đó?

Tôi chỉ đang tìm kiếm một số suy nghĩ có thể giúp tôi thấy những gì đang xảy ra ở đây.


1
Bạn có thể làm theo câu trả lời của tôi cho stackoverflow.com/questions/4824395/…
AlexK,

2
Độc giả, hãy cẩn thận các câu trả lời cho câu hỏi này nếu họ cho bạn biết phải làm: SecurityContextHolder.getContext().setAuthentication(authentication). Nó hoạt động và phổ biến, nhưng có những thiếu sót nghiêm trọng về chức năng mà bạn sẽ gặp nếu chỉ làm như vậy. Để biết thêm thông tin, hãy xem câu hỏi của tôi và câu trả lời: stackoverflow.com/questions/47233187/…

Câu trả lời:


62

Tôi đã gặp vấn đề tương tự như bạn một thời gian trước. Tôi không thể nhớ chi tiết nhưng đoạn mã sau đây có tác dụng với tôi. Mã này được sử dụng trong luồng Spring Webflow, do đó có các lớp RequestContext và ExternalContext. Nhưng phần phù hợp nhất với bạn là phương thức 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
Cảm ơn bạn, mã này rất hữu ích trong việc giúp tôi biết rằng tôi đang khắc phục sự cố ở đúng khu vực. Có vẻ như tôi có một khẩu súng hút thuốc, nó đang tạo một id phiên mới sau khi xác thực thủ công, nhưng id phiên cũ vẫn đang được xác định từ cookie. Tôi đã tìm ra lý do tại sao bây giờ, nhưng ít nhất tôi rõ ràng đang đi đúng hướng. Cảm ơn!
David Parks

4
Bất kỳ ai làm theo hướng dẫn này cũng sẽ thấy vấn đề liên quan này: stackoverflow.com/questions/4824395/…
David Parks,

14
Bạn có thể vui lòng giải thích rằng cách bạn đang nhận được xác
thựcProvider

1
@ s1moner3d bạn sẽ có thể để có được nó tiêm qua IoC -> \ @Autowired
Hartmut

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

66

Tôi không thể tìm thấy bất kỳ giải pháp đầy đủ nào khác nên tôi nghĩ tôi sẽ đăng bài của mình. Đây có thể là một chút hack, nhưng nó đã giải quyết được vấn đề cho vấn đề trên:

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 - Điều này đã giúp tôi! Tôi thiếu bản cập nhật SPRING_SECURITY_CONTEXT. ... Nhưng "bẩn" đến mức nào đây?
l3dx

12
bạn lấy authenticationManagertừ đâu?
Isaac

2
verifyManager được tự động hóa trong lớp của bạn như thế này @Autowosystem AuthenticationServiceImpl authenticManager. Và cũng phải có một tiêm bean trong cấu hình xml của bạn, vì vậy Spring biết những gì cần tiêm.

1
việc triển khai AuthenticationServiceImpl ở đâu? Lớp này nắm giữ những gì?
Pra_A

3
Tại sao cần tạo một phiên mới? SecurityContext không xử lý được điều đó?
Vlad Manuel Mureșan,

17

Cuối cùng đã tìm ra gốc rễ của vấn đề.

Khi tôi tạo ngữ cảnh bảo mật theo cách thủ công, không có đối tượng phiên nào được tạo. Chỉ khi yêu cầu kết thúc quá trình xử lý thì cơ chế Spring Security mới nhận ra rằng đối tượng phiên là rỗng (khi nó cố gắng lưu trữ ngữ cảnh bảo mật vào phiên sau khi yêu cầu đã được xử lý).

Khi kết thúc yêu cầu, Spring Security tạo một đối tượng phiên mới và ID phiên. Tuy nhiên, ID phiên mới này không bao giờ xuất hiện trên trình duyệt vì nó xuất hiện ở cuối yêu cầu, sau khi phản hồi cho trình duyệt được thực hiện. Điều này khiến ID phiên mới (và do đó, ngữ cảnh Bảo mật chứa người dùng đã đăng nhập thủ công của tôi) bị mất khi yêu cầu tiếp theo chứa ID phiên trước đó.


4
Thành thật mà nói, điều này giống như một lỗ hổng thiết kế trong bảo mật mùa xuân hơn bất cứ điều gì. Có rất nhiều khuôn khổ được viết bằng các ngôn ngữ khác sẽ không có vấn đề gì với điều này, nhưng Spring Security chỉ bị hỏng.
chubbsondubs 11/12/12

3
và giải pháp là?
s1moner3d

2
và giải pháp là gì?
Thiago

6

Bật ghi nhật ký gỡ lỗi để có hình ảnh rõ hơn về những gì đang diễn ra.

Bạn có thể biết liệu cookie phiên có đang được đặt hay không bằng cách sử dụng trình gỡ lỗi phía trình duyệt để xem các tiêu đề được trả về trong phản hồi HTTP. (Có nhiều cách khác nữa.)

Một khả năng là SpringSecurity đang đặt cookie phiên bảo mật và trang tiếp theo của bạn được yêu cầu có URL "http" thay vì URL "https". (Trình duyệt sẽ không gửi cookie an toàn cho URL "http".)


Cảm ơn đây là tất cả các đề xuất rất hữu ích và có liên quan!
David Parks

5

Tính năng lọc mới trong Servlet 2.4 về cơ bản giảm bớt hạn chế rằng các bộ lọc chỉ có thể hoạt động trong luồng yêu cầu trước và sau khi máy chủ ứng dụng xử lý yêu cầu thực tế. Thay vào đó, các bộ lọc Servlet 2.4 giờ đây có thể tương tác với bộ điều phối yêu cầu tại mọi điểm gửi. Điều này có nghĩa là khi một tài nguyên Web chuyển tiếp một yêu cầu đến một tài nguyên khác (ví dụ, một servlet chuyển tiếp yêu cầu đến một trang JSP trong cùng một ứng dụng), một bộ lọc có thể hoạt động trước khi yêu cầu được xử lý bởi tài nguyên được nhắm mục tiêu. Điều đó cũng có nghĩa là nếu tài nguyên Web bao gồm đầu ra hoặc chức năng từ các tài nguyên Web khác (ví dụ: một trang JSP bao gồm đầu ra từ nhiều trang JSP khác), thì bộ lọc Servlet 2.4 có thể hoạt động trước và sau mỗi tài nguyên được bao gồm. .

Để bật tính năng đó, bạn cần:

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>

RegisterController

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

Thông tin tốt, nhưng đặt tên người dùng và mật khẩu vào url là không tốt. 1) không có quá trình thoát nào được thực hiện, vì vậy tên người dùng hoặc mật khẩu có ký tự đặc biệt có khả năng bị phá vỡ, hoặc thậm chí tệ hơn, được sử dụng làm véc tơ khai thác bảo mật. 2) mật khẩu trong url không tốt vì url thường được ghi vào đĩa, điều này khá tệ về bảo mật - tất cả mật khẩu của bạn ở dạng bản rõ chỉ nằm ở đó.

1

Tôi đang cố gắng kiểm tra một ứng dụng extjs và sau khi thiết lập testingAuthenticationToken thành công, ứng dụng này đột nhiên ngừng hoạt động mà không rõ nguyên nhân.

Tôi không thể làm cho các câu trả lời trên hoạt động vì vậy giải pháp của tôi là bỏ qua đoạn xuân này trong môi trường thử nghiệm. Tôi đã giới thiệu một đường may xung quanh mùa xuân như thế này:

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

Người dùng là một loại tùy chỉnh ở đây.

Sau đó, tôi gói nó trong một lớp chỉ có tùy chọn cho mã kiểm tra để chuyển sang mùa xuân.

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

Phiên bản thử nghiệm trông giống như sau:

public class TestUserAccessor implements UserAccessor
{
    private static User _user;

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

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

Trong mã gọi điện, tôi vẫn đang sử dụng một người dùng thích hợp được tải từ cơ sở dữ liệu:

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

Rõ ràng điều này sẽ không phù hợp nếu bạn thực sự cần sử dụng bảo mật nhưng tôi đang chạy với thiết lập không bảo mật cho việc triển khai thử nghiệm. Tôi nghĩ ai đó có thể gặp phải tình huống tương tự. Đây là một mẫu mà tôi đã sử dụng để chế nhạo các phụ thuộc tĩnh trước đây. Giải pháp thay thế khác là bạn có thể duy trì tính tĩnh của lớp trình bao bọc nhưng tôi thích điều này hơn vì các phần phụ thuộc của mã rõ ràng hơn vì bạn phải chuyển CurrentUserAccessor vào các lớp mà nó được yêu cầu.

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.