Spring 安全切换到 Ldap 身份验证和数据库权限

Posted

技术标签:

【中文标题】Spring 安全切换到 Ldap 身份验证和数据库权限【英文标题】:Spring security switch to Ldap authentication and database authorities 【发布时间】:2016-04-12 01:02:11 【问题描述】:

我为我的网页和 Web 服务实现了数据库身份验证。 它对两者都有效,现在我必须添加 Ldap 身份验证。 我必须通过远程 Ldap 服务器进行身份验证(使用用户名和密码),如果用户存在,我必须使用我的数据库作为用户角色(在我的数据库中,用户名与 Ldap 的用户名相同)。 所以我必须从我的实际代码切换到如上所述的 Ldap 和数据库身份验证。我的代码是: 安全配置类

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, proxyTargetClass = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter 

    @Autowired
    @Qualifier("userDetailsService")
    UserDetailsService userDetailsService;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    

    @Bean
    public PasswordEncoder passwordEncoder()
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        return encoder;
    

    @Configuration
    @Order(1)
    public static class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter
        @Override
        protected void configure(HttpSecurity http) throws Exception 
             http.csrf().disable()
             .antMatcher("/client/**")
             .authorizeRequests()
             .anyRequest().authenticated()
             .and()
             .httpBasic();
        
    

    @Configuration
    @Order(2)
    public static class FormWebSecurityConfig extends WebSecurityConfigurerAdapter

        @Override
        public void configure(WebSecurity web) throws Exception 
            web
            //Spring Security ignores request to static resources such as CSS or JS files.
            .ignoring()
            .antMatchers("/static/**");
        

        @Override
        protected void configure(HttpSecurity http) throws Exception 
            http
            .authorizeRequests() //Authorize Request Configuration
                //the / and /register path are accepted without login
                //.antMatchers("/", "/register").permitAll()
                //the /acquisition/** need admin role
                //.antMatchers("/acquisition/**").hasRole("ADMIN")
                //.and().exceptionHandling().accessDeniedPage("/Access_Denied");
                //all the path need authentication
                .anyRequest().authenticated()
                .and() //Login Form configuration for all others
            .formLogin()
                .loginPage("/login")
                //important because otherwise it goes in a loop because login page require authentication and authentication require login page
                    .permitAll()
            .and()
            .logout()
                .logoutSuccessUrl("/login?logout")
                .permitAll();
             // CSRF tokens handling
        
    

MyUserDetailsS​​ervice 类

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService 

    @Autowired
    private UserServices userServices;
    static final Logger LOG = LoggerFactory.getLogger(MyUserDetailsService.class);

    @Transactional(readOnly=true)
    @Override
    public UserDetails loadUserByUsername(final String username)
        try
            com.domain.User user = userServices.findById(username);
            if (user==null)
                LOG.error("Threw exception in MyUserDetailsService::loadUserByUsername : User doesn't exist" ); 
            else
                List<GrantedAuthority> authorities = buildUserAuthority(user.getUserRole());
                return buildUserForAuthentication(user, authorities);
            
        catch(Exception e)
            LOG.error("Threw exception in MyUserDetailsService::loadUserByUsername : " + ErrorExceptionBuilder.buildErrorResponse(e));  
        return null;
    

    // Converts com.users.model.User user to
    // org.springframework.security.core.userdetails.User
    private User buildUserForAuthentication(com.domain.User user, List<GrantedAuthority> authorities) 
        return new User(user.getUsername(), user.getPassword(), user.isEnabled(), true, true, true, authorities);
    

    private List<GrantedAuthority> buildUserAuthority(Set<UserRole> userRoles) 

        Set<GrantedAuthority> setAuths = new HashSet<GrantedAuthority>();

        // Build user's authorities
        for (UserRole userRole : userRoles) 
            setAuths.add(new SimpleGrantedAuthority(userRole.getUserRoleKeys().getRole()));
        

        List<GrantedAuthority> Result = new ArrayList<GrantedAuthority>(setAuths);

        return Result;
    

所以我必须:

1) 用户从网页的登录页面和网络服务的用户名和密码访问。这必须通过 Ldap 完成。

2) 数据库查询需要用户的用户名来认证用户。 你知道我该如何实现吗? 谢谢

使用正确的代码更新:在@M 之后。 Deinum 建议我创建 MyAuthoritiesPopulator 类而不是 MyUserDetailsService 并使用数据库和 Ldap 进行身份验证:

    @Service("myAuthPopulator")
public class MyAuthoritiesPopulator implements LdapAuthoritiesPopulator 

    @Autowired
    private UserServices userServices;
    static final Logger LOG = LoggerFactory.getLogger(MyAuthoritiesPopulator.class);

    @Transactional(readOnly=true)
    @Override
    public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOperations userData, String username) 
        Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
        try
            com.domain.User user = userServices.findById(username);
            if (user==null)
                LOG.error("Threw exception in MyAuthoritiesPopulator::getGrantedAuthorities : User doesn't exist into ATS database" );  
            else
                for(UserRole userRole : user.getUserRole()) 
                    authorities.add(new SimpleGrantedAuthority(userRole.getUserRoleKeys().getRole()));
                
                return authorities;
            
        catch(Exception e)
            LOG.error("Threw exception in MyAuthoritiesPopulator::getGrantedAuthorities : " + ErrorExceptionBuilder.buildErrorResponse(e)); 
        return authorities;
    

