Spring Security登录认证源码分析

news/2024/7/1 18:27:29

Security是基于过滤器链实现认证授权的,它支持不同的认证机制,这里我们用户名密码认证机制。

Spring Security 提供了以下内置机制来从 读取用户名和密码:

  • Form Login
  • Basic Authentication
  • Digest Authentication

用户名密码存储机制:

  • 具有内存中身份验证的简单存储
  • 具有JDBC 身份验证的关系数据库
  • 使用UserDetailsService自定义数据存储
  • 带有LDAP 身份验证的LDAP 存储

一、认证流程图

图来自网络
在这里插入图片描述
分析认证过程:

  1. 当用户提交他们的用户名和密码,则UsernamePasswordAuthenticationFilter创建一个UsernamePasswordAuthenticationToken其是一种类型的Authentication通过提取从所述用户名和密码HttpServletRequest。
  2. 过滤器将UsernamePasswordAuthenticationToken提交至认证管理器(AuthenticationManager)进行认证
  3. 如果身份验证失败,则失败,该SecurityContextHolder将被清除出去。RememberMeServices.loginFail被调用。如果记住我没有配置,这是一个空操作。最终AuthenticationFailureHandler 被调用。
  4. 如果认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除) Authentication 实例。该认证被设置在SecurityContextHolder中。RememberMeServices.loginSuccess被调用。如果记住我没有配置,这是一个空操作。ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent。在AuthenticationSuccessHandler被调用。

二、源码分析

1、查看 UsernamePasswordAuthenticationFilter 过滤器

UsernamePasswordAuthenticationFilter 过滤器,主要用于认证操作,默认匹配 URL为 /login且必须为POST请求。
在这里插入图片描述
看到源码,我们应该明白,为什么默认是 post请求,url为/login,并且用户名和密码的参数名称为它。

在 attemptAuthentication方法中,主要做了两件事:

  1. 将填写的用户名和密码封装到了UsernamePasswordAuthenticationToken中
  2. 调用AuthenticationManager对象实现认证

1.1 用户名和密码封装到了UsernamePasswordAuthenticationToken中

将填写的用户名和密码封装到了 UsernamePasswordAuthenticationToken中
在这里插入图片描述

1.2 调用AuthenticationManager对象实现认证

在这里插入图片描述
由源码得知,真正认证逻辑是在 AuthenticationManager接口的 authenticate方法。接着看AuthenticationManager的实现类 ProviderManager类的 authenticate方法。

2、查看 ProviderManager类

源码如下:

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		// 获取传入的Authentication 类型
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		int currentPosition = 0;
		int size = this.providers.size();
		// 循环AuthenticationProvider对象,Spring Security针对每一种认证,比如第三方登录,用户名密码登录等,都封装了一个 AuthenticationProvider对象。
		for (AuthenticationProvider provider : getProviders()) {
			// 1.判断是否支持当前认证方式
			if (!provider.supports(toTest)) {
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
						provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
				// 2.调用找到支持的 AuthenticationProvider对象进行认证逻辑
				result = provider.authenticate(authentication);
				if (result != null) {
					// 3.执行 authentication details 的拷贝逻辑
					copyDetails(authentication, result);
					break;
				}
			} catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				// 4.如果发生 AccountStatusException 或 InternalAuthenticationServiceException 异常,则会通过 Spring事件发布器AuthenticationEventPublisher 发布异常事件。
				prepareException(ex, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw ex;
			} catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			} catch (ProviderNotFoundException ex) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			} catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		if (result != null) {
			if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}
			// If the parent AuthenticationManager was attempted and successful then it
			// will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent
			// AuthenticationManager already published it
			if (parentResult == null) {
				this.eventPublisher.publishAuthenticationSuccess(result);
			}

			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).
		if (lastException == null) {
			lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
					new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
		}
		// If the parent AuthenticationManager was attempted and failed then it will
		// publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
		// parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}
		throw lastException;
	}

2.1 查看 AbstractUserDetailsAuthenticationProvider类

AbstractUserDetailsAuthenticationProvider是针对表单用户名密码登录进行认证的 AuthenticationProvider对象。
源码如下:

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
            return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
        });
        //1.获取用户名,此时封装在UsernamePasswordAuthenticationToken中
        String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
        boolean cacheWasUsed = true;        
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;

            try {
                //2.获取用户信息,我们自己的用户实现了 UserDetails接口
                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 {
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if (!cacheWasUsed) {
                throw var7;
            }

            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            //3.账号状态和密码校验
            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();
        }
		//4.又封装了一次UsernamePasswordAuthenticationToken,返回 Authentication
        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }

