Spring Security 实现用户名密码登录流程源码详解

目录
  • 引言
  • 探究
  • 登录流程
  • 校验
  • 用户信息保存

引言

你在服务端的安全管理使用了 Spring Security,用户登录成功之后,Spring Security 帮你把用户信息保存在 Session 里,但是具体保存在哪里,要是不深究你可能就不知道, 这带来了一个问题,如果用户在前端操作修改了当前用户信息,在不重新登录的情况下,如何获取到最新的用户信息?

探究

无处不在的 Authentication

玩过 Spring Security 的小伙伴都知道,在 Spring Security 中有一个非常重要的对象叫做 Authentication,我们可以在任何地方注入 Authentication 进而获取到当前登录用户信息,Authentication 本身是一个接口,它有很多实现类:

在这众多的实现类中,我们最常用的就是 UsernamePasswordAuthenticationToken 了,但是当我们打开这个类的源码后,却发现这个类平平无奇,他只有两个属性、两个构造方法以及若干个 get/set 方法;当然,他还有更多属性在它的父类上。

但是从它仅有的这两个属性中,我们也能大致看出,这个类就保存了我们登录用户的基本信息。那么我们的登录信息是如何存到这两个对象中的?这就要来梳理一下登录流程了。

登录流程

在 Spring Security 中,认证与授权的相关校验都是在一系列的过滤器链中完成的,在这一系列的过滤器链中,和认证相关的过滤器就是 UsernamePasswordAuthenticationFilter::

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
//默认的用户名和密码对应的key
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
//当前过滤器默认拦截的路径
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST");
    //默认的请求参数名称规定
    private String usernameParameter = "username";
    private String passwordParameter = "password";
    //默认只能是post请求
    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {
    //设置默认的拦截路径
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
       //设置默认的拦截路径,和处理认证的管理器
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

    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);
            username = username != null ? username : "";
            username = username.trim();
            String password = this.obtainPassword(request);
            password = password != null ? password : "";
            //构造用户名和密码登录的认证令牌
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            //设置details---deltails里面默认存放sessionID和remoteaddr
            //authRequest 就是构造好的认证令牌
            this.setDetails(request, authRequest);
            //校验
            //authRequest 就是构造好的认证令牌
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.usernameParameter;
    }

    public final String getPasswordParameter() {
        return this.passwordParameter;
    }
}

根据这段源码我们可以看出:

首先通过 obtainUsername 和 obtainPassword 方法提取出请求里边的用户名/密码出来,提取方式就是 request.getParameter ,这也是为什么 Spring Security 中默认的表单登录要通过 key/value 的形式传递参数,而不能传递 JSON 参数,如果像传递 JSON 参数,修改这里的逻辑即可

获取到请求里传递来的用户名/密码之后,接下来就构造一个 UsernamePasswordAuthenticationToken 对象,传入 username 和 password,username 对应了 UsernamePasswordAuthenticationToken 中的 principal 属性,而 password 则对应了它的 credentials 属性。

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 550L;
    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);
    }

    public Object getCredentials() {
        return this.credentials;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }

    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

接下来 setDetails 方法给 details 属性赋值,UsernamePasswordAuthenticationToken 本身是没有 details 属性的,这个属性在它的父类 AbstractAuthenticationToken 中。details 是一个对象,这个对象里边放的是 WebAuthenticationDetails 实例,该实例主要描述了两个信息,请求的 remoteAddress 以及请求的 sessionId

最后一步,就是调用 authenticate 方法去做校验了。

好了,从这段源码中,大家可以看出来请求的各种信息基本上都找到了自己的位置,找到了位置,这就方便我们未来去获取了。

接下来我们再来看请求的具体校验操作。

校验

