Spring Security Ldap 登录认证流程的源码梳理

news2024/11/26 0:29:44

一、通过请求Controller开始登录认证

通过authenticationManager调用authenticate()方法开始登录认证,因为authenticationManager是通过@Bean注入,因为SecurityLdapConfig是继承的WebSecurityConfigurerAdapter类,所以authenticationManager的类型就是在WebSecurityConfigurerAdapter类的内部类AuthenticationManagerDelegator,所以在调用authenticate()方法时候是在调用AuthenticationManagerDelegator类的authenticate()方法。

代码如下:

package com.framework.ldap.controller;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
 
 
 
/**
 * @Author: LongGE
 * @Date: 2023-03-27
 * @Description:
 */
@Slf4j
@RestController
@RequestMapping("/AuthLdapController")
public class AuthLdapController {
     
    //在SecurityLdapConfig中通过@Bean注入
    @Autowired
    private AuthenticationManager authenticationManager;
 
    @PostMapping(value = "/login")
    public String login(String username, String password) {
        log.info("start spring security login to!");
        //登录认证流程
        Authentication authentication = authenticationManager
                .authenticate(new UsernamePasswordAuthenticationToken(username, password));
 
        log.info("登录返回信息:{}", authentication);
        return "login to success!";
    }
}

二、分析AuthenticationManagerDelegator类

它本身是WebSecurityConfigurerAdapter类的内部类,由WebSecurityConfigurerAdapter初始化生成的。

代码如下:

/**
 * 在项目启动时候通过@Bean构建的 AuthenticationManagerDelegator类,
 * 所以在登录我们注入的AuthenticationManager就是它
 */
static final class AuthenticationManagerDelegator implements AuthenticationManager {
         
        //它在是通过构造函数传入的参数构建
        private AuthenticationManagerBuilder delegateBuilder;
         
        //项目启动后第一请求的时候他是空值
        private AuthenticationManager delegate;
 
        private final Object delegateMonitor = new Object();
 
        private Set<String> beanNames;
         
        //构造函数
        AuthenticationManagerDelegator(AuthenticationManagerBuilder delegateBuilder, ApplicationContext context) {
            Assert.notNull(delegateBuilder, "delegateBuilder cannot be null");
            Field parentAuthMgrField = ReflectionUtils.findField(AuthenticationManagerBuilder.class,
                    "parentAuthenticationManager");
            ReflectionUtils.makeAccessible(parentAuthMgrField);
            this.beanNames = getAuthenticationManagerBeanNames(context);
            validateBeanCycle(ReflectionUtils.getField(parentAuthMgrField, delegateBuilder), this.beanNames);
            this.delegateBuilder = delegateBuilder;
        }
        /**
         * 登录认证
         * 首次请求登录时候 delegate 是空值,我们需要去赋值一个在初始化已经创建好的ProviderManager,这个ProviderManager的providers集合中只有一个AuthenticationProvider,就是AnonymousAuthenticationProvider
         * 参数Authentication authentication看似和我们在Controller时候传入的UsernamePasswordAuthenticationToken不同,实际上UsernamePasswordAuthenticationToken是实现了Authentication
         * Authentication是接口,AbstractAuthenticationToken是实现Authentication接口的抽象类,UsernamePasswordAuthenticationToken是继承了AbstractAuthenticationToken抽象类的,所以可以使用Authentication泛指
        */
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            //项目启动后首次请求时候 delegate 为空值,但是第二次请求时候就不在为空
            if (this.delegate != null) {
                return this.delegate.authenticate(authentication);
            }
            //因为首次请求 delegate 为空值,所以我们需要对 delegate 进行赋值操作,这里通过 synchronized 关键字来做线程锁,保证线程安全
            synchronized (this.delegateMonitor) {
                if (this.delegate == null) {
                    //通过项目启动已经配置好的 AuthenticationManagerBuilder 去获取AuthenticationManager
                    this.delegate = this.delegateBuilder.getObject();
                    //在将项目启动配置好的 AuthenticationManagerBuilder 赋值为空
                    this.delegateBuilder = null;
                }
            }
            //首次请求时候去调用AuthenticationManager 的 认证接口。
            return this.delegate.authenticate(authentication);
        }
    }