2.1.1 获取用户对象(UserDetails)

查看 retrieveUser方法。

  • UserDetails对象是 Spring Security内部的认证用户对象。我们自定义的用户可实现 UserDetails接口。
  • 我们的SysUserService继承了 UserDetailsService类,并重新了 loadUserByUsername方法。

在这里插入图片描述

3.1.2 账号状态和密码校验

1)账号状态校验
在这里插入图片描述
2)加密校验
使用 PasswordEncoder 密码解析器对密码进行加密及解析。
在这里插入图片描述
在这里插入图片描述
BCryptPasswordEncoder是SpringSecurity中最常用的密码解析器。
它使用BCrypt算法。特点是加密使用动态加盐sault,但是解密不需要盐。因为盐就在密文当中。这样可以通过每次添加不同的盐,而给同样的字符串加密出不同的密文。

密文比如:$2a 10 10 10lLi6A4xdn7lCWYgU5yIXoek9DeYC91mTf6d9nO4UUyB/Bv4QXq6.i

其中:$是分割符,无意义;2a是bcrypt加密版本号;10是cost的值;而后的前22位是salt值;再然后的字符串就是密码的密文了。

2.1.3 又封装 UsernamePasswordAuthenticationToken对象

查看 createSuccessAuthentication方法。
在这里插入图片描述
这次又封装 UsernamePasswordAuthenticationToken对象,具体看 它的构造方法。

3、查看 UsernamePasswordAuthenticationToken类

源码如下:

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 510L;
    private final Object principal;
    private Object credentials;
    //认证成功前,调用的是这个带有两个参数的。
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }
    //认证成功后,调用的是这个带有三个参数的。
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
                                               Collection<? extends GrantedAuthority> authorities) {
        //重点看父类
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // 标记已认证
    }
}

3.1 查看 super(authorities)

查看父类 AbstractAuthenticationToken的 构造方法,源码如下:

public abstract class AbstractAuthenticationToken implements Authentication,
        CredentialsContainer {
    private final Collection<GrantedAuthority> authorities; // 用户的权限信息集合
    private Object details;
    private boolean authenticated = false;
            
    public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
		
        if (authorities == null) {//这是两个参数
            this.authorities = AuthorityUtils.NO_AUTHORITIES;
        } else {//三个参数的,看这里
            Iterator var2 = authorities.iterator();
			//1.添加用户的权限信息
            GrantedAuthority a;
            do {
                if (!var2.hasNext()) {
                    ArrayList<GrantedAuthority> temp = new ArrayList(authorities.size());
                    // 添加权限,GrantedAuthority类型的
                    temp.addAll(authorities);
                    this.authorities = Collections.unmodifiableList(temp);
                    return;
                }
                a = (GrantedAuthority)var2.next();
            } while(a != null);
			//2.若没有权限信息,是会抛出异常
            throw new IllegalArgumentException("Authorities collection cannot contain any null elements");
        }
    }
}

这里主要是添加用户的权限信息,因为,我们的 角色类实现了 GrantedAuthority接口。获取用户信息是,也已经将 角色信息放到了 用户信息中,所以这里也不难看懂。
所以,我们自定义认证业务逻辑返回的 UserDetails对象中一定要放置权限信息(GrantedAuthority类型的)。

到此,认证流程中的用户信息封装认证完毕,接下来看具体的 认证成功之后的逻辑。

4、查看 doFilter方法

回到最初的地方 UsernamePasswordAuthenticationFilter 过滤器,我们没有找到 doFilter方法,本类没有,那就去父类找!
查看父类 AbstractAuthenticationProcessingFilter的 doFilter方法,源码如下:

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        //1. 判断是否需要认证
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Request is to process authentication");
            }

            Authentication authResult;
            try {
                //2.调用子类方法获取认证信息,封装到Authentication中
                authResult = this.attemptAuthentication(request, response);
                if (authResult == null) {
                    return;
                }

                // 3. Session 策略处理
                this.sessionStrategy.onAuthentication(authResult, request, response);
            } catch (InternalAuthenticationServiceException var8) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                this.unsuccessfulAuthentication(request, response, var8);
                return;
            } catch (AuthenticationException var9) {
                //5.认证失败处理
                this.unsuccessfulAuthentication(request, response, var9);
                return;
            }

            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }

            // 4. 认证成功处理
            this.successfulAuthentication(request, response, chain, authResult);
        }
    }

