深入理解Spring Security授权机制原理

news2024/11/27 9:58:16

原创/朱季谦

在Spring Security权限框架里,若要对后端http接口实现权限授权控制,有两种实现方式。

一、一种是基于注解方法级的鉴权,其中,注解方式又有@Secured和@PreAuthorize两种。

@Secured如:

  1 @PostMapping("/test")
  2  @Secured({WebResRole.ROLE_PEOPLE_W})
  3  public void test(){
  4  ......
  5  return null;
  6  }

@PreAuthorize如:

  1 @PostMapping("save")
  2 @PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")
  3 public RestResponse save(@RequestBody @Validated SysUser sysUser, BindingResult result) {
  4     ValiParamUtils.ValiParamReq(result);
  5     return sysUserService.save(sysUser);
  6 }

二、一种基于config配置类,需在对应config类配置@EnableGlobalMethodSecurity(prePostEnabled = true)注解才能生效,其权限控制方式如下:

  1 @Override
  2 protected void configure(HttpSecurity httpSecurity) throws Exception {
  3     //使用的是JWT,禁用csrf
  4     httpSecurity.cors().and().csrf().disable()
  5             //设置请求必须进行权限认证
  6             .authorizeRequests()
  7             //首页和登录页面
  8             .antMatchers("/").permitAll()
  9             .antMatchers("/login").permitAll()
 10             // 其他所有请求需要身份认证
 11             .anyRequest().authenticated();
 12     //退出登录处理
 13     httpSecurity.logout().logoutSuccessHandler(...);
 14     //token验证过滤器
 15     httpSecurity.addFilterBefore(...);
 16 }

这两种方式各有各的特点,在日常开发当中,普通程序员接触比较多的,则是注解方式的接口权限控制。

那么问题来了,我们配置这些注解或者类,其security框是如何帮做到能针对具体的后端API接口做权限控制的呢?

单从一行@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")注解上看,是看不出任何头绪来的,若要回答这个问题,还需深入到源码层面,方能对security授权机制有更好理解。

若要对这个过程做一个总的概述,笔者整体以自己的思考稍作了总结,可以简单几句话说明其整体实现,以该接口为例:

  1 @PostMapping("save")
  2 @PreAuthorize("hasAuthority('sys:user:add')")
  3 public RestResponse save(@RequestBody @Validated SysUser sysUser, BindingResult result) {
  4     ValiParamUtils.ValiParamReq(result);
  5     return sysUserService.save(sysUser);
  6 }

即,认证通过的用户,发起请求要访问“/save”接口,若该url请求在配置类里设置为必须进行权限认证的,就会被security框架使用filter拦截器对该请求进行拦截认证。拦截过程主要一个动作,是把该请求所拥有的权限集与@PreAuthorize设置的权限字符“sys:user:add”进行匹配,若能匹配上,说明该请求是拥有调用“/save”接口的权限,那么,就可以被允许执行该接口资源。

在springboot+security+jwt框架中,通过一系列内置或者自行定义的过滤器Filter来达到权限控制,如何设置自定义的过滤器Filter呢?例如,可以通过设置httpSecurity.addFilterBefore(new JwtFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)来自定义一个基于JWT拦截的过滤器JwtFilter,这里的addFilterBefore方法将在下一篇文详细分析,这里暂不展开,该方法大概意思就是,将自定义过滤器JwtFilter加入到Security框架里,成为其中的一个优先安全Filter,代码层面就是将自定义过滤器添加到List<Filter> filters。

