用过SpringSecurity的小伙伴,都知道 Authentication 这个接口,我们在任何地方通过这个接口来获取到用户登录的信息,而我们用的频繁的一个它的一个实现类就是 UsernamePasswordAuthenticationToken。那么我们的登录信息是如何保存在这个类中的?那我们就需要知道SpringSecurity 的登录流程了。这两个类很重要!

在 SpringSecurity 中 认证和授权的校验是通过一系列的过滤器链。

首先到 AbstractAuthenticationProcessingFilter中,doFilter 方法:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res; if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response); return;
} Authentication authResult; try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed); return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed); return;
} // Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
} successfulAuthentication(request, response, chain, authResult);
}

分析:

1,authResult = attemptAuthentication(request, response); 调用登录相关的过滤器中的方法

也就是 UsernamePasswordAuthenticationFilter这个过滤器

2,unsuccessfulAuthentication(request, response, failed); 登录失败的处理

此方法中会调用我们在 SecurityConfig 中重写的方法 failureHandler

3,successfulAuthentication(request, response, chain, authResult); 登录成功的处理

此方法中会调用我们在 SecurityConfig 中重写的方法 successHandler

UsernamePasswordAuthenticationFilter(用户名密码身份验证过滤器)。其中比较重要的几个方法:

public class UsernamePasswordAuthenticationFilter extends

AbstractAuthenticationProcessingFilter {

public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
} String username = obtainUsername(request);
String password = obtainPassword(request); if (username == null) {
username = "";
} if (password == null) {
password = "";
} username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password); // Allow subclasses to set the "details" property
setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest);
}
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
} @Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
} protected void setDetails(HttpServletRequest request,
UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

}

通过上面的代码逻辑我们可以看出:

1,获取用户名和密码通过两个方法 obtainUsername(request);obtainPassword(request); 这两个方法都是通过 request.getParameter() 来获取的。

2,new 了一个 UsernamePasswordAuthenticationToken 来保存获取到的用户名和密码。

用户名保存到了 principal 属性中,密码保存到了 credentials 属性中。请注意:此时的               principal(主要的) 的类型是字符串类型,到后面会变为一个对象类型。

3,通过 setDetails() 方法来设置 details 属性。而 UsernamePassworAuthenticationToken 是没有这个属性的,而他的父类 AbstractAuthenticationTocken 中有,所以就保存在了它的父类中。这个details 属性是一个对象类型,这个对象里放的是 WebAuthenticationDetails 的实例,这个类中有两个属性 remoteAddress(远程地址) 和 sessionId。

4,最后调用 authenticate(authRequest) 方法进行验证,里面传了一个刚才new的 UsernamePassworAuthenticationToken 类的实例

接下来就是具体的校验操作了。

在上面 attemptAuthentication 方法的最后一步是开始做校验的操作,该行代码的首先拿到一个 AuthenticationManager ,然后调用它的 authenticate() 方法进行身份验证,而 AuthenticationManager 是一个接口,需要由它的实现类去执行 authenticate() 方法,而这时候获取到的实现类是 ProviderManger ,这时我们进入到它的 authenticate() 方法中,这里仅提供比较重要的部分:

public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled(); for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
} if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
} try {
result = provider.authenticate(authentication); if (result != null) {
copyDetails(authentication, result);
break;
}
}
...
} if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
...
} if (result != null) {
if (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 than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} ...
}

认证逻辑在这个方法完成

1,我们首先拿到 Authentication 类的 Class对象

2,进入到 for 循环,通过 getProviders() 方法获取到 AuthenticationProvider(身份验证提供者),然后调用 provider.supports(toTest) 来判断 当前拿到的 provider 是否支持对应的 Authentication(这里的Authentication 是 UsernamePasswordAuthenticationToken。如果不支持就 continue 。而第一次只拿到了一个 AuthenticationProvider  ,所以会结束for循环。如果支持调用 result = provider.authenticate(authentication); 来进行身份校验。

3,调用 copyDetails() 方法,这个方法会把旧的 Token 的details信息复制到新的 Token 中

5,调用 eraseCredentials() 方法,来擦除凭证信息,也就是你登录时的密码信息。

for循环分析:第一次循环 拿到的 provider 是 AnonymousAuthenticationProvider(匿名身份验证提供者),它根本就不支持验证 UsernamePasswordAuthenticationToken,然后判断成立 执行 continue。结束了此次循环。然后调用 parent.authenticate(authentication); 来继续验证,而 parent 就是 AuthenticationProvider ,所以又回到了此方法。而第二次拿到的是一个 DaoAuthenticationProvider,这个 provider 是支持验证 UsernamePasswordAuthenticationToken的,但是 DaoAuthenticationProvider 中并没有重写此方法,但是它的父类 AbstractUserDetailsAuthenticationProvider 类中定义了此方法,所以会来到它的父类的这个方法中,依旧是比较重要的部分:

public Authentication authenticate(Authentication authentication)
throws AuthenticationException { ...
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName(); try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
... try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
} postAuthenticationChecks.check(user); if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
} Object principalToReturn = user; if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
} return createSuccessAuthentication(principalToReturn, authentication, user);
}

分析:

1,首先拿到用户名,然后调用 retrieveUser() 方法来检索用户,这个方法会调用自己重写后的 loadUserByUsername() 方法 返回一个 User 对象,也就是从数据库中根据用户名查询出来的

protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
...
}

2,接下来调用 preAuthenticationChecks.check(user); 来校验用户的状态。比如:账户是否锁定,是否禁用,是否过期

3,接下来调用 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); 来校验密码是否正确

