springsecurity开启方法级的授权源码分析

news2025/1/18 18:43:43

1、话不多说,先说使用步骤然后分析源码:

首先使用 @EnableMethodSecurity 注解开启方法级别的权限认证

### 使用该注解开启方法级权限鉴定
@EnableMethodSecurity

使用了方法权限注解开启了方法级的权限鉴定之后,就可以使用如下注解直接在控制器上使用方法级的权限鉴定了。

### 使用方法级的权限鉴定
@PreAuthorize("hasAuthority('sys:user:update')")

具体使用如下:

@PutMapping("/updateUser")
@Operation(description = "更新用户信息")
@PreAuthorize("hasAuthority('sys:user:update')")
public R<Object> update(@RequestBody User user) {
    // 如果密码不为空,则进行加密处理
    if(StrUtil.isNotBlank(user.getPassword())){
        user.setPassword(passwordEncoder.encode(user.getPassword()));
    }
    return R.ok(userService.updateById(user));
}

@PreAuthorize("hasAuthority('sys:user:update')") 实现逻辑是aop拦截,会拦截每次请求,判断当前访问的用户权限情况,源码如下:

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuthorize {

	/**
	 * @return 在调用受保护的方法之前要计算的Spring EL表达式
	 */
	String value();

}

至于 @PreAuthorize 注解的拦截器是如何生效的那就要靠 @EnableMethodSecurity 注解,因为该注解中导入了 @Import({MethodSecuritySelector.class}) 并接入到了IOC容器,因此只需要看 MethodSecuritySelector 类具体是怎么处理方法级的认证的即可。部分关键源码如下:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({MethodSecuritySelector.class})		  // 注解的作用就是向IOC容器注入该选择器类
public @interface EnableMethodSecurity {
    boolean prePostEnabled() default true;    // 注意此属性默认为true,后面的源码会用到

    boolean securedEnabled() default false;

    boolean jsr250Enabled() default false;

    boolean proxyTargetClass() default false;

    AdviceMode mode() default AdviceMode.PROXY;
}

MethodSecuritySelector 的部分关键源码如下:

final class MethodSecuritySelector implements ImportSelector {
    private final ImportSelector autoProxy = new AutoProxyRegistrarSelector();

    MethodSecuritySelector() {
    }

    public String[] selectImports(@NonNull AnnotationMetadata importMetadata) {
        if (!importMetadata.hasAnnotation(EnableMethodSecurity.class.getName())) {
            return new String[0];
        } else {
            EnableMethodSecurity annotation = (EnableMethodSecurity)importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
            List<String> imports = new ArrayList(Arrays.asList(this.autoProxy.selectImports(importMetadata)));
            // 从上面可知 prePostEnabled 默认为true,其他都为false,
            // 因此PrePostMethodSecurityConfiguration会被注册,其他需要待注解中手动属性设置
            if (annotation.prePostEnabled()) {
                imports.add(PrePostMethodSecurityConfiguration.class.getName());
            }

            if (annotation.securedEnabled()) {
                imports.add(SecuredMethodSecurityConfiguration.class.getName());
            }

            if (annotation.jsr250Enabled()) {
                imports.add(Jsr250MethodSecurityConfiguration.class.getName());
            }

            return (String[])imports.toArray(new String[0]);
        }
    }
}

