认证源码分析与自定义后端认证逻辑

简介: 本文深入分析Spring Security认证流程,从UsernamePasswordAuthenticationFilter入手,解析用户认证请求的处理过程,逐步揭示AuthenticationManager、AuthenticationProvider及UserDetailsService等核心组件的协作机制,帮助理解如何基于源码实现自定义认证逻辑。

1.认证流程分析

UsernamePasswordAuthenticationFilter

先看主要负责认证的过滤器UsernamePasswordAuthenticationFilter,有删减,注意注释。

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
{
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    private String usernameParameter = "username";
    private String passwordParameter = "password";
    private boolean postOnly = true;
    
    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }
    
    public Authentication attemptAuthentication(HttpServletRequest request, 
                                                HttpServletResponse response) throws AuthenticationException {
        //必须为POST请求
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " +
                                                     request.getMethod());
        } else {
            
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            
            if (username == null) {
                username = "";
            }
            
            if (password == null) {
                password = "";
            }
            
            username = username.trim();
            
            //将填写的用户名和密码封装到了UsernamePasswordAuthenticationToken中
            UsernamePasswordAuthenticationToken authRequest = new
            UsernamePasswordAuthenticationToken(username, password);
            
            this.setDetails(request, authRequest);
            //调用AuthenticationManager对象实现认证
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
}

AuthenticationManager

由上面源码得知,真正认证操作在AuthenticationManager里面!

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
    
    private static final Log logger = LogFactory.getLog(ProviderManager.class);
    private AuthenticationEventPublisher eventPublisher;
    private List<AuthenticationProvider> providers;
    protected MessageSourceAccessor messages;
    private AuthenticationManager parent;
    private boolean eraseCredentialsAfterAuthentication;
    
    //注意AuthenticationProvider这个对象,SpringSecurity针对每一种认证,什么qq登录啊,
    //用户名密码登陆啊,微信登录啊都封装了一个AuthenticationProvider对象。
    public ProviderManager(List<AuthenticationProvider> providers) {
        this(providers, (AuthenticationManager)null);
    }
    
    public Authentication authenticate(Authentication authentication) throws
    AuthenticationException {
        
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();
        Iterator var8 = this.getProviders().iterator();
        
        //循环所有AuthenticationProvider,匹配当前认证类型。
        while(var8.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var8.next();
            if (provider.supports(toTest)) {
                if (debug) {
                    logger.debug("Authentication attempt using " +
                                 provider.getClass().getName());
                }
                try {
                    //找到了对应认证类型就继续调用AuthenticationProvider对象完成认证业务。
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (AccountStatusException var13) {
                    this.prepareException(var13, authentication);
                    throw var13;
                } catch (InternalAuthenticationServiceException var14) {
                    this.prepareException(var14, authentication);
                    throw var14;
                } catch (AuthenticationException var15) {
                    lastException = var15;
                }
            }
        }
        
        if (result == null && this.parent != null) {
            try {
                result = parentResult = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var11) {
            } catch (AuthenticationException var12) {
                parentException = var12;
                lastException = var12;
            }
        }
        
        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && result instanceof
                CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }
            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }
            return result;
        } else {
            if (lastException == null) {
                lastException = new
                ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new
                                                                   Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }
            if (parentException == null) {
                this.prepareException((AuthenticationException)lastException, authentication);
            }
            throw lastException;
        }
    }
}

AbstractUserDetailsAuthenticationProvider

咱们继续再找到AuthenticationProvider的实现类AbstractUserDetailsAuthenticationProvider:

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
    private PasswordEncoder passwordEncoder;
    private volatile String userNotFoundEncodedPassword;
    private UserDetailsService userDetailsService;
    private UserDetailsPasswordService userDetailsPasswordService;
    protected final UserDetails retrieveUser(String username,
                                             UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();
        try {
            //重点来了!主要就在这里了!
            //可别忘了,咱们为什么要翻源码,是想用自己数据库中的数据实现认证操作啊!
            //UserDetails就是SpringSecurity自己的用户对象。
            //this.getUserDetailsService()其实就是得到UserDetailsService的一个实现类
            //loadUserByUsername里面就是真正的认证逻辑
            //也就是说我们可以直接编写一个UserDetailsService的实现类,告诉SpringSecurity就可以了!
            //loadUserByUsername方法中只需要返回一个UserDetails对象即可
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            //若返回null,就抛出异常,认证失败。
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned
                                                                 null, which is an interface contract violation");
            } else {
                //若有得到了UserDetails对象,返回即可。
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }
}

