从源码分析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就可以实现了

相关推荐
萧瑟余晖4 小时前
Java深入解析篇十七之Spring Security
java·开发语言·spring
Flittly10 小时前
【雕虫大技】Agent 动态 Skill 供应链安全加固(二):执行隔离沙箱实战
java·spring boot·spring
BUG指挥官1 天前
Sa-Token和Spring Security对比
java·后端·spring
geminigoth1 天前
Spring AI Alibaba 入门开发一(备份)
java·人工智能·spring
xbgRS1 天前
spring整合mybaits
java·spring·mybatis
ruleslol1 天前
静态依赖注入 VS 动态依赖查询
spring
墨雨晨曦882 天前
Spring AI总结
java·人工智能·spring
用户3126874877202 天前
Spring Boot 异常处理到底怎么玩的?从 DispatcherServlet 到全局兜底的全链路拆解
spring
不才不才不不才2 天前
Spring 源码系列(17): HandlerMapping 与 HandlerAdapter 两大体系
java·后端·spring
(轻舟已过万重山)2 天前
第40章 Spring AI 实战:企业级 AI 应用架构
人工智能·spring·架构