PrePostMethodSecurityConfiguration 部分核心源码如下:

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class PrePostMethodSecurityConfiguration {

    
    // 注册了一个切面
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    static Advisor preAuthorizeAuthorizationMethodInterceptor(ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
            ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
            ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
            ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
            ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
        
        // 创建了一个前置鉴权管理器,前置意为请求被controller处理前
        PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
        
        // 前置鉴权管理器设置了一个EL表达式处理器
        manager.setExpressionHandler(
                expressionHandlerProvider.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, context)));
        
        // 该拦截器实际就是一个定制专门用于 preAuthorize 注解的切面
        AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
                .preAuthorize(manager(manager, registryProvider));
        
        // 给拦截器设置上了当前登录用户的 SecurityContextHolder,方便获取认证对象
        strategyProvider.ifAvailable(preAuthorize::setSecurityContextHolderStrategy);
        
        // 给拦截器设置上了授权事件发布器
        eventPublisherProvider.ifAvailable(preAuthorize::setAuthorizationEventPublisher);
        return preAuthorize;
    }

    // 定义了一个EL表达式处理器
    private static MethodSecurityExpressionHandler defaultExpressionHandler(
            ObjectProvider<GrantedAuthorityDefaults> defaultsProvider, ApplicationContext context) {
        DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
        defaultsProvider.ifAvailable((d) -> handler.setDefaultRolePrefix(d.getRolePrefix()));
        handler.setApplicationContext(context);
        return handler;
    }

    // 定义了一个授权管理器
    static <T> AuthorizationManager<T> manager(AuthorizationManager<T> delegate,
            ObjectProvider<ObservationRegistry> registryProvider) {
        return new DeferringObservationAuthorizationManager<>(registryProvider, delegate);
    }

}

下面重点看上面调用的拦截器切面中的 preAuthorize 方法来构造拦截器切面的具体内容吧!

从上面可以看到这个拦截器切面是一个针对特定 PreAuthorize 注解才生效的拦截器,而这个拦截器本身实现了PointcutAdvisor 接口PointcutAdvisor中包含了一个Pointcut成员变量,也就是切点,在加上继承的Advisor接口中的Advice,就形成了一个完整的切面

因此也就可以说 AuthorizationManagerBeforeMethodInterceptor 拦截器是一个专门用于拦截标有 PreAuthorize 注解的切面。

处理 PreAuthorize 切点中表达式分析过程如下:

上面 PrePostMethodSecurityConfiguration 关键源码中,我们看到注入了一个 MethodSecurityExpressionHandler 表达式处理器,实际使用的是他的实现类DefaultMethodSecurityExpressionHandler 处理器,其中的这部分源码解释可以看出如何使用表达式的过程。

因此,过程就是,授权管理器AuthorizationManager设置了一个方法级前置授权管理器PreAuthorizeAuthorizationManager 和一个EL表达式处理器 MethodSecurityExpressionHandler

然后AuthorizationManager作为参数用于构造切面

因此该切面就拥有的一个可处理EL表达式的处理器DefaultMethodSecurityExpressionHandler

hasAuthority 方法的处理逻辑:

从上面的 hasAuthority 方法的处理过程可以看出,最终是从我们封装的认证对象中获取权限集合,然后从该权限集合中获取权限的字符串名称,将他们组装成一个Set<String>集合,最终的鉴权过程演变成了遍历字符串集合,判断传入 @PreAuthorize("hasAuthority('sys:user:update')") 的中的字符串sys:user:update 是否被包含在 Set 字符串集合中。原来如此啊!!!是不是突然觉得有一种拨开云雾的感觉呢。

因此,我们要做的就是在登录成功时,将用户角色对应的所有权限(也可以说是菜单或按钮的操作权,例如:sys:user:insert / sys:user:update 等等)全部查出封装成对应的权限对象 GrantedAuthority ,角色和权限不可混为一谈,角色是一组权限的集合,给一个用户赋予了一个角色也就相当于给了他一组权限。

此外,基于权限点和基于角色的授权两者只在验证名称上稍有不同,基于角色的授权中EL表达式中的权限标识会被拼接上 ROLE_ 前缀,如下:

@PreAuthorize("hasRole('admin')") 如果我们这样使用,那么最后和从认证对象中权限名称对比的就是 ROLE_admin ,因此数据库设置角色权限时,名称角色名称需要加上该 ROLE_前缀,可以这样设计: ROLE_admin

我的 Spring Security 参考配置如下:

@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final UserDetailService userDetailService;
    private final SmsService tencentSmsService;

    // 访问白名单,可以设计成读取数据库或缓存
    String[] whitelist = {"/sms/**","/auth/**"};

    @Bean
    @SneakyThrows
    public SecurityFilterChain securityFilterChain(HttpSecurity http) {
        // 全局请求策略配置
        http
                .csrf().disable()       // 禁用csrf跨站请求伪造
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers(whitelist).permitAll()     // 白名单全部放行
                        .requestMatchers(HttpMethod.OPTIONS).permitAll()        // options请求放行
                        .anyRequest().authenticated()       // 其他请求需要认证
                )
                // 禁用session,改用JWT的方式
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 添加自定义认证过滤器,按照默认的security所有过滤器顺序
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                // 设置自定义认证、鉴权异常处理
                .exceptionHandling()
                // 自定义认证异常处理
                .authenticationEntryPoint(blogAuthenticationEntryPoint())
                // 自定义鉴权异常处理
                .accessDeniedHandler(blogAccessDeniedHandler());

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager() {
        List<AuthenticationProvider> providers = new ArrayList<>(3);
        // 可以设置多个认证处理器
        AuthenticationProvider usernameAuthenticationProvider = new DaoAuthenticationProvider();
        MobileAuthenticationProvider mobileAuthenticationProvider = new MobileAuthenticationProvider(userDetailService,tencentSmsService);
        providers.add(usernameAuthenticationProvider);
        providers.add(mobileAuthenticationProvider);
        return new ProviderManager(providers);
    }


    private BlogAuthenticationEntryPoint blogAuthenticationEntryPoint() {
        return new BlogAuthenticationEntryPoint();
    }

    private BlogAccessDeniedHandler blogAccessDeniedHandler() {
        return new BlogAccessDeniedHandler();
    }


    // 笔记1: 凡是在springsecurity的认证和授权过程中抛出的异常,不论是我们认为抛出的异常还是走源码过程抛出的异常都会被springsecurity过滤器链中的ExceptionTranslationFilter异常处理的过滤器捕获
    // 并调用相应的接口方法进行异常处理,所以会导致这些异常的响应结果和我们自定义的响应结果不一致的情况,为了保证给前端的响应结果的一致性,我们有必要对相应的接口
    // 进行自定义的实现,以此来保证响应结果的统一性。同时在完成自定义处理之后,我们必须将实现注册成Bean,并将他们加入到springsecurity过滤环节的对应节点,
    // 也就是在HttpSecurity对象的对应节点进行配置,以此确保自定义实现的组件能够生效。相应接口有:认证失败处理AuthenticationEntryPoint、鉴权失败处理AccessDeniedHandler

    // 笔记2:  自从springsecurity-5.7.X版本开始,官方将废弃WebSecurityConfigurerAdapter这个springsecurity的适配器类,转而推荐使用向容器注入SecurityFilterChain
    // 安全过滤器链的方法配置springsecurity

    // 笔记3:  spring官方推荐使用构造函数的方式完成依赖注入,相应的我们可以使用lombok的相应注解完成,在开发过程中应当减少使用像@Autowired、@Resource的注解

    // 笔记4: 在我们对springsecurity进行配置时,如果我们不对HttpSecurity对象配置formLogin属性的话,则在过滤器链中将不会存在UsernamePasswordAuthenticationFilter过滤器
    // 原因是在springsecurity的默认配置中为HttpSecurity对象配置了formLogin属性,在该属性中配置了一个默认的登陆页面,同时new了一个UsernamePasswordAuthenticationFilter放了容器
    // 但如果我们自定义配置springsecurity时没有配置formLogin属性则不会走UsernamePasswordAuthenticationFilter这一套的过滤器逻辑。所以,对于认证和授权的实现方案,我们可以总结为两套,
    // 一套是,走我们自定义的过滤器并没有配置formLogin属性的认证方案;另一套是,走springsecurity默认的通过走UsernamePasswordAuthenticationFilter的这一套方案,走这套方案需要我们
    // 在对springsecurity进行自定配置是为HttpSecurity对象配置formLogin属性,此外,我们还可以对这套方案中的一些流程处理进行自定义实现,比如自定义实现AuthenticationSuccessHandler
    // AuthenticationFailureHandler等一些关键性流程的自我定制化实现,以此来更好的完成我们的认证授权。此外、如果我们也想对登出操作进行自定义的话,我们同理可以对HttpSecurity对象配置logout属性
    // 然后自定义实现LogoutSuccessHandler

    // SecurityContextHolderFilter、AuthorizationFilter
    // 如果我们不是用默认的formLogin的方式,则需要自定义认证过滤器,然后在其中使用 AuthenticationManager 来认证登录信息
}

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

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