设置增加自行定义的过滤器Filter伪代码如下:

  1 @Configuration
  2 @EnableWebSecurity
  3 @EnableGlobalMethodSecurity(prePostEnabled = true)
  4 public class SecurityConfig extends WebSecurityConfigurerAdapter {
  5     ......
  6     @Override
  7     protected void configure(HttpSecurity httpSecurity) throws Exception {
  8         //使用的是JWT,禁用csrf
  9         httpSecurity.cors().and().csrf().disable()
 10                 //设置请求必须进行权限认证
 11                 .authorizeRequests()
 12                 ......
 13                 //首页和登录页面
 14                 .antMatchers("/").permitAll()
 15                 .antMatchers("/login").permitAll()
 16                 // 其他所有请求需要身份认证
 17                 .anyRequest().authenticated();
 18         ......
 19         //token验证过滤器
 20         httpSecurity.addFilterBefore(new JwtFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
 21     }
 22 }

该过滤器类extrends继承BasicAuthenticationFilter,而BasicAuthenticationFilter是继承OncePerRequestFilter,该过滤器确保在一次请求只通过一次filter,而不需要重复执行。这样配置后,当请求过来时,会自动被JwtFilter类拦截,这时,将执行重写的doFilterInternal方法,在SecurityContextHolder.getContext().setAuthentication(authentication)认证通过后,会执行过滤器链FilterChain的方法chain.doFilter(request, response);

  1 public class JwtFilter  extends BasicAuthenticationFilter {
  2 
  3     @Autowired
  4     public JwtFilter(AuthenticationManager authenticationManager) {
  5         super(authenticationManager);
  6     }
  7 
  8    @Override
  9    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
 10        // 获取token, 并检查登录状态
 11        // 获取令牌并根据令牌获取登录认证信息
 12        Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
 13        // 设置登录认证信息到上下文
 14        SecurityContextHolder.getContext().setAuthentication(authentication);
 15 
 16        chain.doFilter(request, response);
 17    }
 18 
 19 }

那么,问题来了,过滤器链FilterChain究竟是什么?

这里,先点进去看下其类源码:

  1 package javax.servlet;
  2 
  3 import java.io.IOException;
  4 
  5 public interface FilterChain {
  6     void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
  7 }

FilterChain只有一个 doFilter方法,这个方法的作用就是将请求request转发到下一个过滤器filter进行过滤处理操作,执行过程如下:

image

 过滤器链就像一条铁链,中间的每个过滤器都包含对另一个过滤器的引用,从而把相关的过滤器链接起来,像一条链的样子。这时请求线程就如蚂蚁一样,会沿着这条链一直爬过去-----即,通过各过滤器调用另一个过滤器引用方法chain.doFilter(request, response),实现一层嵌套一层地将请求传递下去,当该请求传递到能被处理的的过滤器时,就会被处理,处理完成后转发返回。通过过滤器链,可实现在不同的过滤器当中对请求request做处理,且过滤器之间彼此互不干扰。

Spring Security框架上过滤器链上都有哪些过滤器呢?

可以在DefaultSecurityFilterChain类根据输出相关log或者debug来查看Security都有哪些过滤器,如在DefaultSecurityFilterChain类中的构造器中打断点,如图所示,可以看到,自定义的JwtFilter过滤器也包含其中:

image

这些过滤器都在同一条过滤器链上,即通过chain.doFilter(request, response)可将请求一层接一层转发,处理请求接口是否授权的主要过滤器是FilterSecurityInterceptor,其主要作用如下:

1. 获取到需访问接口的权限信息,即@Secured({WebResRole.ROLE_PEOPLE_W}) 或@PreAuthorize定义的权限信息;

2. 根据SecurityContextHolder中存储的authentication用户信息,来判断是否包含与需访问接口的权限信息,若包含,则说明拥有该接口权限;

3. 主要授权功能在父类AbstractSecurityInterceptor中实现;

  

我们将从FilterSecurityInterceptor这里开始重点分析Security授权机制原理的实现。

过滤器链将请求传递转发FilterSecurityInterceptor时,会执行FilterSecurityInterceptor的doFilter方法:

  1 public void doFilter(ServletRequest request, ServletResponse response,
  2       FilterChain chain) throws IOException, ServletException {
  3    FilterInvocation fi = new FilterInvocation(request, response, chain);
  4    invoke(fi);
  5 }

在这段代码当中,FilterInvocation类是一个有意思的存在,其实它的功能很简单,就是将上一个过滤器传递过滤的request,response,chain复制保存到FilterInvocation里,专门供FilterSecurityInterceptor过滤器使用。它的有意思之处在于,是将多个参数统一归纳到一个类当中,其到统一管理作用,你想,若是N多个参数,传进来都分散到类的各个地方,参数多了,代码多了,方法过于分散时,可能就很容易造成阅读过程中,弄糊涂这些个参数都是哪里来了。但若统一归纳到一个类里,就能很快定位其来源,方便代码阅读。网上有人提到该FilterInvocation类还起到解耦作用,即避免与其他过滤器使用同样的引用变量。

总而言之,这个地方的设定虽简单,但很值得我们学习一番,将其思想运用到实际开发当中,不外乎也是一种能简化代码的方法。