登录认证我们在Controller传入的UsernamePasswordAuthenticationToken类,但是在AuthenticationManagerDelegator类的authenticate()方法时候参数却是Authentication类型,所以要解释一下二者之间的关系。

关系如图:

三、解析ProviderManager类

ProviderManager类主要判断AuthenticationProvider集合中是否由满足要求的AuthenticationProvider,如果存在那么调用authenticate()方法进行认证。

ProviderManager类主要代码如下:

package org.springframework.security.authentication;
 
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
 
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
 
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.log.LogMessage;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
 
/**
* 通过循环遍历AuthenticationProvider集合,找到符合要求的AuthenticationProvider进行认证流程
* 它是一个中央的调度器,主要是管理不同的AuthenticationProvider认证器
*/
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
 
    private static final Log logger = LogFactory.getLog(ProviderManager.class);
 
    private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
     
    // 登录认证器集合
    private List<AuthenticationProvider> providers = Collections.emptyList();
 
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
 
    // 这里还是一个ProviderManager 在加载过程中我们的AuthenticationManagerDelegator会生成一个自定义的抽闲AuthenticationProvider
    private AuthenticationManager parent;
 
    private boolean eraseCredentialsAfterAuthentication = true;
 
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //获取Authentication的实现类的Class格式,后面用于判断具体由哪个AuthenticationProvider去执行认证流程
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        //获取AuthenticationProvider的数量
        int currentPosition = 0;
        int size = this.providers.size();
        for (AuthenticationProvider provider : getProviders()) {
            //判断入参的Authentication类型是否是AuthenticationProvider内的Authentication同类或者子类型
            //如果是同类型或者子类返回true,不是返回false
            if (!provider.supports(toTest)) {
                continue;
            }
            if (logger.isTraceEnabled()) {
                logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
                        provider.getClass().getSimpleName(), ++currentPosition, size));
            }
            try {
                //当ProviderManager中的providers集合中的AuthenticationProvider由满足条件
                result = provider.authenticate(authentication);
                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException | InternalAuthenticationServiceException ex) {
                prepareException(ex, authentication);
                throw ex;
            }
            catch (AuthenticationException ex) {
                lastException = ex;
            }
        }
        //当AuthenticationProvider集合中都没有满足要求的,并且parent不是空值时候那么久要通过parent去进行认证
        if (result == null && this.parent != null) {
            try {
                //通过parent去调用authenticate()方法进行校验
                parentResult = this.parent.authenticate(authentication);
                //返回结果赋值给 result
                result = parentResult;
            }
            catch (ProviderNotFoundException ex) {
            }
            catch (AuthenticationException ex) {
                parentException = ex;
                lastException = ex;
            }
        }
        //判断认证返回结果,如果返回结果不为空,
        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
                ((CredentialsContainer) result).eraseCredentials();
            }
            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }
            //返回认证结果,Authentication
            return result;
        }
        //认证错误并且没有抛出异常时候,那么这里抛出一个新的异常。
        if (lastException == null) {
            lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
        }
         
        if (parentException == null) {
            prepareException(lastException, authentication);
        }
        throw lastException;
    }
 
}

四、分析AbstractLdapAuthenticationProvider类

因为我们使用的Security的Ldap登录认证,所以我们这里主要分析一下AbstractLdapAuthenticationProvider类:(主要分析我们登录认证相关联的部分代码)

部分源代码如下:

package org.springframework.security.ldap.authentication;
 
import java.util.Collection;
 
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
 
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
 
/**
 *  AbstractLdapAuthenticationProvider 是由Security给我们提供的一个关于Ldap的登录认证器
 *  认证器主要是用于用户登录认证
 */
public abstract class AbstractLdapAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {
 
    protected final Log logger = LogFactory.getLog(getClass());
 
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
 
    private boolean useAuthenticationRequestCredentials = true;
 
    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
 