在前面的 attemptAuthentication 方法中,该方法的最后一步开始做校验,校验操作首先要获取到一个 AuthenticationManager,这里拿到的是 ProviderManager ,所以接下来我们就进入到 ProviderManagerauthenticate 方法中,当然这个方法也比较长,我这里仅仅摘列出来几个重要的地方:

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    //获取到主体(用户名)和凭证(密码)组成的一个令牌对象的class类对象
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        int currentPosition = 0;
        //获取所有可用来校验令牌对象的provider数量
        int size = this.providers.size();
        //获取迭代器
        Iterator var9 = this.getProviders().iterator();
         //遍历所有provider
        while(var9.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var9.next();
            //判断当前provider是否支持当前令牌对象的校验
            if (provider.supports(toTest)) {
                if (logger.isTraceEnabled()) {
                    Log var10000 = logger;
                    String var10002 = provider.getClass().getSimpleName();
                    ++currentPosition;
                    var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
                }

                try {
                //如果支持就进行认证校验处理
                    result = provider.authenticate(authentication);
                    //校验成功返回一个新的authentication
                    //将原先的主体由用户名换成了userdetails对象
                    if (result != null) {
                    //拷贝details到新的令牌对象
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (InternalAuthenticationServiceException | AccountStatusException var14) {
                    this.prepareException(var14, authentication);
                    throw var14;
                } catch (AuthenticationException var15) {
                    lastException = var15;
                }
            }
        }

//认证失败但是 provider 的 parent不为null
        if (result == null && this.parent != null) {
            try {
            //调用 provider 的 parent进行验证--parent就是providerManager
                parentResult = this.parent.authenticate(authentication);
                result = parentResult;
            } catch (ProviderNotFoundException var12) {
            } catch (AuthenticationException var13) {
                parentException = var13;
                lastException = var13;
            }
        }

//认证成功
        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;
        }
    }

这个方法就比较魔幻了,因为几乎关于认证的重要逻辑都将在这里完成:

首先获取 authentication 的 Class,判断当前 provider 是否支持该 authentication。

如果支持,则调用 provider 的 authenticate方法开始做校验,校验完成后,会返回一个新的Authentication。一会来和大家捋这个方法的具体逻辑

这里的 provider 可能有多个,如果 provider 的 authenticate 方法没能正常返回一个Authentication,则调用 provider 的 parent 的 authenticate 方法继续校验。

copyDetails 方法则用来把旧的 Token 的 details 属性拷贝到新的 Token 中来。

接下来会调用 eraseCredentials 方法擦除凭证信息,也就是你的密码,这个擦除方法比较简单,就是将 Token 中的credentials 属性置空

最后通过 publishAuthenticationSuccess 方法将登录成功的事件广播出去。

大致的流程,就是上面这样,在 for 循环中,第一次拿到的 provider 是一个 AnonymousAuthenticationProvider,这个 provider 压根就不支持 UsernamePasswordAuthenticationToken,也就是会直接在 provider.supports 方法中返回 false,结束 for 循环,然后会进入到下一个 if 中,直接调用 parent 的 authenticate 方法进行校验。

parent 就是 ProviderManager,所以会再次回到这个 authenticate 方法中。再次回到 authenticate 方法中,provider 也变成了 DaoAuthenticationProvider,这个 provider 是支持 UsernamePasswordAuthenticationToken 的,所以会顺利进入到该类的 authenticate 方法去执行,而 DaoAuthenticationProvider 继承自 AbstractUserDetailsAuthenticationProvider 并且没有重写 authenticate 方法,所以 我们最终来到 AbstractUserDetailsAuthenticationProvider#authenticate 方法中:

public Authentication authenticate(Authentication authentication)
		throws AuthenticationException {
	String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
			: authentication.getName();
	user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
	preAuthenticationChecks.check(user);
	additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
	postAuthenticationChecks.check(user);
	//如果用户没有使用过,将其放进缓存中
	if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }
	Object principalToReturn = user;
	if (forcePrincipalAsString) {
		principalToReturn = user.getUsername();
	}
	return createSuccessAuthentication(principalToReturn, authentication, user);
}

