深入理解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…

多域名SSL证书选择什么品牌比较好?

选择适合的品牌是申请多域名SSL证书的重要一步。不同品牌的SSL证书提供商在性能、安全性和客户支持等方面各有特点。本文将详细介绍选择多域名SSL证书品牌时应考虑的因素&#xff0c;并推荐几个知名的SSL证书品牌。 首先&#xff0c;我们需要考虑的是品牌的声誉和可靠性。选择一…

短地址漏洞

漏洞条件&#xff1a; 当地址的长度不足 20 byte (40个十六进制字符) 时&#xff0c;以太坊虚拟机 EVM 会进行高位补0x0转发以太币时没有对地址长度进行验证 1. input 数据 交易的 input 数据分为三个部分&#xff1a; 1&#xff09;4字节&#xff0c;是方法名的Hash值&…

MySQL_11.InnoDB Buffer Pool原理与配置

1.buffer pool原理 (1)innodb_buffer_pool_instances&#xff1a; windows default 1个实例、windows default 8个实例; 将热点打散,提高并发的性能改善并发,通过降低竞争因为不同的线程读和写cached pages&#xff08;缓存页&#xff09; 内存小于1G默认1个,这个选项只有当设置…

Android开发中压缩文件和文件夹

Android开发中&#xff0c;我们经常会遇到上传文件&#xff0c;用到压缩文件、文件夹的功能&#xff0c;今天我们主要讲解这个功能&#xff0c;具体的实现如下&#xff1a; /*** 压缩文件和文件夹** param srcFileString 要压缩的文件或文件夹* param zipFileString 压缩完成的…

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

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

【算法技术专题】精彩解密KMP算法之跃进式搜索的深度探索

KMP算法 KMP算法介绍KMP算法历史KMP算法思路性能损耗算法思路结构模型准备KMP算法的实现步骤生成next数组构建next数组原理生成nexf数组代码实现 代码案例解释说明 字符串对比操作代码案例解释说明 算法效果 KMP算法介绍 KMP算法&#xff08;Knuth-Morris-Pratt算法&#xff0…

02markdown-学习笔记

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

springboot 拦截器之Advisor不生效问题

SPringBoot使用方法注解拦截器时遇到不生效问题记录&#xff1a; 定义方法注解 Target({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) Inherited Documented public interface DataScope {boolean enable() default true;}定义 Advisor Getter…

出现 java: 找不到符号 符号: 变量 log 的解决方法

目录 1. 问题所示2. 原理分析3. 解决方法3.1 增加编译参数3.2 增加lombok插件3.3 清楚本地缓存1. 问题所示 使用Springboot启动项目的时候,出现如下bug: java: 找不到符号符号: 变量 log位置: 类 org.springblade.example.consumer.rpc.BlogStu

如何定位线上OOM?

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

Docker常用命令总结

文章目录 Docker命令总结 Docker命令总结 简介&#xff1a;Docker是一个基于轻量级虚拟化技术的容器&#xff0c;整个项目基于Go语言开发&#xff0c;并采用了Apache 2.0协议。Docker可以将我们的应用程序打包封装到一个容器中&#xff0c;该容器包含了应用程序的代码、运行环…

【带头学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;双工…

旅游规划

有了一张自驾旅游路线图&#xff0c;你会知道城市间的高速公路长度、以及该公路要收取的过路费。现在需要你写一个程序&#xff0c;帮助前来咨询的游客找一条出发地和目的地之间的最短路径。如果有若干条路径都是最短的&#xff0c;那么需要输出最便宜的一条路径。 输入格式: …

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

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

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

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

Android Studio报Gradle问题解决思路

Gradle的版本号与Android studio版本、AGP&#xff08;Android Gradle Plugin&#xff09;版本、jdk版本都有关系。只有同时匹配三个版本号&#xff0c;才能使用对应的Gradle版本。 1、AGP的版本号与Android studio版本的关系。 Google 搜索 Android studio gradle plugin re…

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

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