相关文章

【微信小程序】-- WXSS 模板样式- rpx import (十三)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &…

OpenCV入门(一)Python环境的搭建

OpenCV入门&#xff08;一&#xff09;Python环境的搭建 因为有点Python基础&#xff0c;并且Python是比较好入门的编程语言&#xff0c;所以&#xff0c;机器视觉后面打算在Python这个平台下进行。 Windows平台OpenCV的Python开发环境搭建 1、Python 的下载与安装 Python是…

Unity UGUI 拖拽组件

效果展示 使用方式 拖到图片上即可用 父节点会约束它的活动范围哦~ 父节点会约束它的活动范围哦~ 父节点会约束它的活动范围哦~ 源码 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems;/// <summary> /…

2023年蜂巢科技最新面试题

2023年蜂巢科技最新面试题 bio与nio的区别 bio同步阻塞io&#xff1a;在此种⽅式下&#xff0c;⽤户进程在发起⼀个IO操作以后&#xff0c;必须等待IO操作的完成&#xff0c;只有当真正完成了IO操作以后&#xff0c;⽤户进程才能运⾏。JAVA传统的IO模型属于此种⽅式&#xff0…

易观:商业银行持续发力趣味营销活动,助力提升手机银行用户活跃度

易观&#xff1a;随着数字化时代发展&#xff0c;商业银行仍需持续在场景营销领域积极探索创新&#xff0c;坚持以用户为中心&#xff0c;开展千人千面的趣味营销活动&#xff0c;增强手机银行的互动性&#xff0c;完善活动参与的用户体验&#xff0c;提升营销活动效果&#xf…

了解高并发场景下的限流算法和解决方案

想必大家在做项目的时候&#xff0c;或多或少的都遇到过一些高并发的场景&#xff0c;这里主要是和大家一起来探讨下有关高并发下的处理方案。 常见的限流算法 1. 计数器 直接计数&#xff0c;简单暴力&#xff0c;举个例子&#xff1a; 比如限流设定为1小时内10次&#xff…

【Maven】(四)图解Maven3依赖的功能特性:依赖范围、依赖传递、依赖冲突

文章目录1.前言2.pom中的依赖配置2.1.依赖的概念2.2.依赖传递2.3.可选依赖 [optional]2.4.依赖范围 [scope]2.4.1.scope的分类2.4.2.依赖范围对依赖传递的影响2.5.依赖冲突2.5.1.直接依赖2.5.2.间接依赖2.6.依赖排除 [exclusions]3.总结1.前言 本系列文章记录了 Maven 从0开始…

搭建XXL-JOB

搭建XXL-JOB 1、 调度中心 首先下载XXL-JOB GitHub&#xff1a;GitHub - xuxueli/xxl-job: A distributed task scheduling framework.&#xff08;分布式任务调度平台XXL-JOB&#xff09; 码云&#xff1a;xxl-job: 一个分布式任务调度平台&#xff0c;其核心设计目标是开…

JDK17 下载与安装

JDK是 Java 语言的软件开发工具包&#xff0c;主要用于移动设备、嵌入式设备上的java应用程序。JDK是整个java开发的核心&#xff0c;它包含了JAVA的运行环境&#xff08;JVMJava系统类库&#xff09;和JAVA工具。 1、下载JDK17 1.1、首先&#xff0c;检查下电脑中是否安装jd…

车机系统开发——Android Automotive

Android Automotive介绍 Android Automotive是⼀个基本的Android平台&#xff0c;它运⾏预安装的&#xff08;车载信息娱乐&#xff09;IVI系统&#xff0c;Android应⽤程序以及可选的第⼆⽅和第三⽅Android应⽤程序。 Android Automotive的硬件抽象层(HAL)为Android框架提供…

