24

Tôi đang xây dựng một ứng dụng với Spring Boot có tích hợp với LDAP. Tôi đã có thể kết nối thành công với máy chủ LDAP và xác thực người dùng. Bây giờ tôi có một yêu cầu để thêm chức năng nhớ-tôi. Tôi đã cố gắng để xem xét thông qua các bài viết khác nhau (this) nhưng không thể tìm thấy một câu trả lời cho vấn đề của tôi. Official Xuân An document khẳng định rằngSpring Security LDAP và Ghi nhớ tôi

Nếu bạn đang sử dụng một nhà cung cấp chứng thực mà không sử dụng một UserDetailsService (ví dụ, các nhà cung cấp LDAP) sau đó nó sẽ không làm việc trừ khi bạn cũng có một bean UserDetailsService trong ứng dụng của bạn bối cảnh

đây mã làm việc của tôi với một vài suy nghĩ ban đầu để thêm nhớ-me chức năng:

WebSecurityConfig

import com.ui.security.CustomUserDetailsServiceImpl; 
import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.context.annotation.Bean; 
import org.springframework.context.annotation.Configuration; 
import org.springframework.security.access.event.LoggerListener; 
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 
import org.springframework.security.config.annotation.web.builders.HttpSecurity; 
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider; 
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper; 
import org.springframework.security.web.authentication.RememberMeServices; 
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; 

@Configuration 
@EnableWebSecurity 
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 

    String DOMAIN = "ldap-server.com"; 
    String URL = "ldap://ds.ldap-server.com:389"; 


    @Override 
    protected void configure(HttpSecurity http) throws Exception { 
     http 
       .csrf().disable() 
       .authorizeRequests() 
       .antMatchers("/ui/**").authenticated() 
       .antMatchers("/", "/home", "/UIDL/**", "/ui/**").permitAll() 
       .anyRequest().authenticated() 
     ; 
     http 
       .formLogin() 
       .loginPage("/login").failureUrl("/login?error=true").permitAll() 
       .and().logout().permitAll() 
     ; 

     // Not sure how to implement this 
     http.rememberMe().rememberMeServices(rememberMeServices()).key("password"); 

    } 

    @Override 
    protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception { 

     authManagerBuilder 
       .authenticationProvider(activeDirectoryLdapAuthenticationProvider()) 
       .userDetailsService(userDetailsService()) 
     ; 
    } 

    @Bean 
    public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() { 

     ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(DOMAIN, URL); 
     provider.setConvertSubErrorCodesToExceptions(true); 
     provider.setUseAuthenticationRequestCredentials(true); 
     provider.setUserDetailsContextMapper(userDetailsContextMapper()); 
     return provider; 
    } 

    @Bean 
    public UserDetailsContextMapper userDetailsContextMapper() { 
     UserDetailsContextMapper contextMapper = new CustomUserDetailsServiceImpl(); 
     return contextMapper; 
    } 

    /** 
    * Impl of remember me service 
    * @return 
    */ 
    @Bean 
    public RememberMeServices rememberMeServices() { 
//  TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("password", userService); 
//  rememberMeServices.setCookieName("cookieName"); 
//  rememberMeServices.setParameter("rememberMe"); 
     return rememberMeServices; 
    } 

    @Bean 
    public LoggerListener loggerListener() { 
     return new LoggerListener(); 
    } 
} 

CustomUserDetailsServiceImpl

public class CustomUserDetailsServiceImpl implements UserDetailsContextMapper { 

    @Autowired 
    SecurityHelper securityHelper; 
    Log ___log = LogFactory.getLog(this.getClass()); 

    @Override 
    public LoggedInUserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> grantedAuthorities) { 

     LoggedInUserDetails userDetails = null; 
     try { 
      userDetails = securityHelper.authenticateUser(ctx, username, grantedAuthorities); 
     } catch (NamingException e) { 
      e.printStackTrace(); 
     } 

     return userDetails; 
    } 

    @Override 
    public void mapUserToContext(UserDetails user, DirContextAdapter ctx) { 

    } 
} 

tôi biết rằng tôi cần phải thực hiện UserService bằng cách nào đó, nhưng không chắc chắn làm thế nào mà có thể đạt được.

Trả lời

26

Có hai vấn đề cần cấu hình của Nhớ đến anh tính năng với LDAP:

  • lựa chọn thực hiện đúng Nhớ đến anh (Token vs PersistentTokens)
  • cấu hình của nó bằng cách sử dụng Cấu hình Java của Spring

Tôi sẽ thực hiện các bước này từng bước.

