Spring Security 学习(一)认证与授权源码分析——一次痛苦的爬坑经历

2023-11-01

一点感悟:

一个疏忽,花了 5h 解决了,哎。用一首歌来表达一下现在的心情:点击

不过也算摸清了Spring Security 一点基本原理,没有白费的时间......

学习新知识的时候,遇到解决不了的问题一定不能心急,越是这个时候越要静下心来一步一步的去分析原理。静心、沉淀。


一、认证过程 :

废话不多说,在学习之前最好先把用到的英语单词熟悉一下。 AuthenticationManager(认证管理器接口),authenticate(认证的方法),ProviderManager,AuthenticationManager

官方文档:https://spring.io/guides/topicals/spring-security-architecture/

先来了解一下以下几个接口,方便后面的学习。

AuthenticationManager :

Authentication

The main strategy interface for authentication is AuthenticationManager which only has one method:

public interface AuthenticationManager {

  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;

}

An AuthenticationManager can do one of 3 things in its authenticate() method:

  1. return an Authentication (normally with authenticated=true) if it can verify that the input represents a valid principal.

  2. throw an AuthenticationException if it believes that the input represents an invalid principal.

  3. return null if it can’t decide.

  • 身份认证的主要接口为 Authentication,其中只有一个方法 authenticate,用来验证。
  • 主要干三件事:

             1. 如果能验证是正确的,就返回一个 Authentication 对象,并把 authenticated=true。

             2. 如果没通过验证,则抛出一个异常。

             3. 如果无法判断,则返回 null。


ProviderManager :

The most commonly used implementation of AuthenticationManager is ProviderManager, which delegates to a chain of AuthenticationProvider instances. An AuthenticationProvider is a bit like an AuthenticationManager but it has an extra method to allow the caller to query if it supports a given Authentication type:

  • ProviderManager 是 Authentication接口 最常用的一个实现类。
  • ProviderManager 会把验证请求委托给 AuthenticationManager。下面是 ProviderManager 的 authenticate 方法源码:


AuthenticationManager :

 

public interface AuthenticationProvider {

	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

	boolean supports(Class<?> authentication);

}
  • 只有通过了 support 即返回 true,才会继续调用 authenticate方法。
  • 注意 AuthenticationManager 是一个接口。

而 AuthenticationManager 有一个子类 AbstractUserDetailsAuthenticationProvider,用于实现用户信息认证的 UserDetailsService 就是 AbstractUserDetailsAuthenticationProvider 的子类。说这一大堆肯定头晕了,看下面图就懂了。

 

 

总结一下过程:

AuthenticationManager 是实现认证的主要策略接口,其认证逻辑主要由子类 ProviderManager实现,而 ①ProviderManager 将委托给 AuthenticationProvider 类型的对象处理,AuthenticationProvider 有一个子类  AbstractUserDetailsAuthenticationProvider此类中 authenticate 方法调用抽象方法 retrieveUser 来获取用户信息,该抽象方法由子类 DaoAuthenticationProvider 实现,DaoAuthenticationProvider是Spring Security中一个核心的Provider,对所有的数据库提供了基本方法和入口。

估计你已经不想往下看了,我主要是为以后自己看的....

AbstractUserDetailsAuthenticationProvider源码:

public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
    //取出用户名
    String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);

    if (user == null) {
        cacheWasUsed = false;
        try {
            //获取用户信息由子类实现即 DaoAuthenticationProvider
            user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (UsernameNotFoundException notFound) {
            logger.debug("User '" + username + "' not found");
            if (hideUserNotFoundExceptions) {
                throw new BadCredentialsException(messages.getMessage("msg...", "msg..."));
            }
            else {
                throw notFound;
            }
        }
        Assert.notNull(user, "msg...");
    }
    /**
     * 到此用户信息获取完毕
     * 下一步进行认证
     * 认证过程一共分为三步
     *  1. preAuthenticationChecks
     * 	2. additionalAuthenticationChecks(抽象方法,子类实现)
     * 	3. postAuthenticationChecks
     */
    try {
        //下面这两个方法是 UserDetailsChecker 接口的实现类
        //前检查由 DefaultPreAuthenticationChecks 类实现(主要判断当前用户是否锁定,过期,冻结User接口)
        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);
    //如果没有缓存则进行缓存,则处的userCache是由NullUserCache类实现的,名如其义,该类的putUserInCache没做任何事
    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }
    Object principalToReturn = user;
    if (forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }
    //将认证之后的信息封装成 token
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

下面看一下 DaoAuthenticationProvider 源码:

private UserDetailsService userDetailsService;

