发布时间:2022-12-27 10:00
最近在做 TienChin 项目,用的是 RuoYi-Vue 脚手架,在这个脚手架中,访问某个接口需要什么权限,这个是在代码中硬编码的,具体怎么实现的,松哥下篇文章来和大家分析,有的小伙伴可能希望能让这个东西像 vhr 一样,可以在数据库中动态配置,因此这篇文章和小伙伴们简单介绍下 Spring Security 中的动态权限方案,以便于小伙伴们更好的理解 TienChin 项目中的权限方案。
通过代码来配置 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 文件内容如下:
org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter 2.1.3 mysql mysql-connector-java runtime
项目创建完成后,接下来在 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 Listroles; //省略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 Listroles; @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 { ListgetUserRoleByUid(Integer uid); User loadUserByUsername(String username); }
UserMapper.xml:
再来看 MenuService,该类只需要提供一个方法,就是查询出所有的 Menu 数据,代码如下:
@Service public class MenuService { @Autowired MenuMapper menuMapper; public List
MenuMapper:
@Mapper public interface MenuMapper { List
MenuMapper.xml:
需要注意,由于每一个 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 CollectiongetAttributes(Object object) throws IllegalArgumentException { String requestURI = ((FilterInvocation) object).getRequest().getRequestURI(); List
自定义 CustomSecurityMetadataSource 类并实现 FilterInvocationSecurityMetadataSource 接口,然后重写它里边的三个方法: