Spring Security基于表单登陆的源码分析

本文深入分析了Spring Security基于表单登录的源码,包括登录信息封装、认证处理流程和具体实现。重点讲解了UsernamePasswordAuthenticationFilter如何封装登录信息,ProviderManager如何选择合适的认证器DaoAuthenticationProvider进行认证,以及DaoAuthenticationProvider如何从数据库获取用户并进行身份验证。
摘要由CSDN通过智能技术生成

 

                                Spring Security 基于表单登录的源码分析

 

一.思维导图

二.原理分析

  首先我们先来看一下 Spring Security 的表单认证的流程图:

从流程图中我们可以不难看出,整个认证流程大致上分为3个模块:

   1.登录信息的封装

   2.认证处理

   3.结果处理(成功&失败处理)

 其中最核心模块为认证模块,下面我们来看看认证模块 AuthenticationManager的相关类图:

该图可以分为两部分来看,分别是左边负责掌控全局的"大哥",以及右边勤勤恳恳的"小弟们"。"大哥" AuthenticationManager 认证管理接口,只定义了认证方法 authenticate(),具体咋实现由右边小弟负责

ProviderManager 为认证管理类,实现了 AuthenticationManager ,并在认证方法 authenticate() 中将身份认证委托给具有认证资格的 AuthenticationProvider;同时ProviderManaer 有一个成员变量 List<AuthenticationProvider> providers 用以存储了所有具体执行认证的具体操作。

接下来介绍一下右边勤勤恳恳的"小弟们",首先是AuthenticationProvider认证接口类,其定义了身份认证方法authenticate();这个也比较好理解;你怎么证明自己是我的"小弟"呢?当然是得入我门为我干活拉!AuthenticationProvider接口就是起这个作用。

AbstractUserDetailAuthenticationProvider为认证抽象类,实现了接口AuthenticationProvider,同时还定义了抽象方法retrieveUser()用于从数据库中获取用户信息,以及additionalAuthenticationChecks()做身份认证;这块可能会不太好理解,为啥子这个"小弟"还是个抽象类呢?不必慌张,其实只是为了一些功能的复用。

DaoAuthenticationProvider认证类继承于AbstractUserDetailAuthenticationProvider抽象认证类,实现了上面提到的2个抽象方法retrieveUser和additionalAuthenticationChecks;并自定义了一些成员变量:private UserDetailsService userDetailsService; 用以用户信息查询,以及private PasswordEncoder passwordEncoder 用作密码的加密认证。

三.源码解析

在大致了解了它的原理之后,我们开始阅读源码;主要分两个模块来看,分别是:登录信息的封装以及认证处理

1.登录信息的封装

登录信息的封装是指将前端传递的username和password封装成 UsernamePasswordAuthenticationToken

UsernamePasswordAuthenticationFilter.class的 attemptAuthentication()方法

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    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();
        // 将http请求的Request带的认证参数:username、password转换为认证的token对象
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        // 设置一些详细信息, 诸如发送请求的ip等...
        this.setDetails(request, authRequest);
        // 调用AuthenticationManager的authenticate方法 执行认证
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

attemptAuthentication() 方法做的事情很简单,主要是将登录信息 username和password 封装成 UsernamePasswordAuthenticationToken。那么这个Token 到底是起什么作用呢?其实也很简单,主要是用于后续认证的时候,寻找匹配的认证处理器,例如表单登录的 UsernamePasswordAuthenticationToken 会唯一匹配相应的认证Provider

2.认证处理

从上面我们也可以看到,在将登录信息封装成Token 后,就调用了 AuthenticationManager 的 authenticate() 方法执行认证操作;因 AuthenticationManager是一个接口,我们来分析它的实现类 ProviderManager