FilterInvocation主要源码如下:

  1 public class FilterInvocation {
  2 
  3    private FilterChain chain;
  4    private HttpServletRequest request;
  5    private HttpServletResponse response;
  6 
  7 
  8    public FilterInvocation(ServletRequest request, ServletResponse response,
  9          FilterChain chain) {
 10       if ((request == null) || (response == null) || (chain == null)) {
 11          throw new IllegalArgumentException("Cannot pass null values to constructor");
 12       }
 13 
 14       this.request = (HttpServletRequest) request;
 15       this.response = (HttpServletResponse) response;
 16       this.chain = chain;
 17    }
 18    ......
 19 }

FilterSecurityInterceptor的doFilter方法里调用invoke(fi)方法:

  1 public void invoke(FilterInvocation fi) throws IOException, ServletException {
  2    if ((fi.getRequest() != null)
  3          && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
  4          && observeOncePerRequest) {
  5      //筛选器已应用于此请求,每个请求处理一次,所以不需重新进行安全检查 
  6       fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
  7    }
  8    else {
  9       // 第一次调用此请求时,需执行安全检查
 10       if (fi.getRequest() != null && observeOncePerRequest) {
 11          fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
 12       }
 13        //1.授权具体实现入口
 14       InterceptorStatusToken token = super.beforeInvocation(fi);
 15       try {
 16        //2.授权通过后执行的业务
 17          fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
 18       }
 19       finally {
 20          super.finallyInvocation(token);
 21       }
 22        //3.后续处理
 23       super.afterInvocation(token, null);
 24    }
 25 }

授权机制实现的入口是super.beforeInvocation(fi),其具体实现在父类AbstractSecurityInterceptor中实现,beforeInvocation(Object object)的实现主要包括以下步骤:

一、获取需访问的接口权限,这里debug的例子是调用了前文提到的“/save”接口,其权限设置是@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')"),根据下面截图,可知变量attributes获取了到该请求接口的权限:

image

二、获取认证通过之后保存在 SecurityContextHolder的用户信息,其中,authorities是一个保存用户所拥有全部权限的集合;

image

这里authenticateIfRequired()方法核心实现:

  1 private Authentication authenticateIfRequired() {
  2    Authentication authentication = SecurityContextHolder.getContext()
  3          .getAuthentication();
  4    if (authentication.isAuthenticated() && !alwaysReauthenticate) {
  5      ......
  6       return authentication;
  7    }
  8    authentication = authenticationManager.authenticate(authentication);
  9    SecurityContextHolder.getContext().setAuthentication(authentication);
 10    return authentication;
 11 }

在认证过程通过后,执行SecurityContextHolder.getContext().setAuthentication(authentication)将用户信息保存在Security框架当中,之后可通过SecurityContextHolder.getContext().getAuthentication()获取到保存的用户信息;

三、尝试授权,用户信息authenticated、请求携带对象信息object、所访问接口的权限信息attributes,传入到decide方法;

image

