一些权限框架一般都包含认证器和决策器,前者处理登陆验证,后者处理访问资源的控制

Spring Security的登陆请求处理如图

下面来分析一下是怎么实现认证器的

拦截请求

首先登陆请求会被UsernamePasswordAuthenticationFilter拦截,这个过滤器看名字就知道是一个拦截用户名密码的拦截器

主要的验证是在attemptAuthentication()方法里,他会去获取在请求中的用户名密码,并且创建一个该用户的上下文,然后在去执行一个验证过程

String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
//创建上下文
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);

可以看看UsernamePasswordAuthenticationToken这个类,他是继承了AbstractAuthenticationToken,然后这个父类实现了Authentication

由这个类的方法和属性可得知他就是存储用户验证信息的,认证器的主要功能应该就是验证完成后填充这个类

回到UsernamePasswordAuthenticationToken中,在上面创建的过程了可以发现

public UsernamePasswordAuthenticationToken(Object principal,Object credentials){
super(null);
this.principal=principal;
this.credentials=credentials;
//还没认证
setAuthenticated(false);
}

还有一个super(null)的处理,因为刚进来是还不知道有什么权限的,设置null是初始化一个空的权限

//权限利集合
private final Collection<GrantedAuthority> authorities;
//空的集合
public static final List<GrantedAuthority> NO_AUTHORITIES = Collections.emptyList();
//初始化
if (authorities == null) {
this.authorities = AuthorityUtils.NO_AUTHORITIES;
return;
}

那么后续认证完还会把权限设置尽量,此时可以看UsernamePasswordAuthenticationToken的另一个重载构造器

//认证完成
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}

在看源码的过程中,注释一直在强调这些上下文的填充和设置都应该是由AuthenticationManager或者AuthenticationProvider的实现类去操作

验证过程

接下来会把球踢给AuthenticationManager,但他只是个接口

/**
* Attempts to authenticate the passed {@link Authentication} object, returning a
* fully populated <code>Authentication</code> object (including granted authorities)
* if successful.
**/
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}

注释也写的很清楚了,认证完成后会填充Authentication

接下来会委托给ProviderManager,因为他实现了AuthenticationManager

刚进来看authenticate()方法会发现他先遍历了一个List<AuthenticationProvider>集合

/**
* Indicates a class can process a specific Authentication
**/
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
//支不支持特定类型的authentication
boolean supports(Class<?> authentication);
}

实现这个类就可以处理不同类型的Authentication,比如上边的UsernamePasswordAuthenticationToken,对应的处理类是AbstractUserDetailsAuthenticationProvider,为啥知道呢,因为在这个supports()

public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}

注意到这个是抽象类,实际的处理方法是在他的子类DaoAuthenticationProvider里,但是最重要的authenticate()方法子类好像没有继承,看看父类是怎么实现这个方法的

  1. 首先是继续判断Authentication是不是特定的类

     Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
    () -> messages.getMessage(
    "AbstractUserDetailsAuthenticationProvider.onlySupports",
    "Only UsernamePasswordAuthenticationToken is supported"));
  2. 查询根据用户名用户,这次就是到了子类的方法了,因为这个方法是抽象的

     user=retrieveUser(username,
    (UsernamePasswordAuthenticationToken)authentication);

    接着DaoAuthenticationProvider会调用真正实现查询用户的类UserDetailsService

    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

    UserDetailsService这个类信息就不陌生了,我们一般都会去实现这个类来自定义查询用户的方式,查询完后会返回一个UserDetails,当然也可以继承这个类来扩展想要的字段,主要填充的是权限信息和密码

  3. 检验用户,如果获取到的UserDetails是null,则抛异常,不为空则继续校验

    //检验用户合法性
    preAuthenticationChecks.check(user);
    //校验密码
    additionalAuthenticationChecks(user,
    (UsernamePasswordAuthenticationToken) authentication);

    第一个教育是判断用户的合法性,就是判断UserDetails里的几个字段

    //账号是否过期
    boolean isAccountNonExpired();
    //账号被锁定或解锁状态。
    boolean isAccountNonLocked();
    //密码是否过期
    boolean isCredentialsNonExpired();
    //是否启用
    boolean isEnabled();

    第二个则是由子类实现的,判断从数据库获取的密码和请求中的密码是否一致,因为用的登陆方式是根据用户名称登陆,所以有检验密码的步骤

     String presentedPassword = authentication.getCredentials().toString();
    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"));
    }

    需要主要的是请求中的密码是被加密过的,所以从数据库获取到的密码也应该是被加密的

    注意到当完成校验的时候会把信息放入缓存

    //当没有从缓存中获取到值时,这个字段会被设置成false
    if (!cacheWasUsed) {
    this.userCache.putUserInCache(user);
    }
    //下次进来的时候回去获取
    UserDetails user = this.userCache.getUserFromCache(username);

    如果是从缓存中获取,也是会走检验逻辑的

    最后完成检验,并填充一个完整的Authentication

    return createSuccessAuthentication(principalToReturn, authentication, user);