    protected UserDetailsContextMapper userDetailsContextMapper = new LdapUserDetailsMapper();
 
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> this.messages.getMessage("LdapAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));
        //强制转换将我们在Controller中登录认证的传入参数转回来
        UsernamePasswordAuthenticationToken userToken = (UsernamePasswordAuthenticationToken) authentication;
        //获取登录用户账号
        String username = userToken.getName();
        //获取登录的密码
        String password = (String) authentication.getCredentials();
        //校验是否为空,或者空字符串
        if (!StringUtils.hasLength(username)) {
            throw new BadCredentialsException(
                    this.messages.getMessage("LdapAuthenticationProvider.emptyUsername", "Empty Username"));
        }
        //校验是否为空,或者空字符串
        if (!StringUtils.hasLength(password)) {
            throw new BadCredentialsException(
                    this.messages.getMessage("AbstractLdapAuthenticationProvider.emptyPassword", "Empty Password"));
        }
        Assert.notNull(password, "Null password was supplied in authentication token");
        //登录认证,核心操作由LdapAuthenticationProvider去重写完成
        DirContextOperations userData = doAuthentication(userToken);
        //获取用户的权限信息并且封装成UserDetails
        UserDetails user = this.userDetailsContextMapper.mapUserFromContext(userData, authentication.getName(),
                loadUserAuthorities(userData, authentication.getName(), (String) authentication.getCredentials()));
        //将Authentication和UserDetails封装成返回结果,返回给登录成功
        return createSuccessfulAuthentication(userToken, user);
    }
     
    /**
     * 这里提供一个抽象方法,由LdapAuthenticationProvider类去重写doAuthentication()方法来完成登录认证。
     */
    protected abstract DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken auth);
     
    /**
     * 这里提供一个抽象方法,由LdapAuthenticationProvider类去重写loadUserAuthorities(),来获取用户的权限信息
     */
    protected abstract Collection<? extends GrantedAuthority> loadUserAuthorities(DirContextOperations userData,
            String username, String password);
     
    /**
     * 在登录认证成功后,构建一个 Authentication 作为登录成功的返回值
     */
    protected Authentication createSuccessfulAuthentication(UsernamePasswordAuthenticationToken authentication,
            UserDetails user) {
        Object password = this.useAuthenticationRequestCredentials ? authentication.getCredentials()
                : user.getPassword();
        UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(user, password,
                this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        this.logger.debug("Authenticated user");
        return result;
    }
     
    /**
     * 这里可以呼应到上面的ProviderManager,在ProviderManager中我们循环AuthenticationProvider集合时候就是通过每一个AuthenticationProvider的supports()方法
     * 来判断我们登录认证在Controller时候传入的Authentication是否能够和AuthenticationProvider中的对应,如果类型相同那么就可以判断去使用当前的Provider去完成登录认证。
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
 
}

五、分析LdapAuthenticationProvider类

LdapAuthenticationProvider主要就是一个承上启下功能,主要做一下两部分内容:

1.主要通过LdapAuthenticator接口的authenticate()方法去完成认证功能,这个接口会被Security自己的AbstractLdapAuthenticator抽象类实现,同样AbstractLdapAuthenticator也被BindAuthenticator类继承,这样子类BindAuthenticator去实现authenticate()方法。

2.为AbstractLdapAuthenticationProvider 类的loadUserAuthorities()方法,提供向下的一个实现功能,这里会通过LdapAuthoritiesPopulator接口的getGrantedAuthorities()方法来获取用户的权限信息,这样我们就能通过实现LdapAuthoritiesPopulator接口来完成用户的权限封装。

LdapAuthenticationProvider类的部分源码如下:

package org.springframework.security.ldap.authentication;
 
import java.util.Collection;
 
import org.springframework.ldap.NamingException;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.ldap.ppolicy.PasswordPolicyException;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.util.Assert;
 
 
public class LdapAuthenticationProvider extends AbstractLdapAuthenticationProvider {
     
    //  LdapAuthenticator 是一个接口类,会被AbstractLdapAuthenticator抽象类实现,但是authenticate()方法会由AbstractLdapAuthenticator类的子类实现
    private LdapAuthenticator authenticator;
     
    // LdapAuthoritiesPopulator 接口,这个接口我们可以选择自己实现,这样可以完成用户的权限的查询
    private LdapAuthoritiesPopulator authoritiesPopulator;
 
    private boolean hideUserNotFoundExceptions = true;
 
    private LdapAuthenticator getAuthenticator() {
        return this.authenticator;
    }
 
    protected LdapAuthoritiesPopulator getAuthoritiesPopulator() {
        return this.authoritiesPopulator;
    }
     
    /**
     * 登录认证,通过 LdapAuthenticator 接口的实现BindAuthenticator类,由它的authenticate()来实现用户的登录认证。
     */
    @Override
    protected DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken authentication) {
        try {
            //获取BindAuthenticator类,调用它的authenticate()方法进行登录认证
            return getAuthenticator().authenticate(authentication);
        }
        catch (PasswordPolicyException ex) {
            throw new LockedException(
                    this.messages.getMessage(ex.getStatus().getErrorCode(), ex.getStatus().getDefaultMessage()));
        }
        catch (UsernameNotFoundException ex) {
            if (this.hideUserNotFoundExceptions) {
                throw new BadCredentialsException(
                        this.messages.getMessage("LdapAuthenticationProvider.badCredentials", "Bad credentials"));
            }
            throw ex;
        }
        catch (NamingException ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }
 
    /**
     * 这里是为AbstractLdapAuthenticationProvider类调用用户权限查询进行实现
     * 这里通过获取一个LdapAuthoritiesPopulator接口的实现类的getGrantedAuthorities()查询用户的角色权限集合
     */
    @Override
    protected Collection<? extends GrantedAuthority> loadUserAuthorities(DirContextOperations userData, String username,
            String password) {
        return getAuthoritiesPopulator().getGrantedAuthorities(userData, username);
    }
 
}

六、分析AbstractLdapAuthenticator类

AbstractLdapAuthenticator类主要是封装Ldap的连接数据以及在SecurityLdapConfig注入的查询用户的条件信息。

源代码如下:

package org.springframework.security.ldap.authentication;
 
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
 
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.ldap.core.ContextSource;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.ldap.search.LdapUserSearch;
import org.springframework.util.Assert;
 
/**
 *  AbstractLdapAuthenticator 类虽然实现了LdapAuthenticator接口但是并未实现接口的authenticate()方法去做登录认证,此方法由 AbstractLdapAuthenticator 子类去实现。
 */
public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, InitializingBean, MessageSourceAware {
     
    //Ldap的连接实例
    private final ContextSource contextSource;
 
    /**
     * 当通过DN不能搜索到用户时候可以通过userSearch去搜索用户
     */
    private LdapUserSearch userSearch;
 
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
 
    /**
     * 目录列表信息
     */
    private String[] userAttributes = null;
 
    /**
     * 通过userDnPatterns 查询用户时候设置对应的userDN路径值,默认格式一般为uid={0}, ou=people
     */
    private MessageFormat[] userDnFormat = null;
 
    /**
     * 初始化时候给Ldap的连接实例赋值
     */
    public AbstractLdapAuthenticator(ContextSource contextSource) {
        Assert.notNull(contextSource, "contextSource must not be null.");
        this.contextSource = contextSource;
    }
     
    //获取Ldap连接实例
    protected ContextSource getContextSource() {
        return this.contextSource;
    }
     
    //获取所有的目录列表
    public String[] getUserAttributes() {
        return this.userAttributes;
    }
 
    /**
     * 把用户名username拼接到用户ND列表中去替换到原来{0},原来的Nd=uid{0},ou=people,现在讲{0}替换成username,替换结果为 uid=username,ou=people
     */
    protected List<String> getUserDns(String username) {
        if (this.userDnFormat == null) {
            return Collections.emptyList();
        }
        List<String> userDns = new ArrayList<>(this.userDnFormat.length);
        String[] args = new String[] { LdapEncoder.nameEncode(username) };
        synchronized (this.userDnFormat) {
            for (MessageFormat formatter : this.userDnFormat) {
                userDns.add(formatter.format(args));
            }
        }
        return userDns;
    }
     
    /**
     * 获取检索用户的条件
     */
    protected LdapUserSearch getUserSearch() {
        return this.userSearch;
    }
 
    /**
     * 设置将从目录检索的用户属性。
     */
    public void setUserAttributes(String[] userAttributes) {
        Assert.notNull(userAttributes, "The userAttributes property cannot be set to null");
        this.userAttributes = userAttributes;
    }
 
    /**
     * 设置用于为用户提供DN的模式。模式应该是相对于根DN的名称。模式参数{0}将包含用户名。例如“cn={0},ou=people”。
     */
    public void setUserDnPatterns(String[] dnPattern) {
        Assert.notNull(dnPattern, "The array of DN patterns cannot be set to null");
        // this.userDnPattern = dnPattern;
        this.userDnFormat = new MessageFormat[dnPattern.length];
        for (int i = 0; i < dnPattern.length; i++) {
            this.userDnFormat[i] = new MessageFormat(dnPattern[i]);
        }
    }
     
    /**
     * 赋值根据userSearch搜用户的条件赋值
     */
    public void setUserSearch(LdapUserSearch userSearch) {
        Assert.notNull(userSearch, "The userSearch cannot be set to null");
        this.userSearch = userSearch;
    }
 
}

