【深入浅出 Spring Security(十一)】授权原理分析和持久化URL权限管理

news2024/11/13 16:47:45

授权原理分析和持久化URL权限管理

  • 一、必须知道的三大组件(Overview)
  • 二、FilterSecurityInterceptor 源码分析
    • SecurityMetadataSource 分析
  • 三、自定义 FilterSecurityMetadataSource 对象(实战)
    • 自定义表
    • CustomSecurityMetadataSource
    • 配置自定义的 SecurityMetadataSource
    • 测试代码
    • 测试效果
  • 四、总结

一、必须知道的三大组件(Overview)

在 【深入浅出Spring Security(一)】Spring Security的整体架构 中小编解释过授权所用的三大组件,在此再解释说明一下(三大组件具体指:ConfigAttribute、AccessDecisionManager(决策管理器)、AccessDecisionVoter(决策投票器))

  • ConfigAttribute 在 Spring Security 中,用户请求一个资源(通常是一个接口或者是一个 java 方法)需要的角色会被封装成一个 ConfigAttribute 对象,在ConfigAttribute 中只有一个 getAttribute 方法,该方法返回一个 String 字符串,就是角色的名称。一般来说,角色名称都带有一个 ROLE_ 前缀,投票器 AccessDecisionVoter 所做的事情,其实就是比较用户所具备的角色和请求某个资源所需的 ConfigAttribute 之间的关系。
  • AccessDecisionVoterAccessDecisionManager 都有众多的实现类,在 AccessDecisionManager 中会挨个遍历 AccessDecisionVoter,进而决定是否允许用户访问,因而 AccessDecisionVoter 和 AccessDecisionManager 两者的关系类似于 AuthenticationManager(ProviderManager) 和 AuthenticationProvider 的关系。

二、FilterSecurityInterceptor 源码分析

小编在述说 【深入浅出Spring Security(二)】Spring Security的实现原理 的时候,总结出了 Spring Security 中所用的过滤器,其中有个 FilterSecurityInterceptor ,它是其默认加载的一个过滤器(FilterSecurityInterceptor 虽继承了 AbstractSecurityInterceptor,但同时也实现了 Filter,实现了其 doFilter 方法核心方法,并在 SecurityFilterChain 中,所以一般称过滤器比较合适)。

(下图展示了拦截请求的一流程,可以看完图下的源码分析后再回过头看这张图,会变得清晰很多。)

请添加图片描述

doFilter 方法具体实现

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		// 将request、response、chain三个对象封装到一FilterInvocation中 
		// 然后再调用 invoke 方法
		invoke(new FilterInvocation(request, response, chain));
	}

invoke 方法具体实现

	public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
		// 判断用户在此次请求中是否已经被允许通过
		if (isApplied(filterInvocation) && this.observeOncePerRequest) {
			filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
			return;
		}
		// 调用父类AbstractSecurityInterceptor 中的 beforeInvocation 方法
		// 传入的参数是 filterInvocation 对象,是request、response、chain的封装对象
		// 这个方法内部就是实现路径权限认证操作
		InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
		try {
			filterInvocation
			.getChain()
			.doFilter(filterInvocation.getRequest(),
			 filterInvocation.getResponse());
		}
		finally {
			super.finallyInvocation(token);
		}
		super.afterInvocation(token, null);
	}

beforeInvocation 方法核心代码分析

	protected InterceptorStatusToken beforeInvocation(Object object) {
		// obtainSecurityMetadataSource() 方法获取子类的 FilterInvocationSecurityMetadataSource 的对象
		// 然后通过获取到的FilterInvocationSecurityMetadataSource对象调用getAttributes方法获取 ConfigAttribute 集合
		// 集用户要访问该资源需要的权限集(后面会对其进行源码分析,这里先知道是获取访问该资源需要的权限集就行,上面(一)也做了解释)
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
		if (CollectionUtils.isEmpty(attributes)) {
			return null; // no further work post-invocation
		}
		// 判断是否已经认证过了,没认证重新认证(重新认证的Authentication对象是从SecurityContextHolder中获取的)
		// 返回用户认证信息
		Authentication authenticated = authenticateIfRequired();
		// Attempt authorization
		// 尝试对路径进行放行,即判断请求路径是否拥有访问权限
		attemptAuthorization(object, attributes, authenticated);
		// no further work post-invocation
		return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);

	}