调用子类方法获取认证信息,上面我们已经分析了。

4.1 查看 successfulAuthentication方法

    //认证成功,调用 successfulAuthentication
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Authentication success. Updating SecurityContextHolder tocontain: " + authResult);
        }
		//1.认证成功,将认证信息存储到SecurityContext中!
        SecurityContextHolder.getContext().setAuthentication(authResult);
		//2.登录成功调用rememberMeServices
        this.rememberMeServices.loginSuccess(request, response, authResult);
        if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new
                    InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }
        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }

4.2 查看 unsuccessfulAuthentication方法

	//认证失败,调用unsuccessfulAuthentication
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Authentication request failed: " + failed.toString(), failed);
            this.logger.debug("Updated SecurityContextHolder to contain null Authentication");
            this.logger.debug("Delegating to authentication failure handler " +
                    this.failureHandler);
        }
        this.rememberMeServices.loginFail(request, response);
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    }

上面就是认证流程的大致过程。

– -- 求知若饥,虚心若愚。


http://www.niftyadmin.cn/n/2257465.html

相关文章

成都榆熙:店铺流量受限主要原因有哪些?

店铺流量是商家们都很看重的东西&#xff0c;有的商家发现店铺出现了流量下降的情况&#xff0c;甚至是店铺出现了流量限制的情况&#xff0c;那么如果遇到这些情况&#xff0c;商家们该怎么面对呢&#xff1f;和成都榆熙小编一起来了解一下吧。 一、怎样预防店铺流量下降的情…

Lombok【汇总】

目录【官网】【使用篇】【注意事项】【Maven依赖】【SpringBoot】【常用注解】【推荐写法】【推荐好文】【官网】 https://github.com/rzwitserloot/lombok 【使用篇】 【注意事项】 在使用Lombok时&#xff0c;你的编辑器可能会报错&#xff0c;这时请在你的IDE中安装Lombo…

PDD商家如何提取热门关键词?

在拼多多平台新颖、独特的新型电商模式的吸引下&#xff0c;选择在拼多多开店的朋友越来越多了。面临不同于其他传统电商创业平台的特殊模式&#xff0c;难以借鉴其他平台运营思路&#xff0c;在拼多多开店的商家很容易陷入运营困境。今天成都榆熙小编就跟大家聊一聊拼多多新店…

全局异常处理总结

目录WEB.XMLSpring全局异常&#xff0c;Controller增强方式&#xff08; Advising Controllers&#xff09;Spirng全局异常&#xff0c;配置方式Sping全局异常&#xff0c;自定义异常类和异常解析Errors and REST推荐文章WEB.XML 就是指定error-code和page到指定地址&#xff…

成都榆熙电子商务有限公司:店铺违规后可以补救吗?

既然选择了在拼多多开店&#xff0c;那么商家们就要遵守平台的规则&#xff0c;但是有的商家因为自己的一些错误操作导致店铺受到处罚&#xff0c;那么商家们经常会遇到哪些违规情况呢&#xff1f;可以补救吗&#xff1f;想要了解的小伙伴一起和榆熙电商小编来看看吧。 一、收…

Spring Security授权源码分析

授权是在用户认证通过后&#xff0c;对访问资源的权限进行检查的过程。 Spring Security使用标准 Filter建立了对 web请求的拦截&#xff0c;最终实现对资源的授权访问。 Spring Security过滤器链加载执行流程源码分析请查看&#xff1a;https://blog.csdn.net/qq_42402854/ar…

mysql迁移

目录 1.SQLyog 2.阿里云DTS 1.创建任务 2.配置源库及目标库信息 3.配置任务对象及高级配置 4.高级配置 1.SQLyog 适用于离线迁移&#xff0c;全量迁移&#xff0c;少量迁移&#xff0c;数据库大小在几百兆范围内&#xff0c;时间需要几十秒到几分钟不等。 选择源库&…

榆熙电商:PDD商家如何留住核心消费者?

一般来说&#xff0c;很多个人商家在拼多多开店之后对自己店铺的运营管理上都比较欠缺&#xff0c;对寻找目标用户以及用户定位管理等方面都很被动&#xff0c;等待流量自主上门往往会丧失店铺竞争力。作为拼多多商家&#xff0c;如何才能对目标用户有着清晰明确的认知呢&#…