七、分析BindAuthenticator类

BindAuthentication类主要做如下功能:

1.通过父类AbstractLdapAuthenticator 获取用户的ND列表路径,如果不存在就通过父类在获取userSearch用户列表查询路径。

2.通过父类AbstractLdapAuthenticator 获取Ldap的连接contextSource。

3.实现了LdapAuthenticator接口的authenticate()方法,完成用户登录认证。

总结:无论是通过DN模式还是通过UserSearch的模式完成登录认证实际上使用的是统一中认证方式,只是在我们配置SecurityLdap的认证器时候可以选择不同方式。

源码分析如下:

package org.springframework.security.ldap.authentication;
 
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
 
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
 
import org.springframework.core.log.LogMessage;
import org.springframework.ldap.NamingException;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.ldap.ppolicy.PasswordPolicyControl;
import org.springframework.security.ldap.ppolicy.PasswordPolicyControlExtractor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
 
/**
 * 作为用户绑定的身份验证器
 */
public class BindAuthenticator extends AbstractLdapAuthenticator {
 
    private static final Log logger = LogFactory.getLog(BindAuthenticator.class);
 
    /**
     * 用BaseLdapPathContextSource初始化Ldap连接实例
     */
    public BindAuthenticator(BaseLdapPathContextSource contextSource) {
        super(contextSource);
    }
     