Token-based nhớ tôi tính năng (TokenBasedRememberMeServices) hoạt động theo cách sau khi xác thực:

  • người dùng được chứng thực (agaisnt AD) và chúng tôi hiện biết ID của người dùng và mật khẩu
  • chúng ta xây dựng Tên truy nhập giá trị + expirationTime + mật khẩu + staticKey và tạo một hash MD5 của nó
  • chúng ta tạo ra một cookie chứa username + hết hạn + hash tính

Khi người dùng muốn quay trở lại với dịch vụ và được xác thực bằng cách sử dụng chức năng nhớ tôi chúng tôi:

  • kiểm tra xem cookie tồn tại và chưa hết hạn
  • cư ID người dùng từ các tập tin cookie và gọi được cung cấp UserDetailsService được dự kiến ​​sẽ trở thông tin liên quan đến ID của người dùng, bao gồm cả mật khẩu
  • sau đó chúng tôi tính toán hash từ dữ liệu trả về và xác minh rằng băm trong cookie phù hợp với giá trị chúng tôi tính
  • nếu nó phù hợp chúng tôi trả lại đối tượng xác thực của người dùng

Quy trình kiểm tra băm là bắt buộc để đảm bảo rằng không ai có thể tạo "giả" nhớ cookie của tôi, điều này sẽ cho phép họ mạo danh người dùng khác. Vấn đề là quá trình này dựa vào khả năng tải mật khẩu từ kho lưu trữ của chúng tôi - nhưng điều này là không thể với Active Directory - chúng tôi không thể tải mật khẩu văn bản thuần túy dựa trên tên người dùng.

Điều này làm cho việc triển khai dựa trên mã thông báo không phù hợp để sử dụng với AD (trừ khi chúng tôi bắt đầu tạo một số cửa hàng người dùng cục bộ chứa mật khẩu hoặc một số thông tin xác thực người dùng bí mật khác và tôi không đề xuất cách tiếp cận này biết các chi tiết khác về ứng dụng của bạn, mặc dù nó có thể là một cách tốt để đi).

Các khác nhớ đến tôi thực hiện được dựa trên thẻ dai dẳng (PersistentTokenBasedRememberMeServices) và nó hoạt động như thế này (theo một cách đơn giản bit):

  • khi người dùng xác thực chúng tôi tạo ra một ngẫu nhiên thẻ
  • chúng tôi lưu trữ thẻ trong lưu trữ cùng với thông tin về ID của người dùng liên kết với nó
  • chúng ta tạo ra một cookie trong đó bao gồm ID thẻ

Khi người dùng muốn để xác thực chúng tôi:

  • kiểm tra cho dù chúng ta có cookie với ID thẻ có sẵn
  • xác minh xem ID thẻ tồn tại trong cơ sở dữ liệu
  • dữ liệu
  • tải của người dùng dựa trên thông tin trong cơ sở dữ liệu

Như bạn có thể thấy, mật khẩu không còn cần thiết, mặc dù bây giờ chúng ta cần một kho lưu trữ mã thông báo (thường là cơ sở dữ liệu, chúng ta có thể sử dụng trong bộ nhớ để kiểm tra) được sử dụng thay vì xác minh mật khẩu.

Và điều đó đưa chúng ta đến phần cấu hình. Cấu hình cơ bản cho persistent-token dựa nhớ tôi trông như thế này:

@Override 
protected void configure(HttpSecurity http) throws Exception {   
    .... 
    String internalSecretKey = "internalSecretKey"; 
    http.rememberMe().rememberMeServices(rememberMeServices(internalSecretKey)).key(internalSecretKey); 
} 

@Bean 
public RememberMeServices rememberMeServices(String internalSecretKey) { 
    BasicRememberMeUserDetailsService rememberMeUserDetailsService = new BasicRememberMeUserDetailsService(); 
    InMemoryTokenRepositoryImpl rememberMeTokenRepository = new InMemoryTokenRepositoryImpl(); 
    PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices(staticKey, rememberMeUserDetailsService, rememberMeTokenRepository); 
    services.setAlwaysRemember(true); 
    return services; 
} 

thực hiện này sẽ sử dụng trong bộ nhớ lưu trữ thẻ cần được thay thế bằng JdbcTokenRepositoryImpl phục vụ sản xuất. Việc cung cấp UserDetailsService có trách nhiệm tải dữ liệu bổ sung cho người dùng được xác định bằng ID người dùng được tải từ cookie nhớ của tôi. Việc thực hiện đơn giản nhất có thể trông như thế này:

public class BasicRememberMeUserDetailsService implements UserDetailsService { 
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 
     return new User(username, "", Collections.<GrantedAuthority>emptyList()); 
    } 
} 