由上述流程来看,Security的检验过程还是比较清晰的,通过AuthenticationManager来委托给ProviderManager,在通过具体的实现类来处理请求,在这个过程中,将查询用户的实现和验证代码分离开来

整个过程看着像是策略模式,后边将变化的部分抽离出来,实现解耦

返回完整的Authentication

前边提到的认证成功会调用createSuccessAuthentication()方法,里边的内容很简单

UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}

这次往supe里放了权限集合,父类的处理是判断里边的权限有没有空的,没有则转换为只读集合

for (GrantedAuthority a : authorities) {
if (a == null) {
throw new IllegalArgumentException(
"Authorities collection cannot contain any null elements");
}
}
ArrayList<GrantedAuthority> temp = new ArrayList<>(
authorities.size());
temp.addAll(authorities);
this.authorities = Collections.unmodifiableList(temp);

收尾工作

回到ProviderManager里的authenticate方法,当我们终于从

result = provider.authenticate(authentication);

走出来时,后边还有什么操作

  1. 将返回的用户信息负责给当前的上下文
   if (result != null) {
copyDetails(authentication, result);
break;
}
  1. 删除敏感信息

    ((CredentialsContainer) result).eraseCredentials();

    这个过程会将一些字段设置为null,可以实现eraseCredentials()方法来自定义需要删除的信息

最后返回到UsernamePasswordAuthenticationFilter中通过过滤

结论

这就是Spring Security实现认证的过程了

通过实现自己的上下文Authentication和处理类AuthenticationProvider以及具体的查询用户的方法就可以自定义自己的登陆实现

具体可以看Spring Security自定义认证器

Spring Security认证器实现的更多相关文章

  1. Spring Security认证配置(三)

    学习本章之前,可以先了解下上篇Spring Security认证配置(二) 本篇想要达到这样几个目的: 1.登录成功处理 2.登录失败处理 3.调用方自定义登录后处理类型 具体配置代码如下: spri ...

  2. spring security 认证源码跟踪

    spring security 认证源码跟踪 ​ 在跟踪认证源码之前,我们先根据官网说明一下security的内部原理,主要是依据一系列的filter来实现,大家可以根据https://docs.sp ...

  3. Spring Security 入门(1-4-1)Spring Security - 认证过程

    理解时可结合一下这位老兄的文章:http://www.importnew.com/20612.html 1.Spring Security的认证过程 1.1.登录过程 - 如果用户直接访问登录页面 用 ...

  4. Authentication讲解(Spring security认证)

    标准认证过程: 1.用户使用username和password登录 2.系统验证这个password对于该username是正确的 3.假设第二步验证成功,获取该用户的上下文信息(如他的角色列表) 4 ...

  5. Spring Security认证配置(二)

    学习本章之前,可以先了解下上篇Spring Security基本配置. 本篇想要达到这样几个目的: 1.访问调用者服务时,如果是html请求,则跳转到登录页,否则返回401状态码和错误信息 2.调用方 ...

  6. spring security认证

    1 开发基于表单的认证 Spring security核心的功能 认证(你是谁?) 授权(你能干什么?) 攻击防护(防止伪造身份) spring security实现了默认的用户名+密码认证,默认用户 ...

  7. Spring Security 入门(1-4-2)Spring Security - 认证过程之AuthenticationProvider的扩展补充说明

    1.用户信息从数据库获取 通常我们的用户信息都不会向第一节示例中那样简单的写在配置文件中,而是从其它存储位置获取,比如数据库.根据之前的介绍我们知道用户信息是通过 UserDetailsService ...

  8. Spring Security认证配置(一)

    学习本章之前,可以先了解下上篇 Spring Security基本配置. 本篇主要讲述Spring Security基于表单,自定义用户认证配置(上篇中的配置,本篇将不再阐述).一共分为三步: 1.处 ...

  9. Authentication(Spring Security 认证笔记)

    这篇文章是对Spring Security的Authentication模块进行一个初步的概念了解,知道它是如何进行用户认证的 考虑一个大家比较熟悉的标准认证过程: 1.用户使用username和pa ...