ProviderManager.class的authenticate()方法

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    Authentication result = null;
    boolean debug = logger.isDebugEnabled();

    // 获取所有干活的“小弟” providers 认证器
    Iterator var6 = this.getProviders().iterator();

    // 挨个遍历,找到能支持当前登录方式(表单登录---由token来区分)的认证器
    while(var6.hasNext()) {
        AuthenticationProvider provider = (AuthenticationProvider)var6.next();
        // 之前我们介绍过 AuthenticationProvider 接口,里面定义的supports方法,就是用于判定一个provider支持那种类型的认证方式
        if (provider.supports(toTest)) {
            if (debug) {
                logger.debug("Authentication attempt using " + provider.getClass().getName());
            }

            // 匹配到对应的provider后,调用provider的authenticate方法进行认证
            try {
                result = provider.authenticate(authentication);
                if (result != null) {
                    // 认证成功,copy一些细节的参数到认证对象上
                    this.copyDetails(authentication, result);
                    break;
                }
            } catch (AccountStatusException var11) {
                this.prepareException(var11, authentication);
                throw var11;
            } catch (InternalAuthenticationServiceException var12) {
                this.prepareException(var12, authentication);
                throw var12;
            } catch (AuthenticationException var13) {
                lastException = var13;
            }
        }
    }

    if (result == null && this.parent != null) {
        try {
            result = this.parent.authenticate(authentication);
        } catch (ProviderNotFoundException var9) {
        } catch (AuthenticationException var10) {
            lastException = var10;
        }
    }

    if (result != null) {
        if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
            ((CredentialsContainer)result).eraseCredentials();
        }

        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}"));
        }

        this.prepareException((AuthenticationException)lastException, authentication);
        throw lastException;
    }
}

ProviderManager 的 authenticate() 方法理解起来也不是很困难,目的性十分的明确;首先是找到所有的认证器,挨个遍历根据Token进行匹配,如果匹配成功则进行认证。本文分析的是表单登录,所以根据UsernamePasswordAuthenticationToken 匹配到的 Provider是 DaoAuthenticationProvider

DaoAuthenticationProvider.class的 authenticate()方法 (PS: DaoAuthenticationProvider继承于抽象类 AbstractUserDetailsAuthenticationProvider,自身并无authenticate())

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // 前置检查 该provider只支持 UsernamePasswordAuthenticationToken的认证方式
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
        this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));

    String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
    boolean cacheWasUsed = true;
    // 尝试从缓存中获取用户信息
    UserDetails user = this.userCache.getUserFromCache(username);

    if (user == null) {
        cacheWasUsed = false;

        // 从缓存中获取不到用户信息, 调用子类 DaoAuthenticationProvider的retrieveUser方法,从数据库中加载用户信息
        try {
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
        } catch (UsernameNotFoundException var6) {
            this.logger.debug("User '" + username + "' not found");
            if (this.hideUserNotFoundExceptions) {
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }

            throw var6;
        }

        Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
    }

    try {
        // 预检查,之前我们介绍UserDetails的时候,有提到过几个方法,例如判断账号是否可用、账号是否过期等...
        this.preAuthenticationChecks.check(user);
        // 认证操作, 调用子类DaoAuthenticationProvider实现的additionalAuthenticationChecks进行认证
        this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
    } catch (AuthenticationException var7) {
        if (!cacheWasUsed) {
            throw var7;
        }

        cacheWasUsed = false;
        user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
        this.preAuthenticationChecks.check(user);
        this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
    }

    this.postAuthenticationChecks.check(user);
    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }

    Object principalToReturn = user;
    if (this.forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }

    return this.createSuccessAuthentication(principalToReturn, authentication, user);
}

阅读代码我们可以看出,首先先尝试用缓存中获取用户,当从缓存中获取不到用户的时候,调用子类DaoAuthenticationProvider 实现的 retrieveUser() 方法,从数据库中加载用户信息,具体代码如下:

DaoAuthenticationProvider.class的 reretrieveUser()方法

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    // 检查passwordEncoder
    this.prepareTimingAttackProtection();

    try {
        // UserDetailsService的loadUserByUsername方法,根据用户名从数据库中获取用户信息,是不是很熟悉~~~
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
        } else {
            return loadedUser;
        }
    } catch (UsernameNotFoundException var4) {
        this.mitigateAgainstTimingAttack(authentication);
        throw var4;
    } catch (InternalAuthenticationServiceException var5) {
        throw var5;
    } catch (Exception var6) {
        throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
    }
}

private void prepareTimingAttackProtection() {
    if (this.userNotFoundEncodedPassword == null) {
        this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
    }

}

当加载完用户信息,进行预检查后,就调用子类DaoAuthenticationProvider.class的additionalAuthenticationChecks() 进行最终的认证校验

DaoAuthenticationProvider.class的additionalAuthenticationChecks()方法

protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    // 认证请求的密码非空判断
    if (authentication.getCredentials() == null) {
        this.logger.debug("Authentication failed: no credentials provided");
        throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    } else {
        // 调用passwordEncoder的matches匹配方法,判断前端传递的密码和从数据库load出来的密码是否匹配
        String presentedPassword = authentication.getCredentials().toString();
        if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            this.logger.debug("Authentication failed: password does not match stored value");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赫赫有安

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值