Bạn cũng có thể cung cấp khác thực hiện UserDetailsService mà tải thuộc tính bổ sung hoặc thành viên nhóm từ AD của bạn hoặc cơ sở dữ liệu nội bộ, tùy thuộc vào nhu cầu của bạn. Nó có thể trông như thế này:

@Bean 
public RememberMeServices rememberMeServices(String internalSecretKey) { 
    LdapContextSource ldapContext = getLdapContext(); 

    String searchBase = "OU=Users,DC=test,DC=company,DC=com"; 
    String searchFilter = "(&(objectClass=user)(sAMAccountName={0}))"; 
    FilterBasedLdapUserSearch search = new FilterBasedLdapUserSearch(searchBase, searchFilter, ldapContext); 
    search.setSearchSubtree(true); 

    LdapUserDetailsService rememberMeUserDetailsService = new LdapUserDetailsService(search); 
    rememberMeUserDetailsService.setUserDetailsMapper(new CustomUserDetailsServiceImpl()); 

    InMemoryTokenRepositoryImpl rememberMeTokenRepository = new InMemoryTokenRepositoryImpl(); 

    PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices(internalSecretKey, rememberMeUserDetailsService, rememberMeTokenRepository); 
    services.setAlwaysRemember(true); 
    return services; 
} 

@Bean 
public LdapContextSource getLdapContext() { 
    LdapContextSource source = new LdapContextSource(); 
    source.setUserDn("[email protected]"+DOMAIN); 
    source.setPassword("password"); 
    source.setUrl(URL); 
    return source; 
} 

Điều này sẽ giúp bạn nhớ đến tôi chức năng mà làm việc với LDAP và cung cấp dữ liệu được nạp bên RememberMeAuthenticationToken mà sẽ có mặt tại các SecurityContextHolder.getContext().getAuthentication(). Nó cũng sẽ có thể tái sử dụng logic hiện tại của bạn để phân tích cú pháp dữ liệu LDAP thành đối tượng Người dùng (CustomUserDetailsServiceImpl).

Như một chủ đề riêng biệt, cũng có một vấn đề với mã đăng trong câu hỏi, bạn nên thay thế:

authManagerBuilder 
      .authenticationProvider(activeDirectoryLdapAuthenticationProvider()) 
      .userDetailsService(userDetailsService()) 
    ; 

với:

authManagerBuilder 
      .authenticationProvider(activeDirectoryLdapAuthenticationProvider()) 
    ; 

Các cuộc gọi đến userDetailsService chỉ nên được thực hiện để thêm xác thực dựa trên DAO (ví dụ: chống lại cơ sở dữ liệu) và nên được gọi với việc triển khai thực sự dịch vụ chi tiết người dùng. Cấu hình hiện tại của bạn có thể dẫn đến vòng lặp vô hạn.

+0

Câu trả lời hay Vladimír! câu hỏi nhanh. Trong dòng mã này: PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices (staticKey, rememberMeUserDetailsService, rememberMeTokenRepository); Tôi giả định rằng "staticKey" giống với "internalSecretKey". Đúng không? – Maksim

+1

Vâng, đó là cùng một khóa - nó được sử dụng để xác minh rằng RememberMeAuthenticationToken được xây dựng với kiến ​​thức về bí mật này và được xác minh trong RememberMeAuthenticationProvider. Hãy chắc chắn rằng bạn đang sử dụng phiên bản mới nhất của câu trả lời của tôi, bởi vì trong văn bản hiện tại ở trên nó được gọi là "internalSecretKey" ở tất cả các nơi. –

+0

Tuyệt vời, cảm ơn bạn rất nhiều! Làm tất cả mọi việc! – Maksim

0

Có vẻ như bạn đang thiếu một phiên bản UserService mà bạn cần tham chiếu đến RememberMeService. Vì bạn đang sử dụng LDAP, bạn cần có phiên bản LDAP là UserService. Tôi chỉ quen thuộc với việc triển khai JDBC/JPA, nhưng trông giống như org.springframework.security.ldap.userdetails.LdapUserDetailsManager là những gì bạn đang tìm kiếm. Sau đó, cấu hình của bạn sẽ trông như thế này:

@Bean 
public UserDetailsService getUserDetailsService() { 
    return new LdapUserDetailsManager(); // TODO give it whatever constructor params it needs 
} 

@Bean 
public RememberMeServices rememberMeServices() { 
    TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("password", getUserDetailsService()); 
    rememberMeServices.setCookieName("cookieName"); 
    rememberMeServices.setParameter("rememberMe"); 
    return rememberMeServices; 
} 
Các vấn đề liên quan