protected final UserDetails retrieveUser(String username,
		UsernamePasswordAuthenticationToken authentication)
		throws AuthenticationException {

	prepareTimingAttackProtection();
	try {
                //1.类型为 UserDetails
		UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
		if (loadedUser == null) {
			throw new InternalAuthenticationServiceException("msg...");
		}
		return loadedUser;
	}
	catch (UsernameNotFoundException ex) {
	    .....
	}
}

可以看出,获取用户信息最后是交由 DaoAuthenticationProvider 中 getUserDetailsService().loadUserByUsername(username) 进行加载,所以在写从数据库中获取用户信息时只需要实现UserDetailsService 并重写 loadUserByUsername() 方法就行了。注意返回的用户信息是 UserDetails 类型的,所以 model 需要实现 UserDetails。

private UserDetailsService userDetailsService;

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

自定义的UserDetails需要注入到 AuthenticationManagerBuilder 中才行,我就是忘了这一步.....

@Autowired
private UserService userService;


@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService);
}

二、认证与授权总流程 :

说到这,应该对 Spring Security 认证原理有了初步的了解。再总结一下流程:

 

1.请求被 UsernamePasswordAuthenticationFilter 拦截 : 

①把输入的用户名个密码封装成 Token 也就是 Authentication ,因为..Token 是 Authentication的子类

UsernamePasswordAuthenticationToken authRequest = new             
    UsernamePasswordAuthenticationToken(username, password);

②​​调用 AuthenticationManager 的 authenticate 方法进行认证

return this.getAuthenticationManager().authenticate(authRequest);

 

2.由 AuthenticationManager 将验证请求委托给 ProviderManager 执行:

ProviderManager 中 authenticate 方法源码:

Authentication result = null;

public Authentication authenticate(Authentication authentication) throws         
    AuthenticationException {
    for (AuthenticationProvider provider : getProviders()) {
        result = provider.authenticate(authentication);
        return result;
    }
}

 

3.ProviderManager遍历 provider 执行验证:

在authenticate 方法中遍历 provider 执行验证,到这里我就发现还有一中方法实现自定义的验证,就是自定义一个类实现AuthenticationProvider 接口,重写 authenticate 与 support 方法。就像下面一样:

@Configuration
public class MyProvider implements AuthenticationProvider {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String username = (String)authentication.getPrincipal();
        //根据用户名查询处所有权限
        List<Permission> list = userMapper.getPermissionByUsername(username);

        //构造权限集合
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Permission p:list){
            authorities.add(new SimpleGrantedAuthority(p.getPername()));
        }
        //构造一个 Authentication
        return new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(), authorities);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }
}

 

4.DaoAuthenticationProvider 类执行验证:

如果没有自定义的 provider 验证逻辑,则会由实现于 ProviderManager接口的抽象类AbstractUserDetailsAuthenticationProvider 的子类 DaoAuthenticationProvider 进行认证,其认证过程:

1.调用 retrieveUser方法从数据库中取出数据

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

2.执行3个校验方法

preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
		(UsernamePasswordAuthenticationToken) authentication);
postAuthenticationChecks.check(user);

3.如果校验成功返回一个 Authentication 实例

return createSuccessAuthentication(principalToReturn, authentication, user);

protected Authentication createSuccessAuthentication(Object principal,
		Authentication authentication, UserDetails user) {
	// Ensure we return the original credentials the user supplied,
	// so subsequent attempts are successful even with encoded passwords.
	// Also ensure we return the original getDetails(), so that future
	// authentication events after cache expiry contain the details
	UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
			principal, authentication.getCredentials(),
			authoritiesMapper.mapAuthorities(user.getAuthorities()));
	result.setDetails(authentication.getDetails());
}

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring Security 学习(一)认证与授权源码分析——一次痛苦的爬坑经历 的相关文章

