用过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. #2059:龟兔赛跑(动态规划dp)

    Problem Description 据说在很久很久以前,可怜的兔子经历了人生中最大的打击--赛跑输给乌龟后,心中郁闷,发誓要报仇雪恨,于是躲进了杭州下沙某农业园卧薪尝胆潜心修炼,终于练成了绝技,能 ...

  2. POJ - 3087:Shuffle'm Up (字符串模拟)

    一.内容 题意:给定2个字符串s1,s2,将2个字符串进行重组成S,规则是S2最下面拿一个,S1最下面拿1个,直到所有块都用完. 二.思路 用map记录下S串结果,若以前访问过这个串代表不可能有结果直 ...

  3. 一、@Configuration、@Conponent 、@ComponentScan 注解等

    一句话概括 区别: @Configuration 中所有带 @Bean 注解的方法都会被动态代理,因此调用该方法返回的都是同一个实例.2. 可以直接调用方法,不需要 @Autowired 注入后使用. ...

  4. 通义千问预体验,如何让 AI 模型应用“奔跑”在函数计算上?

    立即体验基于函数计算部署通义千问预体验: https://developer.aliyun.com/topic/aigc_fc AIGC 浪潮已来,从文字生成到图片生成,AIGC 的创造力让人惊叹,更 ...

  5. oppo和海康嵌入式软件工程师面经总结

    目录 海康 一面(3.23,35min) 自我介绍 项目介绍 你做的这个项目遇到了那些问题,如何解决的? 移植uboot,只做了移植吗? 用的那个文件系统? 移植过程中,网卡驱动做了那些工作? 写过那 ...

  6. uniapp picker组件实现二级联动

    https://blog.csdn.net/hxh_csdn/article/details/111504951 https://www.cnblogs.com/jstll/p/14149600.ht ...

  7. 每天学五分钟 Liunx 0101 | 服务篇:创建进程

    创建子进程 上一节说过创建子进程的三种方式: 1. fork 复制进程:fork 会复制当前进程的副本,产生一个新的子进程,父子进程是完全独立的两个进程,他们掌握的资源(环境变量和普通变量)是一样的. ...

  8. offline RL | ABM:从 offline dataset 的好 transition 提取 prior policy

    ICLR 2020,6 6 6. 材料: 论文题目:Keep Doing What Worked: Behavior Modelling Priors for Offline Reinforcemen ...

  9. Java-Enum常量特定方法

    OnJava8-Enum-常量特定方法 用枚举实现责任链模式 责任链(Chain Of Responsibility)设计模式先创建了一批用于解决目标问题的不同方法,然后将它们连成一条"链& ...

  10. MyBatis02:CRUD操作

    CRUD操作 namespace 这个是命名空间,不可缺少,更不能写错 配置文件中namespace中的名称为对应Mapper接口或者Dao接口的全限定类名,完整包名,必须一致! Select(用于查 ...