从源码分析Spring Security的登录实现

Spring Security主要实现是认证器和决策器,前者处理登陆验证,后者处理访问资源的控制

Spring Security的登陆请求处理如图

下面来分析一下是怎么实现登录的

拦截请求

登陆请求,默认会被UsernamePasswordAuthenticationFilter拦截,这个过滤器看名字就知道是一个拦截用户名密码的拦截器, Spring Security的默认登录实现

拦截的作用------创建Authentication上下文类

主要的验证是在attemptAuthentication()方法里,他会去获取在请求中的用户名密码,并且创建一个该用户的上下文UsernamePasswordAuthenticationToken,然后在去执行一个验证过程

java 复制代码
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
//创建上下文
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);

可以看看UsernamePasswordAuthenticationToken这个类,他是继承了AbstractAuthenticationToken,然后这个父类实现了Authentication

由这个类的方法和属性可得知他就是存储用户验证信息的,认证器的主要功能应该就是验证完成后填充这个类

UsernamePasswordAuthenticationToken的构造方法

回到UsernamePasswordAuthenticationToken中,这个类有两个构造方法,一个是是刚开始认证调用,一个是认证完成调用

java 复制代码
    public UsernamePasswordAuthenticationToken(Object principal,Object credentials){
        super(null);
        this.principal=principal;
        this.credentials=credentials;
        //还没认证
        setAuthenticated(false);
    }

刚开始调用,会将权限相关的属性设置为null,比如上边的super(null)

java 复制代码
//super(null),初始化一个空权限集合
if (authorities == null) {
    this.authorities = AuthorityUtils.NO_AUTHORITIES;
    return;
}

验证完成后,会调用另外一个构造方法

java 复制代码
//认证完成
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
    Collection<? extends GrantedAuthority> authorities) {
    //将认证的信息填充到上下文
    super(authorities);
    this.principal = principal;
    this.credentials = credentials;
    //完成认证
    super.setAuthenticated(true);  
}

主要的区别是super.setAuthenticated()

验证过程

怎么去填充Authentication

那么UsernamePasswordAuthenticationToken这个上下文类,由谁去填充的,从拦截器那边的调用

kotlin 复制代码
return this.getAuthenticationManager().authenticate(authRequest);

可以看到是下一步到AuthenticationManager中,他是一个接口

java 复制代码
/**
 * Attempts to authenticate the passed {@link Authentication} object, returning a
 * fully populated <code>Authentication</code> object (including granted authorities)
 * if successful.
 **/
public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}

注释也写的很清楚了,认证完成后会填充Authentication

通过debug查看调用栈,找到他的实现类ProviderManager

由AuthenticationManager委托给AuthenticationProvider

首先看看他的主要属性和构造方法,主要是设置AuthenticationProvider

csharp 复制代码
//初始化AuthenticationProvider集合
private List<AuthenticationProvider> providers = Collections.emptyList();

//设置AuthenticationProvider集合
public ProviderManager(List<AuthenticationProvider> providers) {
        this(providers, null);
}

看一下这个类,可以得出一个结论,AuthenticationAuthenticationProvider是成对存在的,不同的Authentication由特定的AuthenticationProvider处理,这点对如何实现自定义登录很重要

java 复制代码
/**
 * 处理特定Authentication的类
 **/
public interface AuthenticationProvider {

    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
            
    //支持特定类型的authentication
    boolean supports(Class<?> authentication);
}

通过这个关系,可以找到UsernamePasswordAuthenticationToken对应的AuthenticationProvider

typescript 复制代码
public abstract class AbstractUserDetailsAuthenticationProvider implements
		AuthenticationProvider, InitializingBean, MessageSourceAware {              
          ....
     	public boolean supports(Class<?> authentication) {
		return (UsernamePasswordAuthenticationToken.class
				.isAssignableFrom(authentication));
	}
 }

通过遍历AuthenticationProvider集合,找到对应的处理类

然后看回ProviderManager,主要看实现的authenticate()方法,主要代码是遍历了List<AuthenticationProvider>集合,然后根据Authentication找到对应的AuthenticationProvider

scss 复制代码
public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
              
         //取出上下文对象,就是上边提到的UsernamePasswordAuthenticationToken
         Class<? extends Authentication> toTest = authentication.getClass();     
         ...
         for (AuthenticationProvider provider : getProviders()) {
             //是否匹配
             if (!provider.supports(toTest)) {
                    continue;
             }
             ...
             //调用匹配的provider
             result = provider.authenticate(authentication);
             //填充上下文
             if (result != null) {
                     //将返回的result,复制给当前的authentication中
                    copyDetails(authentication, result);
                    break;
             }
         
         }
}

AbstractUserDetailsAuthenticationProvider委托给子类处理

如果找到了对应的provider,会调用provider的authenticate方法,拿UsernamePasswordAuthenticationToken对应的AbstractUserDetailsAuthenticationProvider举例

java 复制代码
public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
          ....
          //从上下文中获取username
          String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();
                                
          //调用retrieveUser去验证用户                  
          user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
}

