SpringSecurity 的登录流程
用过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 的登录流程的更多相关文章
- SpringSecurity中的Authentication信息与登录流程
目录 Authentication 登录流程 一.与认证相关的UsernamePasswordAuthenticationFilter 获取用户名和密码 构造UsernamePasswordAuthe ...
- 如何设计一个 App 的注册登录流程?
移 动设备发力之前的登录方式很简单:用户名/邮箱+密码+确认密码,所有的用户登录注册都是围绕着邮箱来做.随着移动设备和社交网络的普及,邮箱不再是唯 一,渐渐的出现了微博,QQ,微信等第三方登录方式,手 ...
- 用户登录流程详解 +volley(StringRequest)
在实习期间由于要求使用volley,所以第一次开始接触volley,从一开始的迷茫陌生,到疯狂的查找各种资料,通过在项目中用到的实际问题,我想做一些总结,所以写了这篇文章.下面我将介绍我理解的用户登录 ...
- 二维码闪电登录流程详解,附demo(1/2)
二维码,最早发明于日本,它是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的,使用若干个与二进制相对应的几何形体来表示文字数值信息,通过图象输入设备或光电扫描设 ...
- 微信小程序登录流程
小程序登录流程 参考 app.js需要做的 1,首先通过wx.login()拿到code,成功之后,发送数据,请求接口,把code发送给后端,换区openid,sessionKey,unionId,把 ...
- 小程序页面跳转传参-this和that的区别-登录流程-下拉菜单-实现画布自适应各种手机尺寸
小程序页面跳转传参 根目录下的 app.json 文件 页面文件的路径.窗口表现.设置网络超时时间.设置多 tab { "pages": [ "pages/index/i ...
- Vue + Element UI 实现权限管理系统(优化登录流程)
完善登录流程 1. 丰富登录界面 1.1 从 Element 指南中选择组件模板丰富登录界面,放置一个登录界面表单,包含账号密码输入框和登录重置按钮. <template> <el- ...
- ASP.net 完整登录流程
登录流程 using System; using System.Collections.Generic; using System.Linq; using System.Web; using Syst ...
- Vue + Element UI 实现权限管理系统 前端篇(四):优化登录流程
完善登录流程 1. 丰富登录界面 1.1 从 Element 指南中选择组件模板丰富登录界面,放置一个登录界面表单,包含账号密码输入框和登录重置按钮. <template> <el- ...
- 微信小程序--登录流程梳理
前言 微信小程序凡是需要记录用户信息都需要登录,但是也有几种不同的登录方式,但是在小程序部分的登录流程是一样的.之前就朦朦胧胧地用之前项目的逻辑改改直接用了,这个新项目要用就又结合官方文档重新梳理了下 ...
随机推荐
- #2059:龟兔赛跑(动态规划dp)
Problem Description 据说在很久很久以前,可怜的兔子经历了人生中最大的打击--赛跑输给乌龟后,心中郁闷,发誓要报仇雪恨,于是躲进了杭州下沙某农业园卧薪尝胆潜心修炼,终于练成了绝技,能 ...
- POJ - 3087:Shuffle'm Up (字符串模拟)
一.内容 题意:给定2个字符串s1,s2,将2个字符串进行重组成S,规则是S2最下面拿一个,S1最下面拿1个,直到所有块都用完. 二.思路 用map记录下S串结果,若以前访问过这个串代表不可能有结果直 ...
- 一、@Configuration、@Conponent 、@ComponentScan 注解等
一句话概括 区别: @Configuration 中所有带 @Bean 注解的方法都会被动态代理,因此调用该方法返回的都是同一个实例.2. 可以直接调用方法,不需要 @Autowired 注入后使用. ...
- 通义千问预体验,如何让 AI 模型应用“奔跑”在函数计算上?
立即体验基于函数计算部署通义千问预体验: https://developer.aliyun.com/topic/aigc_fc AIGC 浪潮已来,从文字生成到图片生成,AIGC 的创造力让人惊叹,更 ...
- oppo和海康嵌入式软件工程师面经总结
目录 海康 一面(3.23,35min) 自我介绍 项目介绍 你做的这个项目遇到了那些问题,如何解决的? 移植uboot,只做了移植吗? 用的那个文件系统? 移植过程中,网卡驱动做了那些工作? 写过那 ...
- uniapp picker组件实现二级联动
https://blog.csdn.net/hxh_csdn/article/details/111504951 https://www.cnblogs.com/jstll/p/14149600.ht ...
- 每天学五分钟 Liunx 0101 | 服务篇:创建进程
创建子进程 上一节说过创建子进程的三种方式: 1. fork 复制进程:fork 会复制当前进程的副本,产生一个新的子进程,父子进程是完全独立的两个进程,他们掌握的资源(环境变量和普通变量)是一样的. ...
- offline RL | ABM:从 offline dataset 的好 transition 提取 prior policy
ICLR 2020,6 6 6. 材料: 论文题目:Keep Doing What Worked: Behavior Modelling Priors for Offline Reinforcemen ...
- Java-Enum常量特定方法
OnJava8-Enum-常量特定方法 用枚举实现责任链模式 责任链(Chain Of Responsibility)设计模式先创建了一批用于解决目标问题的不同方法,然后将它们连成一条"链& ...
- MyBatis02:CRUD操作
CRUD操作 namespace 这个是命名空间,不可缺少,更不能写错 配置文件中namespace中的名称为对应Mapper接口或者Dao接口的全限定类名,完整包名,必须一致! Select(用于查 ...