Spring Security 动态权限实现方案

Spring Security-动态权限控制(1) spring security 动态权限控制 阅读详情

最近在做 TienChin 项目,用的是 RuoYi-Vue 脚手架,在这个脚手架中,访问某个接口需要什么权限,这个是在代码中硬编码的,具体怎么实现的,松哥下篇文章来和大家分析,有的小伙伴可能希望能让这个东西像 vhr 一样,可以在数据库中动态配置,因此这篇文章和小伙伴们简单介绍下 Spring Security 中的动态权限方案,以便于小伙伴们更好的理解 TienChin 项目中的权限方案。

1. 动态管理权限规则

通过代码来配置 URL 拦截规则和请求 URL 所需要的权限,这样就比较死板,如果想要调整访问某一个 URL 所需要的权限,就需要修改代码。

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

1.1 数据库设计

简单起见,我们这里就不引入权限表了,直接使用角色表,用户和角色关联,角色和资源关联,设计出来的表结构如图 13-9 所示。

图13-9  一个简单的权限数据库结构

menu 表是相当于我们的资源表,它里边保存了访问规则,如图 13-10 所示。

图13-10  访问规则

role 是角色表,里边定义了系统中的角色,如图 13-11 所示。

图13-11  用户角色表

user 是用户表,如图 13-12 所示。

图13-12  用户表

user_role 是用户角色关联表,用户具有哪些角色,可以通过该表体现出来,如图 13-13 所示。

图13-13  用户角色关联表

menu_role 是资源角色关联表,访问某一个资源,需要哪些角色,可以通过该表体现出来,如图 13-14 所示。

图13-14  资源角色关联表

至此,一个简易的权限数据库就设计好了(在本书提供的案例中,有SQL脚本)。

1.2 实战

项目创建

创建 Spring Boot 项目,由于涉及数据库操作,这里选用目前大家使用较多的 MyBatis 框架,所以除了引入 Web、Spring Security 依赖之外,还需要引入 MyBatis 以及 MySQL 依赖。

最终的 pom.xml 文件内容如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.3</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

项目创建完成后,接下来在 application.properties 中配置数据库连接信息:

spring.datasource.username=root
spring.datasource.password=123
spring.datasource.url=jdbc:mysql:///security13?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

配置完成后,我们的准备工作就算完成了。

创建实体类

根据前面设计的数据库,我们需要创建三个实体类。

首先来创建角色类 Role:

public class Role {
    private Integer id;
    private String name;
    private String nameZh;
       //省略getter/setter
}

然后创建菜单类 Menu:

public class Menu {
    private Integer id;
    private String pattern;
    private List<Role> roles;
    //省略getter/setter
}

菜单类中包含一个 roles 属性,表示访问该项资源所需要的角色。

最后我们创建 User 类:

public class User implements UserDetails {
    private Integer id;
    private String password;
    private String username;
    private boolean enabled;
    private boolean locked;
    private List<Role> roles;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream()
                        .map(r -> new SimpleGrantedAuthority(r.getName()))
                        .collect(Collectors.toList());
    }
    @Override
    public String getPassword() {
        return password;
    }
    @Override
    public String getUsername() {
        return username;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return enabled;
    }
    //省略其他getter/setter
}

由于数据库中有 enabled 和 locked 字段,所以 isEnabled() 和 isAccountNonLocked() 两个方法如实返回,其他几个账户状态方法默认返回 true 即可。在 getAuthorities() 方法中,我们对 roles 属性进行遍历,组装出新的集合对象返回即可。

创建Service

接下来我们创建 UserService 和 MenuService,并提供相应的查询方法。

先来看 UserService:

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) 
                                             throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        user.setRoles(userMapper.getUserRoleByUid(user.getId()));
        return user;
    }
}

这段代码应该不用多说了,不熟悉的读者可以参考本书 2.4 节。

对应的 UserMapper 如下:

@Mapper
public interface UserMapper {
    List<Role> getUserRoleByUid(Integer uid);
    User loadUserByUsername(String username);
}