这里的retrieveUser是抽象方法,所以AbstractUserDetailsAuthenticationProvider将验证用户的方法又抽取出来,给子类去实现,这种实现方式减少各个组件之间的耦合度,增加系统的灵活性和可扩展性

DaoAuthenticationProvider调用验证用户的service

通过继承关系可以知道子类是DaoAuthenticationProvider,里边的实现,又继续调用验证用户的service

ini 复制代码
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

接触过 Spring Security框架的,应该熟悉UserDetailsService这个类,这个类是Spring Security提供给我们扩展的类,通常会去实现这个类来自定义查询用户的方式

也就是说这是个会变化的类,因为他可能会有多个子类,Spring Security将这种变化又抽取出来

loadUserByUsername()方法是我们自己要实现的逻辑,验证完成后返回UserDetails类,UserDetails类中有一些显示用户状态的属性,可以根据这个属性来做一些权限处理,如果字段不够用,可以继承这个类来实现

arduino 复制代码
public interface UserDetails{
    //权限集合,我这里没有用的,初始化空集合,AuthorityUtils.NO_AUTHORITIES
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    //账号是否过期
    boolean isAccountNonExpired();
    //账号被锁定或解锁状态。
    boolean isAccountNonLocked();
    //密码是否过期
    boolean isCredentialsNonExpired();
    //是否启用
    boolean isEnabled();

}
      

验证完成的后续

回到AbstractUserDetailsAuthenticationProvider中,这此时,我们已经经过了几层的调用

scss 复制代码
AbstractUserDetailsAuthenticationProvider
    |->retrieveUser()
        |->DaoAuthenticationProvider
            |->retrieveUser()
                |->UserDetailsService
                    |->loadUserByUsername()

当最后retrieveUser()方法返回user后,会对user对象做一些校验

scss 复制代码
//验证用户状态,就是验证UserDetails中的账号属性,禁用就返回
preAuthenticationChecks.check(user);
//验证前端的密码是否匹配
additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);

在源码中,还有对缓存的处理,如果是由于账号状态或者密码不匹配的问题抛出异常,捕获后判断是否开启了用户缓存,是的话就会再去查询一遍,如果这次通过的话,会将新的信息放到缓存中

返回完整的Authentication

验证完成后,会创建新的Authentication,并填充数据, Spring Security会将新创建的Authentication,复制到当前的会创建新的Authentication中copyDetails(authentication, result);

java 复制代码
protected Authentication createSuccessAuthentication(Object principal,
 
    UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(),
				authoritiesMapper.mapAuthorities(user.getAuthorities()));
    result.setDetails(authentication.getDetails());

    return result;
}

注意看这次创建UsernamePasswordAuthenticationToken使用的构造方法,使用的是另外一个,将权限集合传入,这个构造方法的关键是super.setAuthenticated(true);,表示验证完成

Authentication会将权限集合转换为只读集合

java 复制代码
    for (GrantedAuthority a : authorities) {
        if (a == null) {
            throw new IllegalArgumentException(
            "Authorities collection cannot contain any null elements");
        }
    }
    ArrayList<GrantedAuthority> temp = new ArrayList<>(
    authorities.size());
    temp.addAll(authorities);
    //只读
    this.authorities = Collections.unmodifiableList(temp);

至此,我们已经完成了对Authentication的验证和填充,最后返回到UsernamePasswordAuthenticationFilter中通过过滤

最后再看下整个调用链路

总结

通过对源码的分析,发现Spring Security很多地方都是模块化设计,通过一个高层接口一步一步的调用,即减少了耦合度,也增强了扩展性,将会发生变化的类抽取出来的这种开闭原则的设计非常值得学习

整个登录实现都是围绕着填充和验证Authentication,如果想要自定义登录,通过实现自己的上下文Authentication和处理类AuthenticationProvider就可以实现了

相关推荐
LuckyLay22 分钟前
Spring学习笔记_34——@Controller
spring·controller
ApiHug2 小时前
ApiSmart x Qwen2.5-Coder 开源旗舰编程模型媲美 GPT-4o, ApiSmart 实测!
人工智能·spring boot·spring·ai编程·apihug
背水2 小时前
初识Spring
java·后端·spring
闲人一枚(学习中)3 小时前
spring -第十四章 spring事务
java·数据库·spring
wclass-zhengge4 小时前
SpringCloud篇(注册中心 - Eurea)
后端·spring·spring cloud
小蒜学长5 小时前
springboot基于SpringBoot的企业客户管理系统的设计与实现
java·spring boot·后端·spring·小程序·旅游
海无极6 小时前
EDUCODER头哥 SpringBoot 异常处理
java·spring boot·spring
.生产的驴7 小时前
SpringBootCloud 服务注册中心Nacos对服务进行管理
java·spring boot·spring·spring cloud·tomcat·rabbitmq·java-rabbitmq
Wx-bishekaifayuan8 小时前
springboot市社保局社保信息管理与分析系统-计算机设计毕业源码03479
java·css·spring boot·spring·spring cloud·servlet·guava
一叶飘零_sweeeet9 小时前
Eureka、Zookeeper 与 Nacos:服务注册与发现功能大比拼
spring·zookeeper·eureka·nacos