首先从 Authentication 提取出登录用户名。

然后通过拿着 username 去调用 retrieveUser 方法去获取当前用户对象,这一步会调用我们自己在登录时候的写的 loadUserByUsername 方法,所以这里返回的 user 其实就是你的登录对象

接下来调用 preAuthenticationChecks.check 方法去检验 user 中的各个账户状态属性是否正常,例如账户是否被禁用、账户是否被锁定、账户是否过期等等

additionalAuthenticationChecks 方法则是做密码比对的,好多小伙伴好奇 Spring Security 的密码加密之后,是如何进行比较的,看这里就懂了。

最后在 postAuthenticationChecks.check 方法中检查密码是否过期。

判断用户是否在缓存中存在,如果不存在,就放入缓存中

接下来有一个 forcePrincipalAsString 属性,这个是是否强制将 Authentication 中的 principal 属性设置为字符串,这个属性我们一开始在 UsernamePasswordAuthenticationFilter 类中其实就是设置为字符串的(即 username),但是默认情况下,当用户登录成功之后, 这个属性的值就变成当前用户这个对象了。之所以会这样,就是因为 forcePrincipalAsString 默认为 false,不过这块其实不用改,就用 false,这样在后期获取当前用户信息的时候反而方便很多。

最后,通过 createSuccessAuthentication 方法构建一个新的 UsernamePasswordAuthenticationToken,此时认证主体就由用户名变为了userDetails对象

好了,那么登录的校验流程现在就基本和大家捋了一遍了。那么接下来还有一个问题,登录的用户信息我们去哪里查找?

用户信息保存

要去找登录的用户信息,我们得先来解决一个问题,就是上面我们说了这么多,这一切是从哪里开始被触发的?

我们来到 UsernamePasswordAuthenticationFilter 的父类 AbstractAuthenticationProcessingFilter 中,这个类我们经常会见到,因为很多时候当我们想要在 Spring Security 自定义一个登录验证码或者将登录参数改为 JSON 的时候,我们都需自定义过滤器继承自 AbstractAuthenticationProcessingFilter ,毫无疑问,UsernamePasswordAuthenticationFilter#attemptAuthentication 方法就是在 AbstractAuthenticationProcessingFilter 类的 doFilter 方法中被触发的:

 private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
 //不需要认证就直接放行
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            try {
            //获取认证的结果---null或者新生产的令牌对象
                Authentication authenticationResult = this.attemptAuthentication(request, response);
               //认证失败
                if (authenticationResult == null) {
                    return;
                }

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

                this.successfulAuthentication(request, response, chain, authenticationResult);
            } catch (InternalAuthenticationServiceException var5) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var5);
                this.unsuccessfulAuthentication(request, response, var5);
            } catch (AuthenticationException var6) {
                this.unsuccessfulAuthentication(request, response, var6);
            }

        }
    }

从上面的代码中,我们可以看到,当 attemptAuthentication 方法被调用时,实际上就是触发了 UsernamePasswordAuthenticationFilter#attemptAuthentication 方法,当登录抛出异常的时候,unsuccessfulAuthentication 方法会被调用,而当登录成功的时候,successfulAuthentication 方法则会被调用,那我们就来看一看 successfulAuthentication 方法:

protected void successfulAuthentication(HttpServletRequest request,
		HttpServletResponse response, FilterChain chain, Authentication authResult)
		throws IOException, ServletException {
		//将新生产的令牌对象放入spring security的上下文环境中
	SecurityContextHolder.getContext().setAuthentication(authResult);
	rememberMeServices.loginSuccess(request, response, authResult);
	// Fire event
	if (this.eventPublisher != null) {
		eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
				authResult, this.getClass()));
	}
	successHandler.onAuthenticationSuccess(request, response, authResult);
}