attemptAuthorization 方法源码分析

	// 参数说明:
	// 1. object是filterInvocation对象,即request、response、chain的封装体
	// 2. attributes 是访问路径需要的权限集
	// 3. authentication 用户认证的权限信息
	private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
			Authentication authenticated) {
		try {
			// 通过 AccessDecisionManager 决策管理器对象进行决策是否放行
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException ex) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
			throw ex;
		}
	}

这是 AccessDecisionManager 的结构图,SpringSecurity 默认实现是 AffirmativeBased(我看最新版本即6.1.0,这些全弃用了,估计是让咱自身实现吧🤔)。

在这里插入图片描述

AffirmativeBased 中的 decide 方法源码分析

	@Override
	public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
			throws AccessDeniedException {
		// 否认的一个标志变量,用来判断是否授权成功
		int deny = 0;
		// 遍历 AccessDecisionVoter,看看是否能投票通过
		for (AccessDecisionVoter voter : getDecisionVoters()) {
			int result = voter.vote(authentication, object, configAttributes);
			switch (result) {
			// result 为 1 的话,就说有该 AccessDecisionVoter 已经投票通过了
			case AccessDecisionVoter.ACCESS_GRANTED:
				return;
			// result 为 0 的话,说明被否认了,否认的voter数加1	
			case AccessDecisionVoter.ACCESS_DENIED:
				deny++;
				break;
			// 如果是 -1 或者说其他的话,就表示弃权,不产于投票	
			default:
				break;
			}
		}
		// 到这的话如果deny大于0的话说明被否认了
		// 授权没通过,抛出异常
		if (deny > 0) {
			throw new AccessDeniedException(
					this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
		}
		// To get this far, every AccessDecisionVoter abstained
		checkAllowIfAllAbstainDecisions();
	}

对授权源码分析后的总结:

  1. 先是被 FilterSecurityInterceptor 拦截,调用 doFilter 方法,doFilter 中调用了其自身实现的invoke 方法。
  2. 在 invoke 方法中,调用了父类 AbstractSecurityInterceptor 的 beforeInvocation 方法进行;
  3. 在 beforeInvocation 方法中,调用了 attemptAuthorization 方法进行授权操作;
  4. 在 attemptAuthorization 方法中,使用 AccessDecisionManager 决策对象调用 decide 方法进行决策,即授权。
  5. 在 decide 方法中,即是遍历 AccessDecisionVoter 对象调用 vote 方法进行投票,判断是否授权成功(这里与 AuthenticationManager 中的 authenticate 方法遍历 AuthenticationProvider 对象进行认证类似)。与上图对应。

SecurityMetadataSource 分析

在 FilterSecurityInterceptor 中有个 FilterInvocationSecurityMetadataSource 类型的属性对象,它可以通过 setter 方式进行注入的。FilterInvocationSecurityMetadataSource 中没有方法,继承了 SecurityMetadataSource,Spring Security 对其的默认实现是 DefaultFilterInvocationSecurityMetadataSource。

下面是默认实现的结构图

在这里插入图片描述
DefaultFilterInvocationSecurityMetadataSource 中的 getAttributes 方法源码分析如下:

	private final Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;
	// 构造注入 路径 <=> 权限集 映射
	// requestMap 为 LinkedHashMap 的对象
	public DefaultFilterInvocationSecurityMetadataSource(
			LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap) {
		this.requestMap = requestMap;
	}
	
	@Override
	public Collection<ConfigAttribute> getAttributes(Object object) {
		// 注意 object instanceOf FilterInvocation 为 true
		final HttpServletRequest request = ((FilterInvocation) object).getRequest();
		// 遍历 requestMap,匹配请求路径一致的权限集
		for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : this.requestMap.entrySet()) {
			if (entry.getKey().matches(request)) {
				return entry.getValue();
			}
		}
		return null;
	}

三、自定义 FilterSecurityMetadataSource 对象(实战)

配置 SecurityFilterChain 时,使用代码的方式配置 URL 拦截规则 和 请求 URL 所需要的权限,这种方式比较死板,如果想要调整访问某一个 URL 所需要的权限,就需要修改代码。