AbstractUserDetailsAuthenticationProvider

按理说到此已经知道自定义认证方法的怎么写了,但咱们把返回的流程也大概走一遍,上面不是说到返回了一个 UserDetails对象对象吗?

跟着它就又回到AbstractUserDetailsAuthenticationProvider对象中authenticate方法最后一行了。

public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
   
    public Authentication authenticate(Authentication authentication) throws
    AuthenticationException {
        //最后一行返回值,调用了createSuccessAuthentication方法,此方法就在下面!
        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }
    
    //咿!?怎么又封装了一次UsernamePasswordAuthenticationToken,开局不是已经封装过了吗?
    protected Authentication createSuccessAuthentication(Object principal, 
                                                         Authentication authentication, 
                                                         UserDetails user) {
        //那就从构造方法点进去看看,这才干啥了。
        UsernamePasswordAuthenticationToken result = new
        UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(),
                                            this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        return result;
    }
}


相关文章
|
5月前
|
存储 安全 Java
Java泛型类型擦除以及类型擦除带来的问题
Java泛型在编译时会进行类型擦除,所有泛型信息被移除,替换为原始类型(如Object或限定类型)。这导致List&lt;String&gt;和List&lt;Integer&gt;在运行时等价于List,引发反射可插入不兼容类型等问题。同时,泛型不支持基本数据类型、静态上下文中使用受限,且instanceof无法用于泛型类型判断。编译器通过桥接方法等机制解决多态冲突,并在获取元素时自动插入类型转换,保证类型安全。
|
5月前
|
存储 缓存 安全
One Trick Per Day
本文详解Java开发中六大易被忽视的陷阱:Map初始化容量设置不当、线程池滥用导致OOM、Arrays.asList的不可变性、遍历Map性能优化、SimpleDateFormat线程安全问题及并发更新加锁策略。通过源码分析与实例演示,揭示问题根源并提供Guava等最佳实践方案,助力提升系统稳定性与性能。
|
5月前
|
监控 Java 测试技术
OOM排查之路:一次曲折的线上故障复盘
本文复盘了线上服务因Paimon与RocksDB集成引发的三次OOM故障。从线程激增到堆外内存泄漏,排查过程曲折复杂,最终定位到SDK中RocksDB通过JNI申请内存未释放的根本原因,并通过架构优化解决。分享了MAT、NMT、async-profiler等工具的实战经验,总结了一套系统性的内存问题排查思路,为类似技术栈提供借鉴。
|
5月前
|
存储 缓存 监控
EFC&CTO:缓存引发数据不一致问题排查与深度解析
EFC缓存架构更新后,在CTO测试中出现数据不一致问题。经排查,因分布式缓存版本号回退,导致旧NULL数据被读入pagecache并刷入文件系统,破坏了正常数据。通过维护递增版本号修复,10轮测试验证无误。
 EFC&CTO:缓存引发数据不一致问题排查与深度解析
|
5月前
|
安全 Java 测试技术
从Google线上故障,谈灰度发布的重要性
2025年6月12日,Google Cloud因未灰度发布的配置缺陷导致全球服务中断7小时。本文分析其根因为空指针异常,并详解Nacos等配置中心的灰度发布方案,强调通过IP、标签、流量等多路径实现安全配置变更,提升系统稳定性。
|
5月前
|
消息中间件 监控 Java
RocketMQ:底层Netty频繁OS OOM
本文记录了一例Java应用因Netty多ClassLoader加载导致堆外内存超限引发OS OOM的排查过程。通过NMT、Arthas等工具分析,发现多个中间件独立加载PooledByteBufAllocator,各自绕过MaxDirectMemorySize限制,累计占用远超1G堆外内存。最终定位RocketMQ客户端为主要内存使用者,建议短期调小Java堆以腾出空间,并推动中间件优化。
|
5月前
|
Java 关系型数据库 MySQL
[MES]数据库改造H2到MySQL(☆☆)
本文介绍如何运行并改造一个SpringBoot项目,包括从Gitee克隆代码、环境配置(JDK/Maven)、数据库由H2迁移至MySQL的步骤。强调新人如何高效请教同事、快速适应技术栈,掌握Git、Maven、MyBatis等核心技术,提升实战能力,助力入职后迅速进入角色。
 [MES]数据库改造H2到MySQL(☆☆)
|
5月前
|
自然语言处理 fastjson Java