我将 SecurityConfig 更改如下:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, proxyTargetClass = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter 

    @Autowired
    @Qualifier("myAuthPopulator")
    LdapAuthoritiesPopulator myAuthPopulator;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception 

         auth.ldapAuthentication()
          .contextSource()
            .url("ldap://127.0.0.1:10389/dc=example,dc=com")
//          .managerDn("")
//          .managerPassword("")
          .and()   
            .userSearchBase("ou=people")
            .userSearchFilter("(uid=0)")
            .ldapAuthoritiesPopulator(myAuthPopulator);     
    

    @Configuration
    @Order(1)
    public static class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter
        @Override
        protected void configure(HttpSecurity http) throws Exception 
             http.csrf().disable()
             .antMatcher("/client/**")
             .authorizeRequests()
             //Excluede send file from authentication because it doesn't work with spring authentication
             //TODO add java authentication to send method
             .antMatchers(HttpMethod.POST, "/client/file").permitAll()
             .anyRequest().authenticated()
             .and()
             .httpBasic();
        
    

    @Configuration
    @Order(2)
    public static class FormWebSecurityConfig extends WebSecurityConfigurerAdapter

        @Override
        public void configure(WebSecurity web) throws Exception 
            web
            //Spring Security ignores request to static resources such as CSS or JS files.
            .ignoring()
            .antMatchers("/static/**");
        

        @Override
        protected void configure(HttpSecurity http) throws Exception 
            http
            .authorizeRequests() //Authorize Request Configuration
                //the "/" and "/register" path are accepted without login
                //.antMatchers("/", "/register").permitAll()
                //the /acquisition/** need admin role
                //.antMatchers("/acquisition/**").hasRole("ADMIN")
                //.and().exceptionHandling().accessDeniedPage("/Access_Denied");
                //all the path need authentication
                .anyRequest().authenticated()
                .and() //Login Form configuration for all others
            .formLogin()
                .loginPage("/login")
                //important because otherwise it goes in a loop because login page require authentication and authentication require login page
                    .permitAll()
            .and()
            .logout()
                .logoutSuccessUrl("/login?logout")
                .permitAll();
        
    

我在 Apache directory studio 中创建的 LDAP 开发环境

【问题讨论】:

您是否阅读过包含whole chapter on ldap 以及其中包含哪些组件的参考指南。 我读得很快,因为我使用的是注解而不是 xml。我现在又读了一遍 所以答案你也可以用Java配置一下。 通过数据库认证?网上有很多例子 【参考方案1】:

对于任何使用 grails 的人来说,它要简单得多。只需将其添加到您的配置中:

圣杯: 插入: 弹簧安全: LDAP: 当局: 检索数据库角色:真

【讨论】:

【参考方案2】:

我还发现了这一章Spring Docu Custom Authenicator 并在 LDAP 和我的 DB 用户之间构建了我自己的切换。我可以毫不费力地在具有设定优先级的登录数据之间切换(在我的情况下 LDAP 获胜)。

我已经使用 yaml 配置文件为 LDAP 用户数据配置了一个 LDAP,我在这里不详细披露。这可以通过Spring Docu LDAP Configuration 轻松完成。

我删除了以下示例,例如 logger/javadoc 等,以突出显示重要部分。 @Order 注释确定使用登录数据的优先级。内存中的详细信息是硬编码的调试用户,仅供开发人员使用。

SecurityWebConfiguration

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter 

  @Inject
  private Environment env;
  @Inject
  private LdapConfiguration ldapConfiguration;

  @Inject
  private BaseLdapPathContextSource contextSource;
  @Inject
  private UserDetailsContextMapper userDetailsContextMapper;

  @Inject
  private DBAuthenticationProvider dbLogin;

  @Inject
  @Order(10) // the lowest number wins and is used first
  public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception 
    auth.userDetailsService(new InMemoryUserDetailsManager(getInMemoryUserDetails()));
  

  @Inject
  @Order(11) // the lowest number wins and is used first
  public void configureLDAP(AuthenticationManagerBuilder auth) throws Exception 
    if (ldapConfiguration.isLdapEnabled()) 
      auth.ldapAuthentication().userSearchBase(ldapConfiguration.getUserSearchBase())
          .userSearchFilter(ldapConfiguration.getUserSearchFilter())
          .groupSearchBase(ldapConfiguration.getGroupSearchBase()).contextSource(contextSource)
          .userDetailsContextMapper(userDetailsContextMapper);
    
  

  @Inject
  @Order(12) // the lowest number wins and is used first
  public void configureDB(AuthenticationManagerBuilder auth) throws Exception 
    auth.authenticationProvider(dbLogin);
  

数据库身份验证器