动态管理权限规则就是我们将 URL 拦截规则和访问 URI 所需要的权限都保存在数据库中,这样,在不修改源代码的情况下,只需要修改数据库中的数据,就可以对权限进行调整。即不用去修改代码,达到了解耦的效果。

当 URL 和角色进行匹配的时候,Spring Security 是默认采用“或者”(OR)的方式来匹配用户所拥有的角色。比如:/hello 需要 ROLE_ADMIN、ROLE_USER 角色即可访问,即当用户拥有俩个角色的其中一个就可以访问。

自定义表

用户表(user)- 非 RememberMe 持久化那个 persistent_logins 表
在这里插入图片描述角色表(role)
在这里插入图片描述用户角色关联表 user -> role(user_role)
在这里插入图片描述菜单表(menu)请求路径
在这里插入图片描述菜单可被什么角色访问,即菜单关联角色表(menu_role)
在这里插入图片描述

CustomSecurityMetadataSource

CustomSecurityMetadataSource 是小编自定义的 FilterSecurityMetadataSource 的实现类,根据上面的源码分析,如获取路径的匹配权限需要自己设定的话,需要自定义SecurityMetadataSource中的getAttribute方法。

CustomSecurityMetadataSource

@Component
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {


    private final MenuService menuService;

    public CustomSecurityMetadataSource(@Autowired MenuService menuService){
        this.menuService = menuService;
    }

    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object)
            throws IllegalArgumentException {
        HttpServletRequest request = (HttpServletRequest) ((FilterInvocation) object).getRequest();
        String requestURI = request.getRequestURI();
        // 查询所有菜单
        List<Menu> menus = menuService.getMenus();
        // 遍历菜单
        for (Menu menu : menus) {
            if(antPathMatcher.match(menu.getPattern(),requestURI)){
                String[] roles = menu.getRoles().stream().map(Role::getName).toArray(String[]::new);
                // 将 roles 转化成 List<ConfigAttribute) 对象
                return SecurityConfig.createList(roles);
            }
        }
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

配置自定义的 SecurityMetadataSource

SecurityConfig 配置类

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Resource
    private MyUserDetailService myUserDetailService;

    private final CustomSecurityMetadataSource customSecurityMetadataSource;

    @Autowired
    public SecurityConfig(CustomSecurityMetadataSource securityMetadataSource){
        this.customSecurityMetadataSource = securityMetadataSource;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        // 1. 获取工厂对象
        ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
        // 2. 设置自定义 url 权限处理
        http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>(){

                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setSecurityMetadataSource(customSecurityMetadataSource);
                        // 是否拒绝公共资源的访问
                        object.setRejectPublicInvocations(false);
                        return object;
                    }
                }
                );

        return http
                .formLogin()
                .and()
                .csrf()
                .disable().build();
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(myUserDetailService)
                .and()
                .build();
    }

}

测试代码

测试 Controller 如下(Service、Dao、entity 类代码都没给出来,如果需要该测试工程代码,可以私聊,博客不好全部写出)

@RestController
public class HelloController {

    @GetMapping("/admin/hello")
    public String admin(){
        return "Hello admin!";
    }

    @GetMapping("/user/hello")
    public String user(){
        return "Hello user!";
    }

    @GetMapping("/guest/hello")
    public String guest(){
        return "Hello Guest!";
    }

    @GetMapping("/hello")
    public String hello(){
        return "Hello!";
    }

}

测试效果

