您现在的位置是:亿华云 > 知识

最新版 Spring Security,该如何实现动态权限管理?

亿华云2025-10-04 03:49:58【知识】4人已围观

简介来源:江南一点雨在松哥之前的教程中,曾经和小伙伴们聊过动态权限管理的问题,大家在公众号江南一点雨后台回复 s用不了在松哥之前的教程中,曾经和小伙伴们聊过动态权限管理的问题,大家在公众号江南一点雨后台回

来源:江南一点雨

在松哥之前的最新教程中,曾经和小伙伴们聊过动态权限管理的何实问题,大家在公众号江南一点雨后台回复 s用不了在松哥之前的现动限管教程中,曾经和小伙伴们聊过动态权限管理的态权问题,大家在公众号江南一点雨后台回复 ss 有lss 有oDF 教程。最新

最新版 Spring Security,该如何实现动态权限管理?

但是何实,不知道小伙伴们是现动限管否有留意过,进入到 Spring Boot3 之后,态权Spring Security 现在也进化到 Spring Security6 了,最新Spring Security6 的何实用法跟之前比起来还是有很大差异,松哥之前写了篇文章和小伙伴们介绍 Spring Security6 中的现动限管一些变化:Spring Security6 全新写法,大变样!态权。最新

最新版 Spring Security,该如何实现动态权限管理?

最近有小伙伴说松哥你之前讲的何实动态权限定义的方式,新版中用不了了,现动限管我抽空看了下,今天就和小伙伴们聊聊这个话题。

最新版 Spring Security,该如何实现动态权限管理?

1. 权限开发思路

先来说权限开发的思路,当我们设计好 RBAC 权限之后,具体到代码层面,我们有两种实现思路:

直接在接口/Service 层方法上添加权限注解,这样做的服务器租用好处是实现简单,但是有一个问题就是权限硬编码,每一个方法需要什么权限都是代码中配置好的,后期如果想通过管理页面修改是不可能的,要修改某一个方法所需要的权限只能改代码。将请求和权限的关系通过数据库来描述,每一个请求需要什么权限都在数据库中配置好,当请求到达的时候,动态查询,然后判断权限是否满足,这样做的好处是比较灵活,将来需要修改接口和权限之间的关系时,可以通过管理页面点击几下,问题就解决了,不用修改代码,松哥之前的 vhr 中就是这样做的。亿华云计算

有的小伙伴觉得第二种方案无法做到按钮级别的权限控制,这其实是一个误解。想要做到按钮级别的权限控制,只需要数据库中细化配置即可。

2. 具体实践

2.1 旧方案回顾

在 vhr 中,松哥是通过重写两个类来和实现动态权限的。

第一个类是收集权限元数据的类:

@Componentpublic class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource 

{

    @Override    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException 

{

        //...

    }

    @Override    public Collection<ConfigAttribute> getAllConfigAttributes() 

{

        return null

;

    }

    @Override    public boolean supports(Class<?> clazz) 

{

        return true

;

    }

}

在 getAttributes 方法中,根据当前请求的 URL 地址(从参数 Object 中可提取出来),然后根据权限表中的配置,分析出来当前请求需要哪些权限并返回。

另外我还重写了一个决策器,其实决策器也可以不重写,就看你自己的需求,如果 Spring Security 自带的决策器无法满足你的需求,那么可以自己写一个决策器:

@Componentpublic class CustomUrlDecisionManager implements AccessDecisionManager 

{

    @Override    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException 

{

        //...

    }

    @Override    public boolean supports(ConfigAttribute attribute) 

{

        return true

;

    }

    @Override    public boolean supports(Class<?> clazz) 

{

        return true

;

    }

}

decide 方法就是做决策的地方,第一个参数中可以提取出当前用户具备什么权限,第三个参数是当前请求需要什么权限,亿华云比较一下就行了,如果当前用户不具备需要的权限,则直接抛出 AccessDeniedException 异常即可。