    /**
     * 登录认证逻辑
     * 1.先通过父类的getUserDns(username)获取用户ND模式,如果有返回值那么就采用DN模式登录认证
     * 2.当DN模式不生效获取返回结果为空时候采用 userSearch的方式查询用户登录认证
     * 总结两种模式实际是一样的登录认证,一种是直接配置ND路径,一种是路径和Username分离进行配置
     */
    @Override
    public DirContextOperations authenticate(Authentication authentication) {
        DirContextOperations user = null;
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                "Can only process UsernamePasswordAuthenticationToken objects");
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();
        if (!StringUtils.hasLength(password)) {
            logger.debug(LogMessage.format("Failed to authenticate since no credentials provided"));
            throw new BadCredentialsException(
                    this.messages.getMessage("BindAuthenticator.emptyPassword", "Empty Password"));
        }
        // 如果配置了DN模式,通过DN模式进行用户登录认证
        for (String dn : getUserDns(username)) {
            //通过DN模式登录认证
            user = bindWithDn(dn, username, password);
            if (user != null) {
                break;
            }
        }
        //当DN模式不生效或者查询不到用户时候才会采用userSearch方法登录认证
        if (user == null) {
            logger.debug(LogMessage.of(() -> "Failed to bind with any user DNs " + getUserDns(username)));
        }
        //当DN模式查询不生效,并且配置了userSearche的模式进行登录认证
        if (user == null && getUserSearch() != null) {
            logger.trace("Searching for user using " + getUserSearch());
            //通过父类获取UserSearch拼接获取用户查询条件,实际等同DN模式下的路径
            DirContextOperations userFromSearch = getUserSearch().searchForUser(username);
            //同理还是使用DN模式完成用户登录认证
            user = bindWithDn(userFromSearch.getDn().toString(), username, password, userFromSearch.getAttributes());
            if (user == null) {
                logger.debug("Failed to find user using " + getUserSearch());
            }
        }
        if (user == null) {
            throw new BadCredentialsException(
                    this.messages.getMessage("BindAuthenticator.badCredentials", "Bad credentials"));
        }
        return user;
    }
     
    /**
     * DN模式登录认证
     */
    private DirContextOperations bindWithDn(String userDnStr, String username, String password) {
        return bindWithDn(userDnStr, username, password, null);
    }
     
    /**
     * 通过DN模式完成用户登录认证
     */
    private DirContextOperations bindWithDn(String userDnStr, String username, String password, Attributes attrs) {
        //获取Ldap连接,这里是项目初始化的Ldap连接,主要获取他的Base路径
        BaseLdapPathContextSource ctxSource = (BaseLdapPathContextSource) getContextSource();
        //通过用户DN模式信息创建连接器
        DistinguishedName userDn = new DistinguishedName(userDnStr);
        //通过连接器创建连接器
        DistinguishedName fullDn = new DistinguishedName(userDn);
        //将Ldap连接的base路径添加到连接其中
        fullDn.prepend(ctxSource.getBaseLdapPath());
        logger.trace(LogMessage.format("Attempting to bind as %s", fullDn));
        DirContext ctx = null;
        try {
            //通过连接器和用户名进行登录认证,成功返回当前用户的Ldap连接
            ctx = getContextSource().getContext(fullDn.toString(), password);
            // 检查密码策略控制
            PasswordPolicyControl ppolicy = PasswordPolicyControlExtractor.extractControl(ctx);
            if (attrs == null || attrs.size() == 0) {
                //获取用户信息列表
                attrs = ctx.getAttributes(userDn, getUserAttributes());
            }
            //封装用户信息,返回登录认证结果
            DirContextAdapter result = new DirContextAdapter(attrs, userDn, ctxSource.getBaseLdapPath());
            if (ppolicy != null) {
                result.setAttributeValue(ppolicy.getID(), ppolicy);
            }
            logger.debug(LogMessage.format("Bound %s", fullDn));
            //返回结果
            return result;
        }
        catch (NamingException ex) {
            // This will be thrown if an invalid user name is used and the method may
            // be called multiple times to try different names, so we trap the exception
            // unless a subclass wishes to implement more specialized behaviour.
            if ((ex instanceof org.springframework.ldap.AuthenticationException)
                    || (ex instanceof org.springframework.ldap.OperationNotSupportedException)) {
                handleBindException(userDnStr, username, ex);
            }
            else {
                throw ex;
            }
        }
        catch (javax.naming.NamingException ex) {
            throw LdapUtils.convertLdapException(ex);
        }
        finally {
            LdapUtils.closeContext(ctx);
        }
        return null;
    }
 
    /**
     * Allows subclasses to inspect the exception thrown by an attempt to bind with a
     * particular DN. The default implementation just reports the failure to the debug
     * logger.
     */
    protected void handleBindException(String userDn, String username, Throwable cause) {
        logger.trace(LogMessage.format("Failed to bind as %s", userDn), cause);
    }
 
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/459806.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Unity+MySQL】实现注册登录系统(封装版)

目录 1 MySQL封装2 用户注册、登录方法封装3 Unity交互 接着 上篇文章的注册登录系统&#xff0c;这篇文章将MySQL相关操作封装&#xff0c;在Unity交互脚本中直接调用封装的方法。 1 MySQL封装 编写一个DBConnector脚本&#xff0c;封装MySQL中常用的操作&#xff0c;如连接…

