SpringSecurity主要流程及扩展实现

news2025/1/12 17:40:07

解析主流的SpringSecurity安全框架,结合若依框架进行分析。

文章目录

  • 概述
    • 登录流程
    • 分析SecurityConfig配置类
      • 设置过滤请求
      • 添加过滤器
      • 注册认证provider/获取用户详情服务
    • 关键过滤器
  • 源码分析
    • SpringSecurity实现
    • 若依token生成逻辑
      • 创建令牌
      • 设置用户代理信息
      • 刷新令牌有效期
      • 生成JWT
    • 解析Jwt的过滤器原理
      • 获取用户信息
      • 校验token过期时间
  • 扩展:自定义实现用户鉴权
  • 总结

概述

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。

个人理解就是一个用户请求过来,过滤器就像漏斗一样进行层层筛选,直到检验用户请求合法则放通否则失败。

若依项目关键类:

  • SecurityConfig:spring security配置类
  • UserDetailsServiceImpl:自定义用户校验类
  • JwtAuthenticationTokenFilter:自定义JWT过滤器

SpringSecurity源码关键类:

  • UsernamePasswordAuthenticationFilter:实际并未执行

  • ProviderManager:AuthenticationManager的实现类,遍历多个provider,获取到符合要求的provider。在扩展章节中得到体现

  • AbstractUserDetailsAuthenticationProvider:实现了AuthenticationProvider接口的用户认证实现抽象类(默认实现)

    authenticate()认证方法

    supports()支持某类认证的校验

  • UsernamePasswordAuthenticationToken:认证对象(默认实现)

登录流程

在这里插入图片描述

查看com.ruoyi.framework.web.service.SysLoginService的login()方法,大体流程:

image-20221207110352794

分析SecurityConfig配置类

设置过滤请求

image-20221207180155148

添加过滤器

image-20221207180315711

注册认证provider/获取用户详情服务

image-20221207180542103

关键过滤器

观察SecurityConfig,我们能够发现以下过滤器

  1. LogoutSuccessHandlerImpl实现LogoutSuccessHandler:登出请求过滤器
  2. JwtAuthenticationTokenFilter继承OncePerRequestFilter:校验接口请求用户权限(自定义实现)
  3. CorsFilter继承OncePerRequestFilter:解决跨域
// 查看配置类
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;
    
    /**
     * 跨域过滤器
     */
    @Autowired
    private CorsFilter corsFilter;
    
    // ...
    
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        // 添加Logout filter
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
    }
    
    
}

image-20221208164033304

实际上主要的过滤器还有:

  1. org.springframework.security.web.context.SecurityContextPersistenceFilter

    首当其冲的一个过滤器,非常重要 主要是使用SecurityContextRepository在session中保存或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文,SecurityContext中存储了当前用户的认证和权限信息。

  2. org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter

    此过滤器用于继承SecurityContext到Spring异步执行机制中的WebAsyncManager,和spring整合必须的。

  3. org.springframework.security.web.header.HeaderWriterFilter

    向请求的header中添加响应的信息,可以在http标签内部使用security:headers来控制

  4. org.springframework.security.web.csrf.CsrfFilter

    Csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,如果不包含则报错,起到防止csrf攻击的效果

  5. org.springframework.security.web.authentication.logout.LogoutFilter

    匹配URL为/logout的请求,实现用户退出,清除认证信息

  6. org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter(并未加入到过滤器链路中)

    认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求

  7. org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter

    如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认的认证界面

  8. org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter

    由此过滤器生成一个默认的退出登录页面

  9. org.springframework.security.web.authentication.www.BasicAuthenticationFilter

    此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头部信息

  10. org.springframework.security.web.savedrequest.RequestCacheAwareFilter

    通过HttpSessionRequestCache内部维护一个RequestCache,用于缓存HttpServletRequest

  11. org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter

    针对ServletRequest进行一次包装,使得request具有更加丰富的API

  12. org.springframework.security.web.authentication.AnonymousAuthenticationFilter

    当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存储到SecurityContextHolder中,SpringSecurity为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份

  13. org.springframework.security.web.session.SessionManagementFilter

    SecurityContextRepository限制同一个用户开启多个会话的数量

  14. org.springframework.security.web.access.ExceptionTranslationFilter

    异常转换过滤器位于整个SpringSecurityFilterChain的后方,用来转换整个链路中出现的异常

  15. org.springframework.security.web.access.intercept.FilterSecurityInterceptor

    获取所有配置资源的访问授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。

源码分析

SpringSecurity实现

分析默认实现,创建认证对象UsernamePasswordAuthenticationToken,将认证对象丢入到authenticationManager中执行authenticate()方法的原理。

类图关系如下,遍历ProviderManager中的providers list集合获取AuthenticationProvider接口集合中符合要求的provider:

在这里插入图片描述

  1. 通过ProviderManager能够找到执行AbstractUserDetailsAuthenticationProvider的authenticate()方法(可通过supports发现)

    image-20221207135340855

    AbstractUserDetailsAuthenticationProvider实现了UsernamePasswordAuthenticationToken的support配置,对应ProviderManager里的遍历查找

    image-20221207134532331

  2. AbstractUserDetailsAuthenticationProvider的authenticate抽象类中主要是retrieveUser接口方法->由DaoAuthenticationProvider继承AbstractUserDetailsAuthenticationProvider进行具体实现

    image-20221207141939779

    image-20221207142113322

    此处观察SecurityConfig,能够查看到实现了UserDetailsService接口的自定义实现类UserDetailsServiceImpl被设置,所以实际调用的则是自定义UserDetailsServiceImpl的loadUserByUsername()方法,就可以写结合实际业务逻辑的获取用户信息过程

    image-20221207142321619

  3. 执行loadUserByUsername()成功后返回认证对象

    image-20221207144257967

  4. 终于可以回到最外层SysLoginService

    image-20221207155412014

若依token生成逻辑

/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser)
{
    String token = IdUtils.fastUUID();
    loginUser.setToken(token);
    setUserAgent(loginUser);
    refreshToken(loginUser);

    Map<String, Object> claims = new HashMap<>();
    claims.put(Constants.LOGIN_USER_KEY, token);
    return createToken(claims);
}

创建令牌

String token = IdUtils.fastUUID();
loginUser.setToken(token);

使用性能更好的ThreadLocalRandom生成UUID,并设置loginUser对象中的token属性。

设置用户代理信息

setUserAgent(loginUser);
//setUserAgent...
loginUser.setIpaddr(ip);
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
loginUser.setBrowser(userAgent.getBrowser().getName());
loginUser.setOs(userAgent.getOperatingSystem().getName());

设置登录用户的ip,登录地点,登录客户端等信息

刷新令牌有效期

refreshToken(loginUser);
//refreshToken...
loginUser.setLoginTime(System.currentTimeMillis());
// 默认过期时间为半个小时
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存 格式为login_tokens:token作为缓存key
String userKey = getTokenKey(loginUser.getToken());
// 设置缓存 key是login_tokens:token value是loginUser对象
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);

设置登录时间,设置过期时间,设置缓存信息

生成JWT

Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return createToken(claims);
//createToken...
String token = Jwts.builder()
    // 关联上面生成的uuid
    .setClaims(claims)
    .signWith(SignatureAlgorithm.HS512, secret).compact();
return token;

将上面生成的uuid与该Jwt进行关联,并使用Jwt内置的方法生成一个token并返回给前端,至此token生成结束。

解析Jwt的过滤器原理

JwtAuthenticationTokenFilter:

image-20221208144840151

获取用户信息

优先从请求头里获取token,如果没有则从cookie中获取token;

// getLoginUser...
// 获取请求携带的令牌
String token = getToken(request);
if (token == null) {
    token = getCookieToken(request);
}
if (StringUtils.isNotEmpty(token))
{
    try
    {
        Claims claims = parseToken(token);
        // 解析对应的权限以及用户信息
        String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
        // 获取uuid
        String userKey = getTokenKey(uuid);
        // 从缓存中根据uuid的key获取对应的登录用户信息
        LoginUser user = redisCache.getCacheObject(userKey);
        return user;
    }
    catch (Exception e)
    {
    }
}
return null;

