重要组件

  • SecurityContext 上下文对象,Authentication(认证)对象会放在里面
  • SecurityContextHolder 用于拿到上下文对象的静态工具类
  • Authentication 认证接口,定义了认证对象的数据形式(数据格式)
  • AuthenticationManager 用于校验Authentication,返回一个认证后的Authentication对象

SecurityContext对象负责存放认证后的对象数据

public interface SecurityContext extends Serializable {
// 获取Authentication对象
Authentication getAuthentication(); // 放入Authentication对象
void setAuthentication(Authentication authentication);
}

主要的作用就是get and set

SecurityContextHolder 用于获取上下文对象的静态工具类。用于存储上下文对象的信息。包括当前的用户是谁,是否认证、角色权限.... 都被保存在SecurityContextHolder

public class SecurityContextHolder {

    //清除上下文数据
public static void clearContext() {
strategy.clearContext();
} //获取上下文数据
public static SecurityContext getContext() {
return strategy.getContext();
} //放入上下文数据
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
}

主要的作用就是获取和放入SecurityContext

Authentication认证接口,定义了认证对象的数据形式

public interface Authentication extends Principal, Serializable {
//获取用户权限
Collection<? extends GrantedAuthority> getAuthorities();
//获取证明用户的信息(密码等...)
Object getCredentials();
//用户额外信息(用户表信息)
Object getDetails();
//获取用户身份信息,未认证获取的是用户名,认证后获取的是UserDetails
Object getPrincipal();
//获取当前Authentication是否已认证
boolean isAuthenticated();
//设置当前Authentication是否已认证(true or false)
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

Authentication只是定义了一种在SpringSecurity进行认证的数据的数据形式应该是怎么样的。

AuthenticationManager【接口】是认证相关的核心接口,它定义了一个认证方法,它将一个未认证的Authentication传入,返回一个已认证的Authentication,它的默认实现类是ProviderManager

public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}

ProviderManagerAuthenticationManager 的默认实现类,其实很大一部分工具类都是围绕着这个 ProviderManager 实现类来的,由他衍生出来的 AuthenticationProvider 接口

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

    // 这里维护着一个 AuthenticationProvider 列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。
// 也就是说,核心的认证入口始终只有一个:AuthenticationManager
// 例如下面不同的认证方式,对应了三个 AuthenticationProvider:
// 1、用户名 + 密码(UsernamePasswordAuthenticationToken),
// 2、邮箱 + 密码,
// 3、手机号码 + 密码登录
// 在默认策略下,只需要通过一个 AuthenticationProvider 的认证,即可被认为是登录成功。
private List<AuthenticationProvider> providers = Collections.emptyList(); public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null; // ProviderManager 中的 List(providers),会依照次序去认证,认证成功则立即返回,
// 若认证失败则返回 null,下一个 AuthenticationProvider 会继续尝试认证,如果所有
// 认证器都无法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException 异常。
for (AuthenticationProvider provider : getProviders()) { // 这个 supports 方法用来判断此AuthenticationProvider 是否支持当前的 Authentication 对象
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication); if (result != null) {
copyDetails(authentication, result);
break;
}
}
...
catch (AuthenticationException e) {
lastException = e;
}
} // 如果有 Authentication 信息,则直接返回
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// 移除密码
((CredentialsContainer) result).eraseCredentials();
}
// 发布登录成功事件
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
... // 执行到此,说明没有认证成功,包装异常信息
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
}

AuthenticationProvider接口内部有两个实现类

public interface AuthenticationProvider {
//负责认证
Authentication authenticate(Authentication authentication)
throws AuthenticationException; //负责判断此AuthenticationProvider是否支持当前的 Authentication 对象。
boolean supports(Class<?> authentication);
}

就是输入一个凭证,输出一个Principal,输入输出都是封装在Authenticaiton里面的。

输入(credentials) ---->
AuthenticationProvider.authenticate()
输出(principal)<-------

而且可以有多种类型的认证方式。

​ 那这些 AuthenticationProvider 需要做什么工作呢?首先通过username取得数据库里面的用户数据,所以这时就可以使用这个UserDetailService.loadUserByUserName()来取得数据。这个UserDetailService会将用户数据填充到Authentication里面去【Authentication.getCredentials()】。

​ 最后的最后,为了让其他的过滤器也能够获取到这个认证的AuthenticationSpringSecurity会将其存储到上下文对象中。

DaoAuthenticationProvider就是AuthenticationProvider的实现类。

Dao 正是数据访问层的缩写,也暗示了这个身份认证器的实现思路,用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。

​ 在SpringSecurity中。提交的用户名和密码被封装成了UsernamePasswordAuthenticationToken,而根据用户名价值用户的认为则是提交给了UserDetailsService

如何取得用户?

​ 在DaoAuthenticationProvider中,对应的根据用户名加载用户的方法便是retrieveUser

// 虽然有两个参数,但是 retrieveUser 只有第一个参数(username)起主要作用,返回一个 UserDetails。
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;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
对比密码