4,然后再调用 postAuthenticationChecks.check(user); 来校验密码是否过期

5,接下来有一个 forcePrincipalAsString 属性,这个是 是否强制将 Authentication 中的 principal 属性设置为字符串。这个默认值是 false ,不用改,这样获取信息会很方便。

6,最后通过 createSuccessAuthentication(principalToReturn, authentication, user); 创建一个新的 UsernamePasswordAuthenticationToken 对象返回。

SpringSecurity 的登录流程的更多相关文章

  1. SpringSecurity中的Authentication信息与登录流程

    目录 Authentication 登录流程 一.与认证相关的UsernamePasswordAuthenticationFilter 获取用户名和密码 构造UsernamePasswordAuthe ...

  2. 如何设计一个 App 的注册登录流程?

    移 动设备发力之前的登录方式很简单:用户名/邮箱+密码+确认密码,所有的用户登录注册都是围绕着邮箱来做.随着移动设备和社交网络的普及,邮箱不再是唯 一,渐渐的出现了微博,QQ,微信等第三方登录方式,手 ...

  3. 用户登录流程详解 +volley(StringRequest)

    在实习期间由于要求使用volley,所以第一次开始接触volley,从一开始的迷茫陌生,到疯狂的查找各种资料,通过在项目中用到的实际问题,我想做一些总结,所以写了这篇文章.下面我将介绍我理解的用户登录 ...

  4. 二维码闪电登录流程详解,附demo(1/2)

    二维码,最早发明于日本,它是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的,使用若干个与二进制相对应的几何形体来表示文字数值信息,通过图象输入设备或光电扫描设 ...

  5. 微信小程序登录流程

    小程序登录流程 参考 app.js需要做的 1,首先通过wx.login()拿到code,成功之后,发送数据,请求接口,把code发送给后端,换区openid,sessionKey,unionId,把 ...

  6. 小程序页面跳转传参-this和that的区别-登录流程-下拉菜单-实现画布自适应各种手机尺寸

    小程序页面跳转传参 根目录下的 app.json 文件 页面文件的路径.窗口表现.设置网络超时时间.设置多 tab { "pages": [ "pages/index/i ...

  7. Vue + Element UI 实现权限管理系统(优化登录流程)

    完善登录流程 1. 丰富登录界面 1.1 从 Element 指南中选择组件模板丰富登录界面,放置一个登录界面表单,包含账号密码输入框和登录重置按钮. <template> <el- ...

  8. ASP.net 完整登录流程

    登录流程 using System; using System.Collections.Generic; using System.Linq; using System.Web; using Syst ...

  9. Vue + Element UI 实现权限管理系统 前端篇(四):优化登录流程

    完善登录流程 1. 丰富登录界面 1.1 从 Element 指南中选择组件模板丰富登录界面,放置一个登录界面表单,包含账号密码输入框和登录重置按钮. <template> <el- ...

  10. 微信小程序--登录流程梳理

    前言 微信小程序凡是需要记录用户信息都需要登录,但是也有几种不同的登录方式,但是在小程序部分的登录流程是一样的.之前就朦朦胧胧地用之前项目的逻辑改改直接用了,这个新项目要用就又结合官方文档重新梳理了下 ...

随机推荐

  1. 关于 Jupyter 导出 PDF/Latex 格式报错的简单解决方法

    利用 Jupyter 提供的 Print Preview 功能,然后鼠标右键点击打印,就能导出PDF了,而且不会出问题,中文,图片都可以

  2. 2019年第十届蓝桥杯国赛C++A组

    蓝桥杯历年国赛真题汇总:Here 最后编辑时间: 2021年5月27日 统一声明 如果不写默认带有常用头文件 如果不表明主函数默认表示在 void solve(){} 默认使用 using names ...

  3. 【scikit-learn基础】--『监督学习』之 空间聚类

    空间聚类算法是数据挖掘和机器学习领域中的一种重要技术. 本篇介绍的基于密度的空间聚类算法的概念可以追溯到1990年代初期.随着数据量的增长和数据维度的增加,基于密度的算法逐渐引起了研究者的关注.其中, ...

  4. [转帖]GC日志解读,这次别再说看不懂GC日志了

    https://juejin.cn/post/7029130033268555807   测试环境:机器内存16G,JDK 8,12核CPU 测试用例,从网上找的示例,视情况修改即可:   java ...

  5. [转帖]Linux ps -o 查看进程启动时间

    https://www.cnblogs.com/apink/p/17572435.html 时间参数 如下表 参数  含义 start 显示进程启动时间的简短格式.通常,它会显示日期时间中的月-日 或 ...

  6. Docker导出镜像的总结

    Docker导出镜像的总结 安装Docker mkdir -p /etc/docker cat >/etc/docker/daemon.josn <<EOF { "bip& ...

  7. [转帖]ck的离线安装

    下载 下载地址: https://repo.clickhouse.tech/tgz/stable/ 下载的包: clickhouse-common-static clickhouse-server c ...

  8. [转帖]tgz 安装clickhouse

    一.什么是clickhouse ClickHouse是开源的列式存储数据库(DBMS),主要用于在线处理查询(OLAP),能够使用SQL查询实时生成数据分析报告. 下面介绍下安装clickhouse. ...

  9. [转帖]如何对minio进行性能测试和分析

    https://developer.aliyun.com/article/1006775   环境详情 server(组成集群,ec为12:4) ip hosts 硬盘 storage01 172.1 ...

  10. [转帖]windos的kafka设置账号密码

    1.kafka配置文件 server.properties增加 listeners=SASL_PLAINTEXT://127.0.0.1:9092 advertised.listeners=SASL_ ...