UserMapper.xml:

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.javaboy.base_on_url_dy.mapper.UserMapper">
    <select id="loadUserByUsername" 
                         resultType="org.javaboy.base_on_url_dy.model.User">
        select * from user where username=#{username};
    </select>
    <select id="getUserRoleByUid" 
                         resultType="org.javaboy.base_on_url_dy.model.Role">
        select r.* from role r,user_role ur where ur.uid=#{uid} and ur.rid=r.id
    </select>
</mapper>

再来看 MenuService,该类只需要提供一个方法,就是查询出所有的 Menu 数据,代码如下:

@Service
public class MenuService {
    @Autowired
    MenuMapper menuMapper;
    public List<Menu> getAllMenu() {
        return menuMapper.getAllMenu();
    }
}

MenuMapper:

@Mapper
public interface MenuMapper {
    List<Menu> getAllMenu();
}

MenuMapper.xml:

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.javaboy.base_on_url_dy.mapper.MenuMapper">
    <resultMap id="MenuResultMap" 
                                type="org.javaboy.base_on_url_dy.model.Menu">
        <id property="id" column="id"/>
        <result property="pattern" column="pattern"></result>
        <collection property="roles" 
                              ofType="org.javaboy.base_on_url_dy.model.Role">
            <id column="rid" property="id"/>
            <result column="rname" property="name"/>
            <result column="rnameZh" property="nameZh"/>
        </collection>
    </resultMap>
    <select id="getAllMenu" resultMap="MenuResultMap">
        select m.*,r.id as rid,r.name as rname,r.nameZh as rnameZh from menu m left join menu_role mr on m.`id`=mr.`mid` left join role r on r.`id`=mr.`rid`
    </select>
</mapper>

需要注意,由于每一个 Menu 对象都包含了一个 Role 集合,所以这个查询是一对多,这里通过 resultMap 来进行查询结果映射。

至此,所有基础工作都完成了,接下来配置 Spring Security。

配置Spring Security

回顾 13.3.6 小节的内容,SecurityMetadataSource 接口负责提供受保护对象所需要的权限。在本案例中,受保护对象所需要的权限保存在数据库中,所以我们可以通过自定义类继承自 FilterInvocationSecurityMetadataSource,并重写 getAttributes 方法来提供受保护对象所需要的权限,代码如下:

@Component
public class CustomSecurityMetadataSource 
                         implements FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuService menuService;
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) 
                                               throws IllegalArgumentException {
        String requestURI = 
                   ((FilterInvocation) object).getRequest().getRequestURI();
        List<Menu> allMenu = menuService.getAllMenu();
        for (Menu menu : allMenu) {
            if (antPathMatcher.match(menu.getPattern(), requestURI)) {
                String[] roles = menu.getRoles().stream()
                               .map(r -> r.getName()).toArray(String[]::new);
                return SecurityConfig.createList(roles);
            }
        }
        return null;
    }

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

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