​ 还需要完成UsernamePasswordAuthenticationTokenUserDetails密码的比对,这便是交给 additionalAuthenticationChecks方法完成的。

	这个方法是在父类 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法中被调用的,如果这个 void 方法没有抛异常,则认为比对成功。
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
//比对密码的过程,用到了 PasswordEncoder 和 SaltSource
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}

​ 总之就是 DaoAuthenticationProvider它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。

UserDetails它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。

public interface UserDetails extends Serializable {
//访问权限信息
Collection<? extends GrantedAuthority> getAuthorities();
//获取密码
String getPassword();
//获取用户名
String getUsername();
//判断是没过期
boolean isAccountNonExpired();
//是否没有被锁定
boolean isAccountNonLocked();
//是否没有超时
boolean isCredentialsNonExpired();
//用户是否可用
boolean isEnabled();
}

​ Authentication 的 getCredentials()UserDetails 中的 getPassword() 需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对

​ Authentication 中的 getAuthorities() 实际是由 UserDetailsgetAuthorities() 传递而形成的。

UserDetailsService

public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetailsService 它纯粹是一个用于用户数据的 DAO,除了向框架内的其他组件提供该数据之外,没有其他功能。常见的类型有,从数据库加载,从内存中加载。

总体认证流程:
  1. 首先是一个请求带着身份信息进来,用户名和密码被过滤器获取到,封装成Authentication默认情况下是UsernamePasswordAuthenticationToken这个实现类。
  2. 这个Authentication经过AuthenticationManager的认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的Authentication实例。
  3. SecurityContextHolder安全上下文容器将上面充满信息的Authentication通过SecurityContextHolder.getContext().setAuthentication(...)方法设置到SecurityContext上下文对象中。
  4. Authentication 对象中拿到我们的 UserDetails 对象,之前我们说过,认证后的 Authentication 对象调用它的 getPrincipal() 方法就可以拿到我们先前数据库查询后组装出来的 UserDetails 对象,然后创建 token。
  5. UserDetails 对象放入缓存中,方便后面过滤器使用。

这样的话就算完成了,,因为主要认证操作都会由 AuthenticationManager.authenticate() 帮我们完成。

authenticate方法

这个 authenticate 方法就是认证的核心方法,它位于 AbstractUserDetailsAuthenticationProvider 这个抽象类里面,而大部分具体的 AuthenticationProvider 都是先继承自这个抽象类。

