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

原创/朱季谦

在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 @Override2 protected void configure(HttpSecurity httpSecurity) throws Exception {3     //使用的是JWT,禁用csrf4     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 @Configuration2 @EnableWebSecurity3 @EnableGlobalMethodSecurity(prePostEnabled = true)4 public class SecurityConfig extends WebSecurityConfigurerAdapter {5     ......6     @Override7     protected void configure(HttpSecurity httpSecurity) throws Exception {8         //使用的是JWT,禁用csrf9         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     @Autowired4     public JwtFilter(AuthenticationManager authenticationManager) {5         super(authenticationManager);6     }7 8    @Override9    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.mzph.cn/news/221171.shtml

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

相关文章

elementui select中添加新增标签

<el-select v-model"ruleForm.eventType" :placeholder"请选择事件类型&#xff0c;可手动添加" ref"template" clearable visible-change"(v) > visibleChange(v, template)"><el-option v-for"item in eventTypeOp…

【论文极速读】视频检索中的模态均衡方法

【论文极速读】视频检索中的模态均衡方法 FesianXu 20231206 at Baidu Search Team 前言 传统的视频搜索系统相关性部分主要以文本匹配为基础手段&#xff0c;在其中引入多模态向量容易收到『模态不均衡』的问题&#xff0c;论文[1]尝试对其进行解决&#xff0c;本文进行笔记。…

02markdown-学习笔记

一级标题 二级标题 三级标题 四级标题 五级标题 六级标题 换行符<br>标签 写入一段测试用的正文第二段测试文本,如果要对文本进行换行可以使用<br>标签 文本修饰符 字体为斜体的修饰&#xff0c;一对星号包含 字符为粗体&#xff0c;两对星号包含 字体为…

如何定位线上OOM?

文章目录 造成OOM的原因1.一次性申请的太多2. 内存资源耗尽未释放3.本身资源不够 如何快速定位OOM&#xff1f;1.系统已经OOM了2.系统运行中还未OOM2.1导出dump文件&#xff1a;2.2.结合jvisualvm进行调试2.3 利用ArthasArthas可以做什么&#xff1f;如何使用Arthas小结 造成OO…

【带头学C++】----- 九、类和对象 ---- 9.13 运算符重载——(9.13.7-9.13.8)

❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️创做不易&#xff0c;麻烦点个关注❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️ ❤️❤️❤️❤️❤️❤️❤️❤️❤️文末有惊喜&#xff01;献舞一支&#xff01;❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️ 目录 9.13…

Git篇---第九篇

系列文章目录 文章目录 系列文章目录前言一、使用过git merge和git rebase吗?它们之间有什么区别?二、使用过git cherry-pick,有什么作用?前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看…

GPIO模拟MDIO

背景 CPU&#xff1a;AST2500 驱动里实现GPIO模拟MDIO驱动,参考内核驱动mdio-bitbang.c和mdio-gpio.c&#xff0c;当前项目不支持设备树&#xff0c;驱动需要改成platform注册 MDIO介绍 SMI接口 SMI是MAC内核访问PHY寄存器接口&#xff0c;它由两根线组成&#xff0c;双工…

c++ qt 窗口开发中 俩按钮组合 配合 显影 已解决

在日常项目中&#xff0c;有这么需求&#xff0c;还想窗口移动&#xff0c;还想 右侧关闭 还能tab栏点击显影的需求&#xff0c;不得使用 qt模拟点击事件 进行功能优化 特大杯 大杯 控制 窗口显影&#xff0c; 咖啡 按钮 显示窗口 可乐 豆浆 不显示窗口 四个按钮的 互斥关…

Amazon SageMaker机器学习之旅的助推器

授权声明&#xff1a;本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 亚马逊云科技开发者社区, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道。 一、前言 在当今的数字化时代&#xff0c;人工智能和机器学习已经…

2023一起益企广东省中小企业数字化赋能活动(深圳站)成功举办

12月12日&#xff0c;由广东工业和信息化厅指导&#xff0c;广东省中小企业服务中心、深圳市中小企业服务局主办&#xff0c;深圳联通承办的2023年“一起益企”广东省中小企业数字化赋能专项对接志愿服务活动&#xff08;深圳站&#xff09;在深圳成功举办。 本次活动涵盖中小企…

【AI底层逻辑】——“数学华尔兹”之一元线性回归

一元线性回归模型想必大家都耳熟能详&#xff0c;这里不再赘述。但在使用python中机器学习包时一定见过类似模型评价参数的输出&#xff0c;这一章我们就讲一讲回归分析里一些模型评价概念&#xff01; 一、方差分析ANOVA 方差分析是一种用于确定线性回归模型中不同变量对目标…

【改进YOLOv8】车辆测距预警系统:融合空间和通道重建卷积SCConv改进YOLOv8

1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 研究背景与意义&#xff1a; 随着交通工具的普及和道路交通的不断增加&#xff0c;车辆安全问题日益凸显。特别是在高速公路等高速道路上&#xff0c;车辆之间的距离和速度差异较…

【论文】 虚拟机 和 Linux容器 的 最新性能比较

虚拟机 和 Linux容器 的 最新性能比较 An Updated Performance Comparison of Virtual Machines and Linux Containers 借助DeepL辅助翻译 校准 摘要 云计算广泛使用虚拟机&#xff08;VM&#xff09;&#xff0c;因为它们允许工作负载相互隔离&#xff0c;并在一定程度上控…

springboot框架的客制化键盘个性化商城网站

客制化键盘网站是从客制化键盘的各部分统计和分析&#xff0c;在过程中会产生大量的、各种各样的数据。本文以客制化键盘管理为目标&#xff0c;采用B/S模式&#xff0c;以Java为开发语言&#xff0c;Jsp为开发技术、idea Eclipse为开发工具&#xff0c;MySQL为数据管理平台&am…

西南科技大学数字电子技术实验七(4行串行累加器设计及FPGA实现)FPGA部分

一、实验目的 1、掌握基于Verilog语言的diamond工具设计全流程。 2、熟悉、应用Verilog HDL描述数字电路。 3、掌握Verilog HDL的组合和时序逻辑电路的设计方法。 4、掌握“小脚丫”开发板的使用方法。 二、实验原理 三、程序清单&#xff08;每条语句必须包括注释或在开发…

Leetcode 491 递增子序列

题意理解&#xff1a; 输入&#xff1a;nums [4,6,7,7] 输出&#xff1a;[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]] 这里不止要找一个子序列&#xff0c;还要元素保证其在原来的集合中的前后顺序&#xff0c;且应为增序。 为保证一个增序序列&#xff0c;…

刘家窑中医院王忠主任:心脑血管健康知识的传播者和实践者

为普及心脑血管健康知识&#xff0c;呼吁市民注重心脑血管健康&#xff0c;王忠主任及其科室医护人员举办进社区义诊咨询的活动。王忠主任及多名中医心脑专家和护理人员为社区居民免费进行量血压、测血糖以及健康咨询等义诊活动。 王忠主任用了全身的心血&#xff0c;学习百家吸…

LTC是什么意思?CRM怎样帮助这一流程的实现?

在现代商业环境下&#xff0c;将潜在客户转化成实际销售是公司成功的基石之一。而CRM管理系统是完成LTC的有效工具。本文将向您介绍LTC是什么&#xff1f;公司怎样企业如何通过CRM实现这一流程的&#xff1f; LTC&#xff08;从线索到现金&#xff09;是企业运营管理中的一个重…

Python 使用 openpyxl 写表格

当前环境&#xff1a;Win10 x64 MS office 2016 Python3.7 openpyxl3.0.9 1 写入表格 from openpyxl import Workbook# 创建一个 workbook workbook_w Workbook()# 获取被激活的 worksheet worksheet_w workbook_w.active# 1 批量插入数据# 设置一行数据 worksheet_w.ap…

Mo 人工智能教学实训平台年终发布会——发现意外 创造可能

发布会视频回放 –发现意外 创造可能– 在技术迅猛发展的时代里&#xff0c;人工智能教育成为推动社会进步的关键力量&#xff0c;大模型更是各行业的必备技能。为了深度探索教育与人工智能的融合&#xff0c;Mo 人工智能教学实训平台于12月12日举行线上年终发布会&#xff0…