自定义 CustomSecurityMetadataSource 类并实现 FilterInvocationSecurityMetadataSource 接口,然后重写它里边的三个方法:

  • getAttributes:该方法的参数是受保护对象,在基于 URL 地址的权限控制中,受保护对象就是 FilterInvocation;该方法的返回值则是访问受保护对象所需要的权限。在该方法里边,我们首先从受保护对象 FilterInvocation 中提取出当前请求的 URL 地址,例如/admin/hello,然后通过 menuService 对象查询出所有的菜单数据(每条数据中都包含访问该条记录所需要的权限),遍历查询出来的菜单数据,如果当前请求的 URL 地址和菜单中某一条记录的 pattern 属性匹配上了(例如/admin/hello 匹配上/admin/**),那么我们就可以获取当前请求所需要的权限。从 menu 对象中获取 roles 属性,并将其转为一个数组,然后通过SecurityConfig.createList 方法创建一个Collection<ConfigAttribute> 对象并返回。如果当前请求的 URL 地址和数据库中 menu 表的所有项都匹配不上,那么最终返回 null。如果返回 null,那么受保护对象到底能不能访问呢?这就要看 AbstractSecurityInterceptor 对象中的 rejectPublicInvocations 属性了,该属性默认为 false,表示当 getAttributes 方法返回 null 时,允许访问受保护对象(回顾 13.4.4 小节中关于AbstractSecurityInterceptor#beforeInvocation 的讲解)。
  • getAllConfigAttributes:该方法可以用来返回所有的权限属性,以便在项目启动阶段做校验,如果不需要校验,则直接返回 null 即可。
  • supports:该方法表示当前对象支持处理的受保护对象是 FilterInvocation。

CustomSecurityMetadataSource 类配置完成后,接下来我们要用它来代替默认的 SecurityMetadataSource 对象,具体配置如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    CustomSecurityMetadataSource customSecurityMetadataSource;
    @Autowired
    UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) 
                                                                throws Exception {
        auth.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ApplicationContext applicationContext = 
                              http.getSharedObject(ApplicationContext.class);
        http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
                .withObjectPostProcessor(new 
                           ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O 
                                                            postProcess(O object) {
           object.setSecurityMetadataSource(customSecurityMetadataSource);
                        return object;
                    }
                });
        http.formLogin()
                .and()
                .csrf().disable();
    }
}

关于用户的配置无需多说,我们重点来看 configure(HttpSecurity) 方法。

由于访问路径规则和所需要的权限之间的映射关系已经保存在数据库中,所以我们就没有必要在 Java 代码中配置映射关系了,同时这里的权限对比也不会用到权限表达式,所以我们通过 UrlAuthorizationConfigurer 来进行配置。

在配置的过程中,通过 withObjectPostProcessor 方法调用 ObjectPostProcessor 对象后置处理器,在对象后置处理器中,将 FilterSecurityInterceptor 中的 SecurityMetadataSource 对象替换为我们自定义的 customSecurityMetadataSource 对象即可。

2. 测试

接下来创建 HelloController,代码如下:

@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";
    }
}

最后启动项目进行测试。

首先使用 admin/123 进行登录,该用户具备 ROLE_ADMIN 角色,ROLE_ADMIN 可以访问 /admin/hello、/user/hello 以及 /guest/hello 三个接口。

接下来使用 user/123 进行登录,该用户具备 ROLE_USER 角色,ROLE_USER 可以访问 /user/hello 以及 /guest/hello 两个接口。

最后使用 javaboy/123 进行登录,该用户具备 ROLE_GUEST 角色,ROLE_GUEST 可以访问 /guest/hello 接口。

由于 /hello 接口不包含在 URL-权限 映射关系中,所以任何用户都可以访问 /hello 接口,包括匿名用户。如果希望所有的 URL 地址都必须在数据库中配置 URL-权限 映射关系后才能访问,那么可以通过如下配置实现:

http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
        .withObjectPostProcessor(new  
                           ObjectPostProcessor<FilterSecurityInterceptor>() {
            @Override
            public <O extends FilterSecurityInterceptor> O 
                                                           postProcess(O object) {   
           object.setSecurityMetadataSource(customSecurityMetadataSource);
                object.setRejectPublicInvocations(true);
                return object;
            }
        });

通过设置 FilterSecurityInterceptor 中的 rejectPublicInvocations 属性为 true,就可以关闭URL的公开访问,所有 URL 必须具备对应的权限才能访问。

菜单-角色权限的操作 在上一篇文章中讲解了菜单,角色,用户等管理信息的展示以及基本的查询等功能, 此片文章主要讲解菜单-角色权限管理的实现 首先在讲解菜单-角色权限之前,我们的创建一张表记录菜单和角色之间管理关系的表 menu_role,该表记录了角色id和菜单id外加操作时间和操作人信息等, 先看看界面原型: 在主界面上可以增加一列用来直观的展示角色-菜单的对应关系,该关系时通过他们的menu_role... 阅读详情

相关推荐

Spring Security 实现动态权限菜单方案(附源码)

在实际开发中,开发任何一套系统,基本都少不了权限管理这一块。这些足以说明权限管理的重要性。其实SpringSecurity去年就学了,一直没有时间整理,用了一年多时间了,给我的印象一直都挺好,实用,安全性高(Security可以对密码进行加密)。而且这一块在实际开发中也的确很重要,所以这里整理了一套基于SpringSecurity的权限管理。如果对于SpringSecurity还不了解的话可以先了解一下SpringSecurity安全控件的学习,页面采用的是Bootstrap写的(页面就简单的写了一下,可以

weixin_57907028的博客 533

Spring Security 动态url权限控制

记录一下。。。

weixin_44809110的博客 2056

爆破专栏丨Spring Security系列教程之Spring Security的四种权限控制方式

原创:一一哥 前言: 在前面的章节中,一一哥 已经给大家介绍了Spring Security的很多功能,在这些众多功能中,我们知道其核心功能其实就是认证+授权。 在前面我们分别基于内存模型、基于默认的数据库模型、基于自定义数据库模型实现了认证和授权功能,但是不管哪种方式,我们对某个接口的拦截限制,都是通过编写一个SecurityConfig配置类,在该类的configure (Http Security http)方法中,通过http. authorize Requests ( ). antMatchers

xiaoxijinger的博客 1361

Spring Security实现接口基于路径的动态权限控制

在荔枝之前的一篇博客中,已经对Spring Security的鉴证授权的相关知识进行了梳理,弄清楚了为什么使用JWT以及用户登录后台执行的相关的鉴证授权的逻辑。在下面的文章中荔枝主要是补充梳理一下在真实的项目中如何使用Spring Security来实现接口动态权限控制的功能。

荔枝当大佬的博客 3160

Spring Boot+Spring Security:基于URL动态权限n种方案 - 第13篇

需求缘起 之前的版本的用户的权限需要使用@PreAuthorize硬编码,那么无法动态的进行权限的配置了,本节基于URL实现动态权限配置方案分析。 一、权限动态配置方案 1.1 扩展access()的SpEL表达式 我们先看下常见的Spring Security的安全表达式: 掌握了Spring Security的SpEL表达式,我们就能够不再局限于...

悟纤学院 2万+

Spring Boot+Spring Security:动态权限修改Session刷新权限信息-过滤器方案 - 第31篇

四、过滤器方案 过滤器的方案整体和拦截器差不多,就是把拦截器中的代码迁移到过滤器来就可以了。 原本这种方案是没有打算讲解的,但是在写Demo的时候,发现有些坑,所以在这里和大家分享一下。 4.1 编写Filter 根据之前讲过的,我们编写一个RoleCheckFilter继承GenericFilterBean: public class RoleCheckF...

悟纤学院 2万+

Spring Security 实现动态权限菜单方案

Spring Security 动态权限

m0_54013833的博客 3438

Spring Security如何使用URL地址进行权限控制

主要介绍了Spring Security如何使用URL地址进行权限控制,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

SpringBoot+Spring Security+Vue实现动态权限菜单方案(附源码)

1、前言在实际开发中,开发任何一套系统,基本都少不了权限管理这一块。这些足以说明权限管理的重要性。其实SpringSecurity去年就学了,一直没有时间整理,用了一年多时间了,给我的印象一直都挺好,实用,安全性高(Security可以对密码进行加密)。而且这一块在实际开发中也的确很重要,所以这里整理了一套基于SpringSecurity的权限管理。案例代码下面有下载链接...

2113

从‘最速控制’到代码:深入拆解韩京清ADRC中TD跟踪器的fhan函数(含Matlab/Simulink实例)

本文深入解析韩京清ADRC中的fhan函数,从理论到代码实现,详细拆解其在Matlab/Simulink中的应用。通过分析最速控制问题和非线性TD的连续形式,揭示fhan函数在信号处理和噪声抑制中的独特优势,并提供工程实践中的参数整定和优化技巧。

weixin_42659194的博客 247

实战指南:在摩尔线程GPU上部署AI模型的完整流程

本文提供了一份详尽的实战指南,手把手教你如何在摩尔线程GPU上成功部署AI模型。内容涵盖从驱动安装、Docker环境配置,到核心的代码适配与模型运行全流程,并分享了性能调优技巧与常见避坑方法,旨在帮助开发者高效利用国产硬件进行AI应用开发。

weixin_29271263的博客 541

spring boot 3.x版本中集成spring security 6.x版本进行实现动态权限控制解决方案

框架会自动使用的方法进行用户加载,在加载用户以后,会在过滤器中的方法中,进行前端输入的用户信息和加载的用户信息进行信息对比。/*** 这里为了演示方便,模拟从数据库查询,直接设置一下权限");return new User("jack" , // 用户名称 new BCryptPasswordEncoder() . encode("123456") , //密码 list //权限列表);} }/**

Coder-文小白的博客 2668

APM32F103 配置CAN

摘要: 本文介绍了基于APM32F103RCT6的CAN通信实现,包含GPIO初始化、CAN配置、数据收发功能。通过配置CAN1的GPIO引脚(PB8/PB9)和波特率(500Kbps),实现标准帧格式的数据传输。发送函数支持8字节数据包,接收函数通过FIFO0处理消息,并根据接收ID触发响应发送(如ID 0x1回复0x2)。代码还包含错误状态检测(如总线关闭处理)和过滤器配置(32位掩码模式)。整体实现了基础的CAN通信与错误管理功能。

weixin_43676660的博客 863

史上最简单的Spring Security教程(十四):动态权限(自定义UserDetailsService)

在前面的示例中,我们在自定义的UserDetailsService中,固化了用户所拥有的的权限,即User。其实,这和最早我们在配置文件配置用户的用户名、密码、权限列表并无二致,这种固化的方法尤其独到之处,但却并不适合所有业务场景。 本例,我们来说一下如何实现动态权限。 首先,定义角色表、角色用户关联表。 create table SYS_ROLE ( ID varchar(32) not null comment '主键', NAME...

银河架构师的博客 3912

AI影像组学在结直肠癌诊疗中的应用:从特征提取到临床决策

影像组学通过高通量计算从医学影像中提取定量特征,揭示肿瘤内部的微观生物学行为,这些特征反映了纹理异质性、形状不规则度等影像表型。其核心原理在于将图像数据转化为可分析的数字特征,结合机器学习算法进行模式识别,从而实现对疾病状态的精准预测。这一技术的价值在于能够无创、定量地评估肿瘤特性,为精准医疗提供关键支持。在临床实践中,AI驱动的影像组学模型广泛应用于结直肠癌的诊疗全链条,包括肿瘤良恶性鉴别、基因突变预测、治疗反应评估和预后分析等场景。通过整合影像组学特征与临床数据,可以构建预测模型,辅助医生进行更准确的诊

weixin_28295799的博客 409

3D打印切片软件Cura及CuraEngine原理分析

引言         年初开始进入3D打印行业,受命以Cura为基础,研发一款自主的3D打印切片软件。         自主研发要取其长处,补其不足,首先自然是要搞清楚Cura到底做了什么,读Cura的代码是必需的。我一向都觉得比起自己写代码来,读别人的代码是一个漫又而痛苦的过程,读者的思想与写者总有偏差,往往又无法验证自己的猜想是否正确,只叹人脑不是电脑,无法把眼前的代码从头到尾执行一遍...

AiDiShengDeXiaoWu的博客 4421

Spring Cloud实战(六):Spring Cloud Gateway动态权限访问控制

一、需求 在2018年写的基于OAUTH2.0统一认证授权的微服务基础架构只是基于OAUTH认证授权的入门级应用。本文基于实战目的,实现权限的动态控制。 现有如下需求: 基于用户-角色-权限控制 权限粒度控制到具体的请求URL 当用户的角色或者权限变动后,已获授权的用户需要重新登录授权 本文围绕上面三个基本需求进行实现。 二、工程说明 设计的框架已经中间件有: Nacos 1.3 Spring Cloud Hoxton.SR8 JWT nimbus-jose-jwt Spring Cloud Gat

仰望星空 脚踏实地 1万+

SI案例分享--FPC(柔性电路板)信号仿真

设计高性能的柔性PCB时,由于采用了网格地的参考面构造,因此,需要按照特定的结构进行布局,才可以保证所需目标阻抗的稳定性,该结构的配置过程称为“Hatch Configuration”,合理的参数配置将有助于得到满意的设计效果。

一只豌豆象的博客 2829
上一篇: 从响应式编程到 Combine 实践
下一篇: 一次 MySQL 误操作导致的事故,「高可用」都顶不住了
JavaShark
博客等级 码龄4年 184粉丝 438原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值