// AbstractUserDetailsAuthenticationProvider
// 注意这个 AbstractUserDetailsAuthenticationProvider 类是上面的 DaoAuthenticationProvider 的父类,
// 认证部分都是在这个父类做的
public Authentication authenticate(Authentication authentication) { // 校验未认证的Authentication对象里面有没有用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName(); boolean cacheWasUsed = true;
// 从缓存中去查用户名为XXX的对象
UserDetails user = this.userCache.getUserFromCache(username);
// 如果没有就进入到这个方法
if (user == null) {
cacheWasUsed = false;
try {
// 调用我们重写 UserDetailsService 的 loadUserByUsername 方法
// 拿到我们自己组装好的 UserDetails 对象
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
} else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// 校验账号是否禁用
preAuthenticationChecks.check(user);
// 校验数据库查出来的密码,和我们传入的密码是否一致
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
}
逻辑模型
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
AuthenticationManager am = new SampleAuthenticationManager();
while(true) {
System.out.println("请输入您的用户名:");
String name = in.readLine();
System.out.println("请输入您的密码:");
String password = in.readLine(); try {
// 1、封装一个 UsernamePasswordAuthenticationToken 对象
Authentication request = new UsernamePasswordAuthenticationToken(name, password); // 2、经过 AuthenticationManager 的认证,如果认证失败会抛出一个 AuthenticationException 错误
Authentication result = am.authenticate(request); // 3、将这个认证过的 Authentication 填入 SecurityContext 里面
SecurityContextHolder.getContext().setAuthentication(result);
break;
} catch(AuthenticationException e) {
System.out.println("认证失败:" + e.getMessage());
}
} System.out.println("认证成功: Security context contains:\n" +
SecurityContextHolder.getContext().getAuthentication());
} // 关键认证部分
@Override
public Authentication authenticate(Authentication auth) throws AuthenticationException { // getCredentials 返回的是密码,这里随便写了,直接用户名和密码一致就算登陆成功
if (auth.getName().equals(auth.getCredentials())) {
// 认证成功返回一个已经认证的 UsernamePasswordAuthenticationToken 的对象,并把这个用户的权限填入
return new UsernamePasswordAuthenticationToken(auth.getName(),
auth.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}

SpringSecurity认证流程分析的更多相关文章

  1. 阶段5 3.微服务项目【学成在线】_day17 用户认证 Zuul_01-用户认证-用户认证流程分析

    1 用户认证 1.1 用户认证流程分析 用户认证流程如下: 访问下面的资源需要携带身份令牌和jwt令牌,客户端可以通过身份认证的令牌从服务端拿到长令牌, 一会要实现认证服务请求用户中心从数据库内来查询 ...

  2. Spring Security认证流程分析--练气后期

    写在前面 在前一篇文章中,我们介绍了如何配置spring security的自定义认证页面,以及前后端分离场景下如何获取spring security的CSRF Token.在这一篇文章中我们将来分析 ...

  3. SpringSecurity认证流程详解

    SpringSecurity基本原理 在之前的文章<SpringBoot + Spring Security 基本使用及个性化登录配置>中对SpringSecurity进行了简单的使用介绍 ...

  4. SpringSecurity认证流程

    SpringSecurity配置 SecurityConfig.java @Override protected void configure(HttpSecurity http) throws Ex ...

  5. 手把手带你撸一把springsecurity框架源码中的认证流程

    提springsecurity之前,不得不说一下另外一个轻量级的安全框架Shiro,在springboot未出世之前,Shiro可谓是颇有统一J2EE的安全领域的趋势. 有关shiro的技术点 1.s ...

  6. rest_framework框架之认证功能的使用和源码实现流程分析

    rest_framework框架之认证的使用和源码实现流程分析 一.认证功能的源码流程 创建视图函数 Note 创建视图函数后,前端发起请求,url分配路由,执行视图类,视图类中执行对应方法必须经过d ...

  7. DRF认证流程及源码分析

    认证 前言 用户验证用户是否合法登陆. 部分内容在DRF视图的使用及源码流程分析讲解,建议先看讲解视图的这篇文章. 使用流程 认证使用的方法流程如下: 自定义认证类,继承BaseAuthenticat ...

  8. Spring Security拦截器加载流程分析--练气中期

    写在前面 上回我们讲了spring security整合spring springmvc的流程,并且知道了spring security是通过过滤器链来进行认证授权操作的.今天我们来分析一下sprin ...

  9. freeswitch呼叫流程分析

    今天翻文档时发现之前整理的关于freeswitch呼叫相关的内容,写成博文分享出来也方便我以后查阅. 整体结构图 FreeswitchCore 模块加载过程 freeswitch主程序初始化时会从mo ...

  10. CWMP开源代码研究4——认证流程

    TR069 Http Digest 认证流程   一 流程及流程图 1.1盒端主动发起Http Digest认证流程  盒端CPE                                    ...

随机推荐

  1. C4996 'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS.

    错误原因 VS平台认为scanf函数不安全,要求换成scanf_s函数 解决方案 方案一:将scanf换成scanf_s[不建议] 将scanf换成scanf_s 但是,scanf_s函数只能在vs上 ...

  2. 全网最适合入门的面向对象编程教程:56 Python字符串与序列化-正则表达式和re模块应用

    全网最适合入门的面向对象编程教程:56 Python 字符串与序列化-正则表达式和 re 模块应用 摘要: Python 的 re 模块提供了强大的正则表达式操作功能,用于在字符串中搜索.匹配.替换等 ...

  3. 简单粗暴的实现 Blazor Server 登录鉴权

    既然是简单粗暴,那么就不用关心诸如 IDentityServer4,OAuth 之类的组件,也不使用 AuthenticationStateProvider.IAuthService, razor 页 ...

  4. G-数据结构-G

    \[\huge 近日多做数据结构题,或恐后再读不能醒悟,或记其思路,或骂出题人,或不想刷题,虽有此篇. \] \[\] \[\] \[\] \[\] T1 距离 首先这题部分分很多,直接 $ O (n ...

  5. 初探AI之got-ocr2.0大模型本地部署与遇到的各种坑处理

    一.环境搭建 1.安装cuda,本人使用的是12.1版本,下载地址:https://developer.nvidia.com/cuda-12-1-1-download-archive 2.安装cond ...

  6. 自学PHP笔记(四) PHP变量和常量

    PHP中变量有普通变量.可变变量和预定义变量,而常量就是普通变量和预定义变量. 1. 变量 在PHP中变量是内存中得一个命名单元,在系统中为程序中每个变量都分配一个存储单元,在这些存储单元中可以存储任 ...

  7. 2021年华为Java面试真题解析,帮你解决95%以上的问题!

    前言 由于作者面试过程中高度紧张,本文中只列出了自己还记得的部分题目. 经历了漫长一个月的等待,终于在前几天通过面试官获悉已被蚂蚁金服录取,这期间的焦虑.痛苦自不必说,知道被录取的那一刻,一整年的阴霾 ...

  8. SSIS连接Excel2007版本之后的数据源

    今天我发现了新大陆,兴奋得不得了,由于原文写得太过详细与专业,我就偷偷懒直接Copy过来了,怕自己以后没地儿找,哈哈哈 原文链接:https://www.cnblogs.com/biwork/p/34 ...

  9. selenium3环境搭建,Firefox与对应的geckodriver, chrome与对应的Chromedriver

    Firefox与对应的geckodriver 火狐下载:http://ftp.mozilla.org/pub/firefox/releases/ geckodriver下载:https://githu ...

  10. 华为云开源时序数据库openGemini:使用列存引擎解决时序高基数问题

    本文来源:<华为云DTSE>第五期开源专刊,作者:向宇,华为云数据库高级研发工程师.黄飞腾,博士,openGemini存储引擎架构师 在时序数据场景中,大部分的解决方案是以时间线为粒度对时 ...