在这里有一段很重要的代码,就是 SecurityContextHolder.getContext().setAuthentication(authResult); ,登录成功的用户信息被保存在这里,也就是说,在任何地方,如果我们想获取用户登录信息,都可以从 SecurityContextHolder.getContext() 中获取到,想修改,也可以在这里修改。

最后大家还看到有一个 successHandler.onAuthenticationSuccess,这就是我们在 SecurityConfig 中配置登录成功回调方法,就是在这里被触发的

当认证失败时,会调用登录失败处理器,并清空上下文环境中的对象

 protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        this.logger.trace("Failed to process authentication request", failed);
        this.logger.trace("Cleared SecurityContextHolder");
        this.logger.trace("Handling authentication failure");
        this.rememberMeServices.loginFail(request, response);
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    } 

以上就是Spring Security 实现用户名密码登录流程源码详解的详细内容,更多关于Spring Security 用户名密码登录的资料请关注我们其它相关文章!

(0)

相关推荐

  • Spring security密码加密实现代码实例

    xml配置如下 <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> <version>3.2.3.RELEASE</version> </dependency> BCryptPasswordEncoder相关知识: 用户表的密码通常使用MD5等不可逆算

  • Spring Security 密码验证动态加盐的验证处理方法

    本文个人博客地址:https://www.leafage.top/posts/detail/21697I2R 最近几天在改造项目,需要将gateway整合security在一起进行认证和鉴权,之前gateway和auth是两个服务,auth是shiro写的一个,一个filter和一个配置,内容很简单,生成token,验证token,没有其他的安全检查,然后让对项目进行重构. 先是要整合gateway和shiro,然而因为gateway是webflux,而shiro-spring是webmvc,所

  • Spring Security保护用户密码常用方法详解

    1. 前言 本节将对 Spring Security 中的密码编码进行一些探讨. 2. 不推荐使用md5 首先md5 不是加密算法,是哈希摘要.以前通常使用其作为密码哈希来保护密码.由于彩虹表的出现,md5 和sha1之类的摘要算法都已经不安全了.如果有不相信的同学 可以到一些解密网站 如 cmd5 网站尝试解密 你会发现 md5 和 sha1 是真的非常容易被破解. 3. Spring Security中的密码算法 ObjectProvider<PasswordEncoder>参数.这里的P

  • Spring Security 实现用户名密码登录流程源码详解

    目录 引言 探究 登录流程 校验 用户信息保存 引言 你在服务端的安全管理使用了 Spring Security,用户登录成功之后,Spring Security 帮你把用户信息保存在 Session 里,但是具体保存在哪里,要是不深究你可能就不知道, 这带来了一个问题,如果用户在前端操作修改了当前用户信息,在不重新登录的情况下,如何获取到最新的用户信息? 探究 无处不在的 Authentication 玩过 Spring Security 的小伙伴都知道,在 Spring Security 中

  • Spring Security实现用户名密码登录详解

    目录 环境 用户名密码登录 E-R图 POM依赖 配置文件 Mapper Service设计 HTML Controller 启动 完整代码 环境 JDK 1.8 Spring Boot 2.3.0.RELEASE Maven 3.6.1 H2 数据库 用户名密码登录 首先,我们用 Spring Security 实现用户输入用户名密码登录验证并获取相应权限. E-R图 完整建表语句 因为是测试程序,所以用H2数据库来测试.SQL脚本在resouces/db目录下,项目启动后会自动初始化脚本,无

  • Mybatis SQL运行流程源码详解

    概述 源码就是能够被用来执行,生成机器能够识别的代码,通过开源源码,可以引用其功能. 重要性 1.mybatis中的sql执行,不仅要知道返回的结果是什么,还需要知道这结果是怎么来的,经过了怎样的处理,只有知道了这样的原理,碰到问题才能更好的知道问题出在那个环节. 2.能更好的扩展应用程序,可以做到代码的复用,减少开发成本和时间. 3.学习其中的设计思想能够在其他应用得已应用. 步骤 源码阅读,可以从测试用例着手,每个源码,多有大师们写的测试用例,咱们可以用大师们写的测试用例进行相关功能的测试,

  • spring security中的默认登录页源码跟踪

    ​2021年的最后2个月,立个flag,要把Spring Security和Spring Security OAuth2的应用及主流程源码研究透彻! ​项目中使用过Spring Security的童鞋都知道,当我们没有单独自定义登录页时,Spring Security自己在初始化的时候会帮我们配置一个默认的登录页,之前一直疑问默认登录页是怎么配置的,今晚特地找了源码跟一下. springboot项目依赖 <dependency> <groupId>org.springframewo

  • Spring Security过滤器链加载执行流程源码解析

    目录 Spring Security实现原理 一.Spring Security过滤器链加载 1.注册名为 springSecurityFilterChain的过滤器 2.查看 DelegatingFilterProxy类 3.查看 FilterChainProxy类 3.1 查看 doFilterInternal方法. 3.2 查看 getFilters方法. 4 查看 SecurityFilterChain接口 5 查看 SpringBootWebSecurityConfiguration类

  • SpringBoot整合Spring Security过滤器链加载执行流程源码分析(最新推荐)

    目录 1.引言 2.Spring Security过滤器链加载 2.1.注册名为 springSecurityFilterChain的过滤器 3.查看 DelegatingFilterProxy类 4.查看 FilterChainProxy类 4.1 查看 doFilterInternal方法 4.2 查看 getFilters方法 5 查看 SecurityFilterChain接口 6. 查看 SpringBootWebSecurityConfiguration类 总结: 1.引言 在 Sp

  • Spring JPA联表查询之OneToOne源码详解

    目录 前言 源码 注解属性 单向联表 user 实体类 car 实体类 查询结果 双向联表 user 实体 car 实体 查询结果 延迟加载(懒加载) user 实体 查询结果: 查询完会发现,控制台又打印了一个 JPQL: 最后结论 前言 前面几篇我们学习的都是单表查询,就是对一张表中的数据进行查询.而实际项目中,基本都会有多张表联合查询的情况,今天我们就来了解下JPA的联表查询是如做的. 源码 @OneToOne 注解实现一对一关系映射.比如用户跟车辆的关系(这里假设一个人只能有一辆车),一

  • Spring Security使用中Preflight请求和跨域问题详解

    Spring Security Spring Security是能够为J2EE项目提供综合性的安全访问控制解决方案的安全框架.它依赖于Servlet过滤器.这些过滤器拦截进入请求,并且在应用程序处理该请求之前进行某些安全处理. Spring Security对用户请求的拦截过程如下: 背景 在一个前后端分离开发的项目中,使用SpringSecurity做安全框架,用JWT来实现权限管理提升RESTful Api的安全性.首先遇到的就是跨域问题,但是在携带jwt请求过程中出现了服务端获取不到jwt

  • 浅析Spring Security登录验证流程源码

    一.登录认证基于过滤器链 Spring Security的登录验证流程核心就是过滤器链.当一个请求到达时按照过滤器链的顺序依次进行处理,通过所有过滤器链的验证,就可以访问API接口了. SpringSecurity提供了多种登录认证的方式,由多种Filter过滤器来实现,比如: BasicAuthenticationFilter实现的是HttpBasic模式的登录认证 UsernamePasswordAuthenticationFilter实现用户名密码的登录认证 RememberMeAuthe

  • SpringSecurity 默认表单登录页展示流程源码

    SpringSecurity 默认表单登录页展示流程源码 本篇主要讲解 SpringSecurity提供的默认表单登录页 它是如何展示的的流程, 涉及 1.FilterSecurityInterceptor, 2.ExceptionTranslationFilc,xmccmc,ter , 3.DefaultLoginPageGeneratingFilter 过滤器, 并且简单介绍了 AccessDecisionManager 投票机制  1.准备工作(体验SpringSecurity默认表单认证

随机推荐