随机推荐

  • 又一个开源工具搞完了,工作效率直接翻倍

    博客首页 派 大 星 欢迎关注 点赞 收藏 留言 本文由派大星原创编撰 系列专栏 开源专栏 本系列主要输出作者自创的开源项目 作品 低代码生成器平台 大家好 我是派大星 距离上一次开发出开源项目的时间已经过去一段时间了 也不知道大家有没有使
  • 使用本地mysql+linux实现mysql主从同步

    1 配置linux 保证linux已经安装好了mysql 1 1修改该linux配置文件 vim etc my cnf 1 2重启linux的mysql systemctl restart mysqld 1 3使用账户密码登录linux中的
  • Arduino小车资料整理

    目录 一 小车简介 二 材料清单 三 Arduino UNO R3简介及使用说明 四 各模块安装接线及测试 1 驱动模块接线及测试 1 减速直流电机 2 L298N电机驱动模块 3 具体接线 4 代码及测试 2 巡线模块接线及测试 1 TC
  • 解决 font-weight 无效的问题

    近期调页面时有几个 font weight 需要修改 无论怎么调整字体粗细都没有变化 深入研究后总结下文 初探 本地写个例子 代码如下 p class thin This is a paragraph p p class normal Th
  • springboot 定时任务@Scheduled 和 异步@Async

    使用 EnableScheduling开启功能 Configuration EnableScheduling public class ScheduleConfig 编写任务 Component public class Scheduled
  • linux下使用动态壁纸(fantascene)

    让你的linux桌面动起来 幻梦动态壁纸 我也是突发奇想 做了这么一个程序 目前在多个linux下可以运行 支持双屏 理论上说支持mpv gt 27 0 qt gt 5 6的系统版本可编译 ubuntu16 04可以自行删减代码和编译 或者
  • java玫瑰花代码_Java版给爱人表白的玫瑰花程序代码

    1 书写表白语句的frame 渐入功能 package com wanju blessing import java awt Color import java awt Container import java awt Dimension
  • 常用眼底图像数据集简介及下载--糖尿病视网膜病变 EyePacs,APTOS2019,STARE数据集

    1 糖尿病视网膜病变图像介绍 1 微动脉瘤通常出现在病变早期 它是由于眼部毛细血管缺氧导致血管壁变薄 从而在视网膜上呈现出深红色的点状物 2 出血点一般出现在血管附近 它是由于血管阻塞导致血液渗出形成的 呈现暗斑状 3 软性和硬性渗出物的形
  • python中变量,python中变量的概念

    python中变量的概念 在python中 变量就是一种标识符 它是数据的名字 更专业的理解 变量是内存中数据的引用 编程语言里的变量和初中学习代数时的方程变量很相似 前面学习数字类型 bool类型时 我们一直在交互式解释器里进行操作 目的
  • java springboot 实现从数据库查询数据下载为md格式文件

    java springboot 实现从数据库查询数据下载为md格式文件 param param response 功能描述 下载文件 标题 byId getTitle 内容 byId getTextContent 格式 response s
  • 增强型PWM(EPWM)如何输出互补功能?

    1 概念 互补 两根线 输出的PWM 只有一端导通 和死区概念类似 死区时间 指在这段时间 上下都没有输出 带死区的PWM波可以防止上下两个器件同时导通 也就是说 当一个器件导通后关闭 再经过一段死区 这时才能让另一个导通 例如 红色线条的
  • nuxt百度收录

    import cheerio from cheerio export default Global page headers https go nuxtjs dev config head mode universal 修改百度收录 hoo
  • 04 ImageView中图片保存到文件

    最近做的一个小App中的一个功能 把ImageView中的图片保存为一个 jpg文件 如果设备上有SDCard 图片会被保存到SD卡上 如果没有则保存在设备的存储空间中 这里主要包含了两个要点 一是 Android文件保存时文件夹的创建 二
  • detectron2概述

    目录 detectron2框架 configs datasets README md prepare for tests sh prepare panoptic fpn py demo demo py predictor py detect
  • 关于Docker如何安装nginx

    目录 1 Nginx 1 2 安装nginx 2 容器之间相互通信 2 1 两个容器在同一网段 2 2 两个容器在不同网段 1 Nginx Nginx也是一款服务器 我们常用它做如 反向代理 负载均衡 动态与静态资源的分离的工作 反向代理
  • C语言-数据结构-栈(静态栈与动态栈)

    一 简介 在哔哩哔哩看视频学的 赫斌老师数据结构入门的内容 b站搜索 av6159200 P33 通过学习 能独立把赫斌老师教的敲出来 由于动态栈 链表阉割版 的功能很少 我并没有增加什么其它功能 但是我自己实现了静态栈 数组阉割版 还有就
  • 卸载联软UniAccess,删除UniAccess Agent记录

    UniAccess 卸载 公司假以安全上网为由 让公司员工安装所谓的 XX上网助手 实则是内嵌了联软的UniAccess监控系统 有关这个软件的用途就不用多介绍了 能找到这里的 我想已经对这个 流氓 软件有了基本的认识 话不多说 赶紧想办法
  • Kafka使用工具封装

    maven依赖
  • c# redis hashid如何设置过期时间_Redis系列(三):Redis持久化机制(RDB & AOF)

    在前两篇关于Redis的文章中 已经详细的介绍了Redis常用的数据结构相关内容 如果还没看的小伙伴可以先过一遍 Redis基本数据类型 Redis跳跃表详解 本篇文章主要介绍 Redis数据持久化机制 RDB AOF 在此之前需要先了解一
  • Spring Security 学习(一)认证与授权源码分析——一次痛苦的爬坑经历

    一点感悟 一个疏忽 花了 5h 解决了 哎 用一首歌来表达一下现在的心情 点击 不过也算摸清了Spring Security 一点基本原理 没有白费的时间 学习新知识的时候 遇到解决不了的问题一定不能心急 越是这个时候越要静下心来一步一步的