校验token过期时间

// verifyToken...
/**
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param loginUser
* @return 令牌
*/
public void verifyToken(LoginUser loginUser)
{
    long expireTime = loginUser.getExpireTime();
    long currentTime = System.currentTimeMillis();
    if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
    {
        // 刷新token过期时间
        refreshToken(loginUser);
    }
}

扩展:自定义实现用户鉴权

根据上述的源码分析,我们可以知道AbstractAuthenticationToken,AuthenticationProvider这两个是最重要的配套设施,前者是认证对象,后者是具体认证实现。所以我们按照默认实现来自定义独特的认证方法,比如微信登录,扫码登录等等。举例:

  1. 新建一个继承AbstractAuthenticationToken的Token认证对象

    public class XXXAuthenticationToken extends AbstractAuthenticationToken {
        /**
        * 用户实际信息对象
        */
        private final Object principal;
    
        /**
        * 用户凭证
        */
        private Object credentials;
    
        /**
        * 自定义所需的其他属性
        */
        private String userName;
        private String id;
        private String targetUrl;
        
    }
    
  2. 新建一个实现AuthenticationProvider接口的自定义Provider类,实现authenticate()和support()方法

    @Component
    public class XXXAuthenticationProvider implements AuthenticationProvider {
        
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            // 关键实现...例如使用扫码登录/微信登录等第三方认证        
            // 若未通过验证可以抛出异常
            throw new ServiceException("publicKey不正确或者id_token过期!");
            // 否则表示认证通过,返回一个Token对象
            return new XXXAuthenticationToken(xxxInfoData, xxxInfoData.getName());
        }
        
        @Override
        public boolean supports(Class<?> authentication) {
            // 将第一步新建的token进行标记,建立provider和token之间的联系(此处呼应ProviderManager如何实现关联关系)
            return XXXAuthenticationToken.class.isAssignableFrom(authentication);
        }
    }
    
  3. 注册自定义Provider到providerManage中

    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter
    {
        /**
         * 身份认证接口
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception
        {
            // 进行注册
            auth.authenticationProvider(xxxAuthenticationProvider);
            auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
        }
    }
    
  4. Service逻辑可以参照ruoyi实现

    /**
    * xxx登录
    * @param xxxInfoData
    * @return
    */
    public String loginForXXX(XxxInfoData xxxInfoData){
        // 用户验证
        Authentication authentication = null;
        try
        {
            Authentication xxxAuthenticationToken = new XXXAuthenticationToken(xxxInfoData, xxxInfoData.getIdToken());
            AuthenticationContextHolder.setContext(xxxAuthenticationToken);
            // 该方法会去调用 XXXAuthenticationProvider.authenticate
            authentication = authenticationManager.authenticate(xxxAuthenticationToken);
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(authentication != null ? (String) authentication.getCredentials() : XXX_DEFAULT_USERNAME, Constants.LOGIN_FAIL, MessageUtils.message("user.id_token.not.match")));
                throw new UserPasswordNotMatchException();
            }
            else
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(authentication != null ? (String) authentication.getCredentials() : XXX_DEFAULT_USERNAME, Constants.LOGIN_FAIL, e.getMessage()));
                throw new ServiceException(e.getMessage());
            }
        }
        finally
        {
            AuthenticationContextHolder.clearContext();
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(authentication != null ? (String) authentication.getCredentials() : XXX_DEFAULT_USERNAME, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        XXXInfoData ipdUserData = (XXXInfoData) authentication.getPrincipal();
        LoginUser loginUser = new LoginUser();
        loginUser.setUser(new SysUser());
        // 暂时无需对接我方用户
        // recordLoginInfo(loginUser.getUserId());
        // 生成token
        return tokenService.createToken(loginUser);
    }
    

总结

通过分析SpringSecurity框架,可以发现一个好的框架具有极佳的可扩展性,支持用较简单的方法进行自定义扩展。

参考资料:

  • 若依管理系统RuoYi-Vue
  • SpringSecurity常用过滤器介绍

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/74911.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

青春,不过几届世界杯系列1 —— 我经历的2002 ~ 2022年五届世界杯速览

1. 前言&#xff1a; 应 SoftwareTeacher, 邹欣老师在CSDN上的关于世界杯的邀约&#xff0c;在此回顾我目前经历的五届世界杯的点点滴滴。 2. 正文&#xff1a; 我是从2002年韩日世界杯&#xff0c;开始看世界杯的。现在算来&#xff0c;我已经经历了5届世界杯&#xff0c;而…

智慧水务平台建设方案全流程管控方案 智慧水务信息化系统的意义_管理_数据_设备

平升电子智慧水务平台建设方案全流程管控方案/智慧水务信息化系统/水务综合运营管理平台/智慧水务平台&#xff0c;综合水务公司对管网地理信息在线、供水调度SCADA、各环节数据互联互通、工单执行过程监督、运营情况分析等管理需求&#xff0c;建立了一套面向基层执行者、中层…

java计算机毕业设计ssm校园舆情监控系统tgv13(附源码、数据库)

java计算机毕业设计ssm校园舆情监控系统tgv13&#xff08;附源码、数据库&#xff09; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff0…

说反话

目录 1009:说反话 输入格式&#xff1a; 输出格式&#xff1a; 输入样例&#xff1a; 输出样例&#xff1a; 代码长度限制: 时间限制 内存限制 思路: 1.数组 1.2代码: 2.拷贝单词 2.2代码: 总代码: 时间复杂度: 总结: 题目链接: 1009:说反话 给定一句英语&#x…

RocketMq使用(5.0)

RocketMq使用&#xff08;5.0&#xff09; 基础概念 这一部分我们可以结合一下管理控制台&#xff0c;先来理解下RocketMQ的一些重要的基础概念&#xff1a; 官方文档-消息发送领域模型&#xff1a;https://rocketmq.apache.org/zh/docs/domainModel/01main 整个消息流程&am…

【强化学习论文合集】十四.2018神经信息处理系统大会论文(NIPS2018)

强化学习(Reinforcement Learning, RL),又称再励学习、评价学习或增强学习,是机器学习的范式和方法论之一,用于描述和解决智能体(agent)在与环境的交互过程中通过学习策略以达成回报最大化或实现特定目标的问题。 本专栏整理了近几年国际顶级会议中,涉及强化学习(Rein…

倍福:通过FB_EcCoeSdoWrite功能块写入第三方伺服的COE参数

本文介绍通过PLC写入第三方伺服的COE参数。例如力矩等信息 功能块介绍 首先需要用到的功能块为FB_EcCoeSdoWrite,该参数是写入COE online中的数据。 PLC程序编写 首先扫描程序配置,并链接好NC变量如下所示 然后再COEOnline中确定需要写入的变量的地址如下所示: 确定所在…

Hello erupt-cloud-node

前言 cloud-node我们可以用来封装某些场景下使用的功能集。不启动不用&#xff0c;启动了就用&#xff0c;方便得很。后台管理突然就变成了搭积木。 搭建项目 这个项目的基础设施我们都使用和主项目相同的。所以&#xff0c;我们这里就先搭建项目来看一下。先说目录结构&…

网络属性设置函数 setsockopt (设置绑定地址快速重用)

setsockopt 函数可以设置应用层、传输层、网络层的一些属性&#xff0c;比如 应用层&#xff1a;绑定地址快速重用、允许广播、传输层&#xff1a;设置TCP最大数据段大小网络层&#xff1a;设置IP首部选项、服务类型、生存时间目录 1、setsockopt 参数和返回值解析 2、使用s…

重点| 系统集成项目管理工程师考前50个知识点(3)

本文章总结了系统集成项目管理工程师考试背记50个知识点&#xff01;&#xff01;&#xff01; 帮助大家更好的复习&#xff0c;希望能对大家有所帮助 比较长&#xff0c;放了部分&#xff0c;需要可私信&#xff01;&#xff01; 19、项目管理计划的主要内容&#xff1a; &…

RNN--学习笔记

RNN引入“记忆”的概念&#xff0c;即输出需要依赖于之前的输入序列&#xff0c;并把关键输入记住。循环2字来源于其每个元素都执行相同的任务&#xff1b;并⾮刚性地记忆所有固定⻓度的序列&#xff0c;而是通过隐藏状态来存储之前时间步的信息。不同类型的RNN&#xff1a;One…

SpringBoot的自动配置原理

目录 一、关于SpringBoot的启动原理 二、源码分析 SpringBootConfiguration ComponentScan EnableAutoConfiguration Import(AutoConfigurationImportSelector.class) AutoConfigurationPackage 三、总结 一、引言 要理解SpringBoot自动配置原理&#xff0c;首先要从S…

java计算机毕业设计ssm校园二手平台交易系统11w7i(附源码、数据库)

java计算机毕业设计ssm校园二手平台交易系统11w7i&#xff08;附源码、数据库&#xff09; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#…

【计算机视觉】Deep SORT多目标跟踪算法讲解(图文解释 超详细)

觉得有帮助请点赞关注收藏~~~ 一、目标跟踪简介 目标跟踪算法可以进行轨迹特征的分析和提取&#xff0c;以弥补目标检测的不足&#xff1b;有效地去除误检&#xff0c;提高检测精度&#xff0c;为进一步的行为分析提供基础。例如&#xff0c;在自动驾驶系统中&#xff0c;目标跟…

4 - 线程池 Java内置的线程池 - ScheduledExecutorService

ScheduledExecutorService ScheduledExecutorService是ExecutorService的子接口&#xff0c;具备了延迟运行或定期执行任务的能力。 1、常用获取方式 &#xff08;注&#xff1a;还是通过 Executors. 的方式进行调用&#xff09; 1&#xff09;static ScheduledExecutorSer…

5702开发板用户手册

一、 底板简介 1.1 产品简介 在这里&#xff0c;对这款 MP5702 开发板底板进行简单的功能介绍。本公司相关核心板都可与其对接&#xff0c;型号如 MP5652&#xff08;请详见用户手册&#xff09;。底板核心板的模式来设计组成完整的开发。底板和核心板之间使用高速板间连接器连…

EMCC 中 运用SQL 优化的建议

1. 性能中心 关于SQL的执行信息 2. 优化SQL&#xff1a; ------整个过程SQL 实现---------------------- DECLARE my_task_name VARCHAR2(30); my_sqltext CLOB; BEGIN my_sqltext : SELECT * || FROM sales || WHERE prod_id 10 AND || cust_id …

Android穿戴设备新功能Compose for Wear OS 1.1

Todoist的增长率提高了50%&#xff0c;因为为Wear 3和Outdooractive重建了他们的应用程序&#xff0c;将开发时间缩短了30%&#xff0c;并显着提高了开发人员的生产力和更好的设计/开发人员协作&#xff1a; “Compose 使 UI 代码更易于编写和阅读&#xff0c;使我们能够在设计…

倍福:通过读取FB_EcCoESdoRead第三方伺服的COE参数,例如力矩等信息

本文介绍通过PLC读取第三方伺服的COE参数。例如力矩等信息 功能块介绍 首先需要用到的功能块为FB_EcCoESdoRead,该参数是读取COE online中的数据。PLC程序编写 首先扫描程序配置,并链接好NC变量如下所示然后再COEOnline中确定需要读取的变量的地址如下所示: 确定所在主站的…

shell 基本语法第一讲之(变量的使用、文件名替换和参数扩展、命令代换、算术代换、单引号、双引号)

1、变量的使用 #!/usr/sh #1、声明即赋值 注意:等号两边不能留空格&#xff0c;留了空格就编程一个命令两个参数 varNamename #2、使用变量 $varName ${varName}2、变量的分类 shell 内变量在shell的解析环境中存在的变量全局范围的变量(全局变量)shell中*不使用任何修饰符修…