【已解决】SpringBoot 工程 war包服务部署与调用测试

1.开发环境&#xff1a;IDEA&#xff0c;JDK1.8 2.服务打包类型&#xff1a; war包 3.war包部署环境&#xff1a;Linux系统&#xff0c;tomcat服务器&#xff0c;端口号&#xff1a;8081 4.war包部署位置&#xff1a;tomcat-8081/webapps/temp.war 5.服务名为&#xff1a;t…

Java版本企业电子招投标采购系统源码——功能模块功能描述+数字化采购管理 采购招投标

功能模块&#xff1a; 待办消息&#xff0c;招标公告&#xff0c;中标公告&#xff0c;信息发布 描述&#xff1a; 全过程数字化采购管理&#xff0c;打造从供应商管理到采购招投标、采购合同、采购执行的全过程数字化管理。通供应商门户具备内外协同的能力&#xff0c;为外部供…

Postman轻松签名,让SHA256withRSA保驾护航!

postman接口签名教程&#xff1a;https://www.bilibili.com/video/BV1r14y1A7MQ/? 目录&#xff1a;导读 前言 获取pmlib 引入依赖bundle.js&#xff0c;有以下两种方式&#xff1a; 使用Pre-request Script对请求进行加签(具体加签字段请看自己项目) 结语 前言 在接口测…

荔枝派Zero(全志V3S)开启alsa,测试codec

文章目录 前言一、ALSA 简介二、ALSA 框架三、buildroot 配置四、烧录到 SD 卡五、测试1、查看 CODEC 设备2、alsa-utils 使用①、查看设备②、调节音量③、查看控制器④、录音测试⑤、播放测试 前言 默认 dts 中使能了 codec 需要使用的话&#xff0c;在 buildroot 中勾选 a…

2023年五月份图形化四级打卡试题

活动时间 从2023年5月1日至5月21日&#xff0c;每天一道编程题。 本次打卡的规则如下&#xff1a; 小朋友每天利用10~15分钟做一道编程题&#xff0c;遇到问题就来群内讨论&#xff0c;我来给大家答疑。 小朋友做完题目后&#xff0c;截图到朋友圈打卡并把打卡的截图发到活动群…

开放式耳机有什么好处,盘点几款性能不错的开放式耳机

随着人们对生活质量要求的提高&#xff0c;大家在运动的时候都喜欢戴上耳机&#xff0c;享受运动的乐趣。但是传统耳机戴久了之后就会出现耳朵酸痛的情况&#xff0c;这是因为传统耳机佩戴方式是通过空气振动来传递声音&#xff0c;而人在运动时就会伴随着大量的汗水&#xff0…

基于ResNet-attention的负荷预测

一、attention机制 注意力模型最近几年在深度学习各个领域被广泛使用&#xff0c;无论是图像处理、语音识别还是自然语言处理的各种不同类型的任务中&#xff0c;都很容易遇到注意力模型的身影。从注意力模型的命名方式看&#xff0c;很明显其借鉴了人类的注意力机制。我们来看…

GB/T28181-2022相对2016版“基于TCP协议的视音频媒体传输要求“规范解读和技术实现

规范解读 GB/T28181-2022和GB/T28181-2016规范&#xff0c;有这么一条“更改了附录 D 基于 TCP 协议的视音频媒体传输要求&#xff08;见附录 D&#xff0c;2016 年版的附录 L&#xff09;。”。 本文主要是针对GB/T28181-2022里面提到的“基于 TCP 协议的视音频媒体传输要求…

chmod 命令 (chmod 0660)

chmod的作用: 用于设置文件所有者和文件关联组的命令,就是控制用户的权限命令 注意事项: chown 需要超级用户 root 的权限才能执行此命令。 自己常用chmod 命令是 chmod 777 * 给所有文件权限 chmod 777 文件名 给单独文件权限 这个777 是怎么来的, 或者chmod 0660 这…

Servlet路径问题(“/“到底代表什么)-“web应用程序的根目录“与“web站点的根目录“