decide()是决策管理器AccessDecisionManager定义的一个方法。

  1 public interface AccessDecisionManager {
  2    void decide(Authentication authentication, Object object,
  3          Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
  4          InsufficientAuthenticationException;
  5    boolean supports(ConfigAttribute attribute);
  6    boolean supports(Class<?> clazz);
  7 }

AccessDecisionManager是一个interface接口,这是授权体系的核心。FilterSecurityInterceptor 在鉴权时,就是通过调用AccessDecisionManager的decide()方法来进行授权决策,若能通过,则可访问对应的接口。

AccessDecisionManager类的方法具体实现都在子类当中,包含AffirmativeBased、ConsensusBased、UnanimousBased三个子类;

image

AffirmativeBased表示一票通过,这是AccessDecisionManager默认类;

ConsensusBased表示少数服从多数;

UnanimousBased表示一票反对;

如何理解这个投票机制呢?

点进去AffirmativeBased类里,可以看到里面有一行代码int result = voter.vote(authentication, object, configAttributes):

image

这里的AccessDecisionVoter是一个投票器,用到委托设计模式,即AffirmativeBased类会委托投票器进行选举,然后将选举结果返回赋值给result,然后判断result结果值,若为1,等于ACCESS_GRANTED值时,则表示可一票通过,也就是,允许访问该接口的权限。

这里,ACCESS_GRANTED表示同意、ACCESS_DENIED表示拒绝、ACCESS_ABSTAIN表示弃权:

  1 public interface AccessDecisionVoter<S> {
  2    int ACCESS_GRANTED = 1;//表示同意
  3    int ACCESS_ABSTAIN = 0;//表示弃权
  4    int ACCESS_DENIED = -1;//表示拒绝
  5    ......
  6    }

那么,什么情况下,投票结果result为1呢?

这里需要研究一下投票器接口AccessDecisionVoter,该接口的实现如下图所示:

image

这里简单介绍两个常用的:

1. RoleVoter:这是用来判断url请求是否具备接口需要的角色,这种主要用于使用注解@Secured处理的权限;
2. PreInvocationAuthorizationAdviceVoter:针对类似注解@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")处理的权限;

image

到这一步,代码就开始难懂了,这部分封装地过于复杂,总体的逻辑,是将用户信息所具有的权限与该接口的权限表达式做匹配,若能匹配成功,返回true,在三目运算符中,

allowed ? ACCESS_GRANTED : ACCESS_DENIED,就会返回ACCESS_GRANTED ,即表示通过,这样,返回给result的值就为1了。

image

image

到此为止,本文就结束了,笔者仍存在不足之处,欢迎各位读者能够给予珍贵的反馈,也算是对笔者写作的一种鼓励。

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

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

相关文章

推荐几款值得收藏的3DMAX插件

推荐几款值得收藏的3DMAX插件 StairGenerator StairGenerator一键楼梯插件&#xff0c;不需要花费太多的时间&#xff0c;轻松从2D平面图生成3D楼梯模型&#xff0c;生成的楼梯模型细节丰富真实。 【主要功能】 1.简单&#xff1a;轻松实现2D到3D建模。 2.具有最详细三维结…

六:爬虫-数据解析之BeautifulSoup4

六&#xff1a;bs4简介 基本概念&#xff1a; 简单来说&#xff0c;Beautiful Soup是python的一个库&#xff0c;最主要的功能是从网页抓取数据官方解释如下&#xff1a; Beautiful Soup提供一些简单的、python式的函数用来处理导航、搜索、修改分析树等功能。 它是一个工具箱…

中断响应,解决死锁的例子

对于关键字synchronized来说&#xff0c;如果一个线程在等待锁&#xff0c;那么结果只有两种情况&#xff0c;要么它获得这把锁继续执行&#xff0c;要么它就保持等待。而使用重入锁&#xff0c;则提供另外一种可能&#xff0c;那就是线程可以被中断。也就是在等待锁的过程中&a…

【Java 集合】ArrayBlockingQueue

ArrayBlockingQueue, 顾名思义: 基于数组的阻塞队列, 位于 JUC (java.util.concurrent) 下, 是一个线程安全的集合, 其本身具备了 不支持 null 元素: 存入 null 元素会抛出异常固定容量: 在初始化时需要指定一个固定的容量大小。这意味着一旦队列达到最大容量&#xff0c;将不再…

去除Antd组件库中上传文件中的一些默认样式

最近写的项目用到了antd组件库中的Upload了 <><Upload {...UploadProps} classNamechooseFile><Button style{{ marginRight: 8px }}>选择文件</Button></Upload><Button type"primary" onClick{upload}>开始上传</Button&g…

YOLOv8代码下载及环境配置

Github 下载YOLOv8官方代码进入Miniconda pytorch虚拟环境&#xff0c;或者用Pycharm打开文件进入虚拟环境&#xff0c;切换到YOLOv8代码目录&#xff0c;输入pip install -r requirements.txt 安装YOLOv8环境。 如果报错&#xff1a;WARNING: Ignore distutils configs in set…

高级桌面编程(一)

前言 学习心得&#xff1a;C# 入门经典第8版书中的第15章《高级桌面编程》 创建控件并设置样式 1 样式 Style WPF 当中我们可以对每一个控件进行完全的自定义。我们可以随意更改控件外观和功能。提供我们能完成这样的效果与控件的样式&#xff08;Style&#xff09;有着不可分…

OpenCV技术应用(8)— 如何将视频分解

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。本节课就手把手教大家如何将一幅图像转化成热力图&#xff0c;希望大家学习之后能够有所收获~&#xff01;&#x1f308; 目录 &#x1f680;1.技术介绍 &#x1f680;2.实现代码 &#x1f680;1.技术介绍 视频是…

蚂蚁SEO强引蜘蛛是什么

强引蜘蛛在网页中是指一些特殊类型的网页&#xff0c;这些网页具有极高的吸引力和价值&#xff0c;能够吸引搜索引擎蜘蛛&#xff08;Spider&#xff09;的强烈关注和抓取。强引蜘蛛的网页通常具有以下特点&#xff1a; 如何联系蚂蚁seo&#xff1f; baidu搜索&#xff1a;如…

C++ Qt开发:ComboBox下拉组合框组件

Qt 是一个跨平台C图形界面开发库&#xff0c;利用Qt可以快速开发跨平台窗体应用程序&#xff0c;在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置&#xff0c;实现图形化开发极大的方便了开发效率&#xff0c;本章将重点介绍ComboBox下拉组合框组件的常用方法及灵活运用…

概率论复习

第一章&#xff1a;随机概率及其概率 A和B相容就是 AB 空集 全概率公式与贝叶斯公式&#xff1a; 伯努利求概率&#xff1a; 第二章&#xff1a;一维随机变量及其分布&#xff1a; 离散型随机变量求分布律&#xff1a; 利用常规离散性分布求概率&#xff1a; 连续性随机变量…

Unity 如何通过2D Sprite切割一张图为多张

1、理解 在一些2D游戏开发中&#xff0c;我们常常使用2D Sprite把一张大图切割成多个小图使用。 这样做有不少好处&#xff0c;首先&#xff0c;通过精准使用小图&#xff0c;能够一定程度上节省内存&#xff0c;提高渲染性能。 其次把同类的小图做成一张大图在切割使用会更…

强大的数学软件 GeoGebra 多平台适用

GeoGebra 是一款教育数学软件&#xff0c;可以帮助学生和教师探索、学习和教授各种数学概念和科学领域的知识。GeoGebra 以其灵活性和强大的功能而闻名&#xff0c;它融合了几何、代数、微积分、概率、统计和其他数学领域的工具&#xff0c;以及绘图和计算功能。 功能 GeoGeb…

深度学习项目实战:垃圾分类系统

简介&#xff1a; 今天开启深度学习另一板块。就是计算机视觉方向&#xff0c;这里主要讨论图像分类任务–垃圾分类系统。其实这个项目早在19年的时候&#xff0c;我就写好了一个版本了。之前使用的是python搭建深度学习网络&#xff0c;然后前后端交互的采用的是java spring …

【开源】基于Vue和SpringBoot的天然气工程业务管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、使用角色3.1 施工人员3.2 管理员 四、数据库设计4.1 用户表4.2 分公司表4.3 角色表4.4 数据字典表4.5 工程项目表4.6 使用材料表4.7 使用材料领用表4.8 整体E-R图 五、系统展示六、核心代码6.1 查询工程项目6.2 工程物资…

详解—【C++】lambda表达式

目录 前言 一、lambda表达式 二、lambda表达式语法 2.1. lambda表达式各部分说明 2.2. 捕获列表说明 三、函数对象与lambda表达式 前言 在C98中&#xff0c;如果想要对一个数据集合中的元素进行排序&#xff0c;可以使用std::sort方法。 #include <algorithm> #i…

mysql函数(三)之常见数学函数

MySQL提供了很多常用的数学函数&#xff0c;常见的数学函数及使用如下&#xff1a; ABS(X)&#xff1a;返回X的绝对值。 例子&#xff1a;SELECT ABS(-5); – 结果为5 效果图&#xff1a; CEIL(X)&#xff1a;返回大于或等于X的最小整数值。 例子&#xff1a;SELECT CEIL(3.2…

13603个字彻底弄清linux

13603个字彻底弄清linux 关键命令说明 系统关机命令linux查看文本的指令mountdmesggrepfindlsusblsof linux软件开发知识点 linux进程间通讯方式内存申请函数gcc编译过程文件系统硬链接和软连接linux内核子系统进程几种状态文件系统组成linux文件类型linux常用的系统调用函数f…

MidJourney笔记(8)-ask和blend命令

经过前面的课程介绍,我相信大家对MidJourney有一定的认识,接下来就给大家介绍一下MidJourney的常用命令。 /ask 获取问题答案。 我一开始以为是随便问题都可以问,最后发现只能回答MidJourney相关的问题。 我们先试试一些日常生活问题: 今天天气如何? 以为它不会识别中文,…

基于Levenberg-Marquardt算法改进的BP神经网络-公式推导及应用

Levenberg-Marquardt算法是一种用于非线性最小化问题的优化算法&#xff0c;通常用于训练神经网络。它结合了梯度下降和高斯-牛顿方法的特点&#xff0c;旨在提高收敛速度和稳定性。下面是基于Levenberg-Marquardt算法改进的反向传播&#xff08;BP&#xff09;神经网络的详细推…