封装小程序request请求[接口函数]

在这篇小程序API的Promise化文章中讲到小程序官方提供的异步API都是基于回调函数来实现的&#xff0c;在大量的使用这种回调函数就会造成回调地狱的问题&#xff0c;以及代码的可读性和可维护性差&#xff0c;通过对小程序API的Promise化能解决&#xff0c;那么本篇是来讲进行对…

203. 移除链表元素 L2-027 名人堂与代金券

给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,6,3,4,5,6], val 6 输出&#xff1a;[1,2,3,4,5] 示例 2&#xff1a; 输入&#…

内网离线安装docker-ce工具,带你来了解!

虽然通常我们都是使用网络来安装 docker 的&#xff0c;但是对于安全要求比较高的业务或者用户来说&#xff0c;部署产品需要在内网部署&#xff0c;而内网通常是无法访问外部网络的&#xff0c;所以就需要通过内网进行安装了。 1. yum 离线安装包获取方法 下载软件包 这里以…

语义分割数据标注案例分析

语义分割&#xff08;Semantic Segmentation&#xff09;是计算机视觉领域中的一种重要任务&#xff0c;它的目的是将图像中的每个像素分配到对应的语义类别中。简单来说&#xff0c;就是将一张图像分割成多个区域&#xff0c;并为每个像素指定一个标签&#xff0c;标识出它属于…

实时手势识别(C++与python都可实现)

一、前提配置&#xff1a; Windows&#xff0c;visual studio 2019&#xff0c;opencv&#xff0c;python10&#xff0c;opencv-python&#xff0c;numpy&#xff0c;tensorflow&#xff0c;mediapipe&#xff0c;math 1.安装python环境 这里我个人使用的安装python10&#…

MySQL进阶篇之MySQL管理

07、MySQL管理 7.1、系统数据库 MySQL数据库安装完成后&#xff0c;自带了四个数据库&#xff0c;具体作用如下&#xff1a; 数据库含义mysql存储MySQL服务器正常运行所需要的各种信息&#xff08;时区、主从、用户、权限等&#xff09;information_schema提供了访问数据库元…

科技赋能智慧警务,“链上天眼科技助警中国行”在京启动

2月28日&#xff0c;由全球领先的区块链大数据科技企业欧科云链和中国警察网联合举办的“链上天眼科技助警中国行”活动&#xff08;下称“活动”&#xff09;&#xff0c;在北京正式启动。为了普及区块链基础知识&#xff0c;以及虚拟货币犯罪追踪与打击的新型技术应用经验&am…

(三)随处可见的LED广告屏是怎么工作的呢?接入GUI

续上文&#xff0c;本篇我们将尝试接入一个GUI来控制点阵屏。在前两篇中&#xff0c;我们相继介绍了点阵屏的控制原理&#xff0c;以及如何让点阵屏按照我们所想的进行显示。本篇将在此基础上接入一个GUI&#xff0c;使点阵屏的控制更加优雅。限于阅读体验和展示效果&#xff0…

王道计算机网络课代表 - 考研计算机 第一章 计算机网络体系结构 究极精华总结笔记

本篇博客是考研期间学习王道课程 传送门 的笔记&#xff0c;以及一整年里对 计算机网络 知识点的理解的总结。希望对新一届的计算机考研人提供帮助&#xff01;&#xff01;&#xff01; 关于对 “计算机网络体系结构” 章节知识点总结的十分全面&#xff0c;涵括了《计算机网络…

SpringMVC之JSON工具:Jackson Gson 和fastjson通过JSON工具来解决文字乱码和时间格式问题——通过JSON工具生成JSON

什么是Json&#xff1f; JSON&#xff1a;JavaScript Object NotationJS对象 它是一种轻量级的数据交换格式JSON&#xff08;当前是交互的顶流&#xff09;&#xff0c;它自身具有独立的编程格式&#xff0c;它的特点是简洁和清晰&#xff0c;Json的存在大大改造了网络传输的…