拿 user/123 进行测试,从数据库表中可以得知,user 用户只有 ROLE_USER 角色,只能访问 /user/** 和 /guest/** 的资源。

测试效果:

请添加图片描述
效果解释:user 用户只有 ROLE_USER 角色,可以访问 /user/** 资源,也可以访问 /guest/** ,这是原因 Spring Security默认的匹配规则是 OR,访问 /guest/** 只要有 ROLE_GUEST 或 ROLE_USER 即可。而不能访问 /admin/** ,因为该请求需要 ROLE_ADMIN 角色。

四、总结

  • 三大组件:ConfigAttribute(角色的封装体)、AccessDecisionManager(决策管理者,调用 decide 方法进行决策)、AccessDecisionVoter(决策者,通过 vote 方法进行投票决策)。
  • 请求授权流程概述:被 FilterSecurityInterceptor 拦截-》调用invoke方法-》调用父类 AbstractSecurityInterceptorbeforeInvocation 方法-》通过 FilterSecurityMetadataSource 对象调用 getAttribute 得知访问该路径可被哪些权限访问-》将其作为参数调用 attemptAuthorization 进行尝试授权-》通过 AccessDecisionManager 做出决策,做决策者是 AccessDecisionVoter,遍历决策者得出决策结果。
  • AccessDecisionManager 和 AccessDecisionVoter 之间的关系,类似于前期说的认证中的 AuthenticationManager 和 AuthenticationProvider。
  • 自定义持久化URL权限管理时,需要自定义 FilterSecurityMetadataSource 实现类,实现 getAttribute 方法(在该方法中从数据库中获取对应 URL 匹配的角色),然后进行配置。

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

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

相关文章

【C++】构造函数调用规则

欢迎来到博主 Apeiron 的博客&#xff0c;祝您旅程愉快 &#xff01;时止则止&#xff0c;时行则行。动静不失其时&#xff0c;其道光明。 1、缘起 &#xff08;1&#xff09;默认情况下&#xff0c;C 编译器至少给一个类添加 3 个函数 ① 默认构造函数&#xff08;无参&#…

多无人车自动编队

matlab2016b可运行 Kaveh Fathian - Distributed Formation Control of Cars with Collision Avoidance (google.com)

极致呈现系列之:Echarts桑基图的流动旋律

目录 什么是桑基图桑基图的特点及应用场景Echarts中桑基图的常用属性Vue3中创建桑基图美化桑基图 在各种复杂系统中&#xff0c;我们经常需要了解不同流量之间的关系和流动情况。这种信息的可视化呈现对于我们理解系统的结构和转移过程至关重要。桑基图作为一种强大的可视化工具…

Lambda and Collections

我们先从最熟悉的Java集合框架(Java Collections Framework, JCF)开始说起。 为引入Lambda表达式&#xff0c;Java8新增了java.util.funcion包&#xff0c;里面包含常用的函数接口&#xff0c;这是Lambda表达式的基础&#xff0c;Java集合框架也新增部分接口&#xff0c;以便与…

tf卡打不开怎么办?tf卡数据丢失怎么恢复

TF卡打不开怎么办&#xff1f;当TF卡中的数据丢失后&#xff0c;又如何恢复呢&#xff1f;下面小编已为您梳理和归纳了答案&#xff01;请继续阅读下文。 一、TF卡打不开怎么办&#xff1f; 首先&#xff0c;我们需要了解导致TF卡读不出来的具体原因&#xff0c;这可能包括没…

ch8_4中断系统

为什么需要中断&#xff1f; 输入&#xff0c;输出。 计算机程序调试&#xff1b;发生异常事件&#xff1b; 都需要由中断系统进行处理. 引发中断的各种因素包括&#xff1a;人为设置中断&#xff0c;程序性事故&#xff0c; 硬件故障&#xff0c;I/O设备&#xff0c;外部事件等…

英伟达驱动安装

https://zhuanlan.zhihu.com/p/60307377 https://www.nvidia.cn/Download/index.aspx?langcn

路由器的工作原理详解

什么叫路由&#xff1f; 路由器的英文是 Router&#xff0c;也就是「找路的工具」。找什么路&#xff1f;寻找各个网络节点之间的路。 换句话说&#xff0c;路由器就像是快递中转站&#xff0c;包裹会经过一个个的中转站&#xff0c;从遥远的地方寄到你家附近&#xff0c;数据…

驱动开发:内核远程线程实现DLL注入

在笔者上一篇文章《驱动开发&#xff1a;内核RIP劫持实现DLL注入》介绍了通过劫持RIP指针控制程序执行流实现插入DLL的目的&#xff0c;本章将继续探索全新的注入方式&#xff0c;通过NtCreateThreadEx这个内核函数实现注入DLL的目的&#xff0c;需要注意的是该函数在微软系统中…

Java多线程阻塞队列(BlockingDeque)的简析

目录 一.什么是阻塞队列(BlockingDeque) 二.阻塞队列有什么用? 三.运用阻塞队列来实现一个最简单的生产者消费者 四.模拟实现阻塞队列 一.什么是阻塞队列(BlockingDeque) 既然叫做阻塞队列,那么他就满足两个特性 1.队列:先进先出 2.阻塞:空了不让出,满了不让进 &#…

kali常用ping命令探测

ping 判断目标主机网络是否畅通 ping $ip -c 1其中&#xff0c;-c 1 表示发送一个数据包 traceroute 跟踪路由 traceroute $domain ARPING 探测局域网IP ARP&#xff08;地址解析协议&#xff09;&#xff0c;将IP地址转换成MAC地址arping $ip -c 1 #!/bin/ bash######…

云原生监控平台 Prometheus 从部署到监控

1.监控系统架构设计 角色 节点 IP地址 监控端 Prometheus &#xff0c;Grafana&#xff0c;node_exporter &#xff0c;Nginx 47.120.35.251 被监控端1 node_exporter 47.113.177.189 被监控端2 mysqld_exporter&#xff0c;node_exporter&#xff0c;Nginx&#xff…

Centos7下载安装mysql

参考文档&#xff1a;https://xie.infoq.cn/article/5da9bfdfbdaabf7b0b982ab6e https://blog.csdn.net/Lance_welcome/article/details/107314575 一、下载mysql 5.7 # 下载mysql5.7.42版本 wget https://cdn.mysql.com//Downloads/MySQL-5.7/mysql-5.7.42-linux-glibc2.12-…

Mysql 表的七种连接方式【附带练习sql】

连接 七种JOIN介绍 图形连接方式说明SQL内连接共有部分SELECT <select_list> FROM TableA A INNER JOIN TableB B ON A.Key B.Key;左连接A表独有共有部分SELECT <select_list> FROM TableA A LEFT JOIN TableB B ON A.Key B.Key;右连接B表独有共有部分SELECT &…

字符设备驱动内部实现原理解析以及分步注册流程和代码示例

1、字符设备驱动内部实现原理解析 原理&#xff1a;用户层调用 open() 函数打开设备文件&#xff0c;用ls-i查看inode号并找到与之对应的struct inode 结构体。在struct inode 结构体中&#xff0c;可以找到与文件关联的 struct cdev 设备驱动结构体。设备驱动结构体中包含了文…

Spring6 数据校验 Validation

1、Spring Validation概述 在开发中&#xff0c;经常遇到参数校验的需求&#xff0c;比如用户注册的时候&#xff0c;要校验用户名不能为空、用户名长度不超过20个字符、手机号是合法的手机号格式等等。如果使用普通方式&#xff0c;会把校验的代码和真正的业务处理逻辑耦合在一…

FL Studio 21中文永久版网盘下载(含Key.reg注册表补丁)

FL Studio 21全称Fruity Loops Studio&#xff0c;就是大家熟悉的水果编曲软件&#xff0c;一个全能的音乐制作软件&#xff0c;包括编曲、录音、剪辑和混音等诸多功能&#xff0c;让你的电脑编程一个全能的录音室。FL Studio 21版本发布了&#xff0c;为我们带来了多种新功能&…

在Centos Stream 9上Docker的实操教程(八) - Docker可视化管理工具

&#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Micro麦可乐的博客 &#x1f425;《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程&#xff0c;入门到实战 &#x1f33a;《RabbitMQ》…

构建二叉树的两种情况【根据前序遍历和中序遍历 构造树】【根据后序遍历和中序遍历 构造树】

【根据前序遍历和中序遍历 构造树】【根据后序遍历和中序遍历 构造树】 6. 重建二叉树根据前序遍历和中序遍历 得到树 树的遍历 6. 重建二叉树 原题链接 根据前序遍历和中序遍历 得到树 过程如下&#xff1a; 首先根据前序遍历找到 根节点找到中序遍历中&#xff0c;该根节点…

C# Http 请求接口 Get / Post

目录 一、概述 二、创建 Web API 三、HttpRequestHelper 三、测试 结束 一、概述 get 和 post 请求&#xff0c;最早被用来做浏览器与服务器之间交互HTML和表单的通讯协议&#xff0c;后来又被广泛的扩充到接口格式的定义上&#xff0c;到目前为止&#xff0c;get / pos…