JavaWeb——Servlet路径问题("/"到底代表什么) JavaWeb——Servlet路径问题("/"到底代表什么) 在JavaWeb中&#xff0c;使用路径时出现了大量的"/“&#xff0c;而每次使用”“时都感觉其代表的含义是不同的&#xff0c;因此&#xff0c;本篇文章将汇…

分布式ID解决方案对比

在复杂的分布式系统中&#xff0c;往往需要对大量的数据进行唯一标识&#xff0c;比如在对一个订单表进行了分库分表操作&#xff0c;这时候数据库的自增ID显然不能作为某个订单的唯一标识。除此之外还有其他分布式场景对分布式ID的一些要求&#xff1a; 趋势递增&#xff1a; …

(栈和队列) 232. 用栈实现队列 ——【Leetcode每日一题】

❓232. 用栈实现队列 难度&#xff1a;中等 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作&#xff08;push、pop、peek、empty&#xff09;&#xff1a; 实现 MyQueue 类&#xff1a; void push(int x) 将元素 x 推到队列的末尾int pop() 从队列…

Jenkins之allure测试报告邮件通知

Jenkins入门教程&#xff1a;Jenkins入门 一般情况下&#xff0c;我们想让Jenkins将构建结果或构建中的代码执行结果&#xff08;比如跑批数据&#xff09;通知我们&#xff0c;这个是构建后的操作。 经常用的是邮件通知 邮件通知基础 Jenkins自带了邮件通知配置&#xff0c…

灌区信息化和灌区自动化监测方案

一、方案背景 以提升粮食综合生产能力和水资源高效利用作为现代化改造的目标&#xff0c;把国家粮食安全和节水放在首位&#xff1b;以完善灌区工程设施体系和管理体系作为现代化改造的基础&#xff0c;以现代信息技术应用、智慧灌区建设作为提高管理能力和服务水平的手段&…

windows 文件夹目录过长超过长度259字符 文件打不开

当路径超过260个字符时&#xff0c;Windows操作系统就无法处理文件或文件夹&#xff0c;并且无法打开或重命名。这是因为Windows系统使用的文件系统&#xff0c;即FAT和NTFS文件系统&#xff0c;都有最大路径限制。NTFS文件系统最大长度限制为32767个字符&#xff0c;但操作系统…

算法训练day4:栈与队列

那么我这里再列出四个关于栈的问题&#xff0c;大家可以思考一下。以下是以C为例&#xff0c;使用其他编程语言的同学也对应思考一下&#xff0c;自己使用的编程语言里栈和队列是什么样的。 C中stack 是容器么&#xff1f;我们使用的stack是属于哪个版本的STL&#xff1f;我们…

FPGA基于XDMA实现PCIE X8采集AD9226数据 提供工程源码和QT上位机程序和技术支持

目录 1、前言2、我已有的PCIE方案3、PCIE理论4、总体设计思路和方案5、vivado工程详解6、驱动安装7、QT上位机软件8、上板调试验证9、福利&#xff1a;工程代码的获取 1、前言 PCIE&#xff08;PCI Express&#xff09;采用了目前业内流行的点对点串行连接&#xff0c;比起 PC…

就这些了, 常见 6 款API 文档工具推荐

8年开发经验&#xff0c;想分享一下我接触到这些 API 文档工具&#xff1a; Swagger: Swagger 是一个开源的 API 文档管理工具&#xff0c;可以通过注解自动生成 API 文档&#xff0c;并提供交互式 UI 和 API 调试功能。 Swagger 支持多种语言和格式&#xff0c;包括 Java、Pyt…

Linux没网络的情况下快速安装依赖或软件(挂载本地yum仓库源(Repository))

一、上传iso系统镜像&#xff1a; 上传和系统同一版本、同一位数&#xff08;32bit&#xff1a;i686或i386/64bit:x86_64&#xff09;的系统&#xff0c;不能是Minimal版本&#xff0c;可以是DVD&#xff08;较全&#xff09;或everything&#xff08;最全&#xff09;。 注&am…