@Component
public class DBAuthenticationProvider implements AuthenticationProvider 

  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException 
    String name = authentication.getName();
    String password = authentication.getCredentials().toString();

   // your code to compare to your DB
  

  @Override
  public boolean supports(Class<?> authentication) 
    return authentication.equals(UsernamePasswordAuthenticationToken.class);
  

  /**
   * @param original <i>mandatory</i> - input to be hashed with SHA256 and HEX encoding
   * @return the hashed input
   */
  private String sha256(String original) 
    MessageDigest md = null;
    try 
      md = MessageDigest.getInstance("SHA-256");
     catch (NoSuchAlgorithmException e) 
      throw new AuthException("The processing of your password failed. Contact support.");
    

    if (false == Strings.isNullOrEmpty(original)) 
      md.update(original.getBytes());
    

    byte[] digest = md.digest();
    return new String(Hex.encodeHexString(digest));
  

  private class AuthException extends AuthenticationException 
    public AuthException(final String msg) 
      super(msg);
    
  

欢迎询问详情。我希望这对其他人有用:D

【讨论】:

【参考方案3】:

Spring Security 已经支持开箱即用的 LDAP。它实际上有一个whole chapter。

要使用和配置 LDAP,请添加 spring-security-ldap 依赖项,然后使用 AuthenticationManagerBuilder.ldapAuthentication 进行配置。 LdapAuthenticationProviderConfigurer 允许您设置所需的东西。

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception 
    auth.ldapAuthentication()
      .contextSource()
        .url(...)
        .port(...)
        .managerDn(...)
        .managerPassword(...)
      .and()
        .passwordEncoder(passwordEncoder())
        .userSearchBase(...)        
        .ldapAuthoritiesPopulator(new UserServiceLdapAuthoritiesPopulater(this.userService));      

类似的东西(它至少应该让您了解什么/如何配置东西)还有更多选项,但请查看 javadocs。如果您不能按原样使用UserService 来检索角色(因为只有角色在数据库中),请为此实现您自己的LdapAuthoritiesPopulator

【讨论】:

我知道 Spring 安全性支持 LDAP,但我必须在 LDAP 和数据库之间进行混合,而且我也找不到仅适用于 LDAP 的有效示例。 您真的阅读过我的回答并阅读过 javadocs 吗?从你没有的评论来看。 是的,它解释说“如果您只想使用 LDAP 进行身份验证,但从不同的源(例如数据库)加载权限,那么您可以提供自己的此接口实现并注入而是那个。”所以我更新了我的第一篇文章 您是否测试了修改后的解决方案?为什么它不是正确的方法(您仍然必须根据您的结构修复您的 ldap 配置)。 @M.Deinum 您与我们共享的所有链接都已失效。请检查它们。【参考方案4】:

您需要创建一个 CustomAuthenticationProvider 来实现 实现 AuthenticationProvider,并覆盖身份验证 方法,例如:

@Component
public class CustomAuthenticationProvider
    implements AuthenticationProvider 

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException 
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        boolean authenticated = false;
        /**
         * Here implements the LDAP authentication
         * and return authenticated for example
         */
        if (authenticated) 

            String usernameInDB = "";
            /**
             * Here look for username in your database!
             * 
             */
            List<GrantedAuthority> grantedAuths = new ArrayList<>();
            grantedAuths.add(new     SimpleGrantedAuthority("ROLE_USER"));
            Authentication auth = new     UsernamePasswordAuthenticationToken(usernameInDB, password,     grantedAuths);
            return auth;
         else 
            return null;
        
    

    @Override
    public boolean supports(Class<?> authentication) 
        return     authentication.equals(UsernamePasswordAuthenticationToken.class);
    


然后,在您的 SecurityConfig 中,您需要覆盖使用 AuthenticationManagerBuilder /strong>:

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception 
    auth.authenticationProvider(this.authenticationProvider);

您可以这样做自动装配 CustomAuthenticationProvider:

@Autowired
private CustomAuthenticationProvider authenticationProvider;

这样做,您可以覆盖默认的身份验证行为。

【讨论】:

感谢您的回复。对于 LDAP 身份验证,您是指弹簧身份验证还是仅检查用户和密码是否存在? 为什么?为什么这么复杂。 Spring Security 开箱即用支持 Ldap,配置正确。然后,不要从 ldap 检索角色,而是使用数据库驱动 LdapAuthoritiesPopulator(已经有一个用户服务驱动的,所以这一切都可能是一个简单的配置问题)。 @luca 使用此实现,您可以覆盖 AuthenticationProvider 的默认行为。我做了类似的事情,因为我有一个第三方应用程序,它为我提供身份验证服务,但与 Ldap 必须非常相似。

以上是关于Spring 安全切换到 Ldap 身份验证和数据库权限的主要内容,如果未能解决你的问题,请参考以下文章

通过 spring LDAP 进行身份验证,并在数据库中进行额外的安全检查

Spring Security - 在运行时在身份验证提供者之间切换(本地数据库或远程 LDAP)

使用 LDAP 和组成员身份的 Spring 安全性

Grails - Spring 安全 ldap 活动目录身份验证 - 凭据错误错误

如何使用 xml 配置文件、JAVA、Spring 安全性对 LDAP 用户进行身份验证

具有Spring安全性的LDAP身份验证