随机推荐

  1. Zalando Postgres Operator 快速上手

    本指南旨在让您快速了解在本地 Kubernetes 环境中使用 Postgres Operator. 前提条件 由于 Postgres Operator 是为 Kubernetes (K8s) 框架设 ...

  2. 记一次线上websocket返回400问题排查

    现象 生产环境websocket无法正常连接,服务端返回400 bad request,开发及测试环境均正常. 抓包排查 src:nginx服务器 172.16.177.193dst:imp应用服务器 ...

  3. 推荐一个我写的Python库——PyNAS

    介绍 PyNAS是一个以Python的Updog的库为基础,制作而来的库 安装 pip安装(推荐) pip install PyNAS 源码安装(推荐) Github: git clone https ...

  4. 基于全志A40i开发板——Linux-RT内核应用开发教程(1)

    目录 1 Linux-RT内核简介 3 2 Linux系统实时性测试 3 3 rt_gpio_ctrl案例 10 4 rt_input案例 15 本文为Linux-RT内核应用开发教程的第一章节--L ...

  5. 不care工具,在大数据平台中Hive能自动处理SQL

    摘要:有没有更简单的办法,可以直接将SQL运行在大数据平台? 本文分享自华为云社区<Hive执行原理>,作者: JavaEdge . MapReduce简化了大数据编程的难度,使得大数据计 ...

  6. .Net中字符串不变性与相等判断的特殊场景

    今天写bug的时候帮同事解决了一个有趣的问题,可能很多人都会答错.分享给大家. 问题 请看以下例子,并回答问题. var s1 = "12"; var s2 = "12& ...

  7. SpringBoot 三层架构 Controller、Service、Dao作用和关系详解

    首先创建一个springboot项目. model层 model层也叫pojo层或者entity层,个人比较喜欢pojo层. 一般数据库的一张表对应一个pojo层,并且表中所有字段都在pojo层都一一 ...

  8. jQuery与JavaScript与Ajax三者的区别与联系

    简单总结: 1.JS是一门 前端语言. 2.Ajax是一门 技术,它提供了异步更新的机制,使用客户端与服务器间交换数据而非整个页面文档,实现页面的局部更新. 3.jQuery是一个 框架,它对JS进行 ...

  9. 基于casbin的RBAC权限实践

    五一假期疫情封在家也没事做,就想来优化一下一个前端容器小项目 之前的TODOlist里面有一项是权限这块时隔2年了还一直没有动手 迟迟没搞主要还是我太懒了,哈哈 其实我一直想要找一个轻量级的权限通用方 ...

  10. 用 Docker 快速搭建 Kafka 集群

    开源Linux 一个执着于技术的公众号 版本 •JDK 14•Zookeeper•Kafka 安装 Zookeeper 和 Kafka Kafka 依赖 Zookeeper,所以我们需要在安装 Kaf ...