最后,通过 Bean 的后置处理器 BeanPostProcessor,将这两个配置类放到 Spring Security 的 FilterSecurityInterceptor 拦截器中:

@Overrideprotected void configure(HttpSecurity http) throws Exception 

{

    http.authorizeRequests()

            .withObjectPostProcessor(new

 ObjectPostProcessor() {

                @Override                public <O extends FilterSecurityInterceptor> O postProcess(O object) 

{

                    object.setAccessDecisionManager(customUrlDecisionManager);

                    object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);

                    return

 object;

                }

            })

            .and()

            //...

}

大致上的逻辑就是如此,以上类完整代码小伙伴们可以参考 https://github.com/lenve/vhr。

2.2 新方案

不过以上代码在目前最新的 Spring Security6 中用不了了,不是因为类过期了,而是因为类被移除了!哪个类被移除了?FilterSecurityInterceptor。

FilterSecurityInterceptor 这个过滤器以前是做权限处理的,但是在新版的 Spring Security6 中,这个拦截器被 AuthorizationFilter 代替了。

老实说,新版的方案其实更合理一些,传统的方案感觉带有很多前后端不分的影子,现在就往更纯粹的前后端分离奔去。

由于新版中连 FilterSecurityInterceptor 都不用了,所以旧版的方案显然行不通了,新版的方案实际上更加简单。

虽然新旧写法不同,但是核心思路是一模一样。

我们来看下新版的配置:

@BeanSecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception 

{

    http.authorizeHttpRequests(register -> register.anyRequest().access((authentication, object) -> {

                //表示请求的 URL 地址和数据库的地址是否匹配上了                boolean isMatch = false

;

                //获取当前请求的 URL 地址

                String requestURI = object.getRequest().getRequestURI();

                List menuWithRole = menuService.getMenuWithRole();

                for

 (MenuWithRoleVO m : menuWithRole) {

                    if

 (antPathMatcher.match(m.getUrl(), requestURI)) {

                        isMatch = true

;

                        //说明找到了请求的地址了                        //这就是当前请求需要的角色

                        List roles = m.getRoles();

                        //获取当前登录用户的角色

                        Collection  authorities = authentication.get().getAuthorities();

                        for

 (GrantedAuthority authority : authorities) {

                            for

 (Role role : roles) {

                                if

 (authority.getAuthority().equals(role.getName())) {

                                    //说明当前登录用户具备当前请求所需要的角色                                    return new AuthorizationDecision(true

);

                                }

                            }

                        }

                    }

                }

                if

 (!isMatch) {

                    //说明请求的 URL 地址和数据库的地址没有匹配上,对于这种请求,统一只要登录就能访问                    if (authentication.get() instanceof

 AnonymousAuthenticationToken) {

                        return new AuthorizationDecision(false

);

                    } else

 {

                        //说明用户已经认证了                        return new AuthorizationDecision(true

);

                    }

                }

                return new AuthorizationDecision(false

);

            }))

            .formLogin(form -> 

            //...

            )

            .csrf(csrf -> 

            //...

            )

            .exceptionHandling(e -> 

            //...

            )

            .logout(logout ->

            //...

            );

    return

 http.build();

}

核心思路还是和之前一样,只不过现在的工作都在 access 方法中完成。

access 方法的回调中有两个参数,第一个参数是 authentication,很明显,这就是当前登录成功的用户对象,从这里我们就可以提取出来当前用户所具备的权限。

第二个参数 object 实际上是一个 RequestAuthorizationContext,从这个里边可以提取出来当前请求对象 HttpServletRequest,进而提取出来当前请求的 URL 地址,然后依据权限表中的信息,判断出当前请求需要什么权限,再和 authentication 中提取出来的当前用户所具备的权限进行对比即可。

如果当前登录用户具备请求所需要的权限,则返回 new AuthorizationDecision(true);,否则返回 new AuthorizationDecision(false); 即可。

其实无论什么框架,只要能把其中一个版本掌握个 70%,以后无论它怎么升级,你都能快速上手!

很赞哦!(17)