基于Spring Security的动态权限系统设计与实现
作者:用户9198372681846
本文介绍一个基于Spring Boot 2.7.18和SpringSecurity实现的权限系统,支持接口级权限控制,支持权限,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧
本文介绍一个基于 Spring Boot 2.7.18 和 Spring Security 实现的权限系统,支持接口级权限控制,支持权限点的动态配置与加载。
技术栈
- Spring Boot 2.7.18
- Spring Security
- MyBatis Plus(用于持久化)
- MySQL
核心表结构设计
权限点表auth_permission_point
用于定义所有权限点(如 user:create, user:update):
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| code | varchar | 权限点编码(唯一) |
| name | varchar | 权限点名称 |
| type | varchar | 类型(操作、页面、字段等) |
| resource | varchar | 资源模块标识 |
| action | varchar | 操作标识 |
| remark | varchar | 备注说明 |
角色表auth_role
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| role_code | varchar | 角色编码 |
| name | varchar | 角色名称 |
| is_builtin | boolean | 是否为系统内置角色 |
| enabled | boolean | 是否启用 |
用户角色关联表auth_user_role
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | varchar | 用户唯一 ID |
| role_code | varchar | 关联角色编码 |
角色权限点关联表auth_role_permission_point
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| role_code | varchar | 角色编码 |
| permission_code | varchar | 权限点编码 |
接口权限映射表auth_url_permission_point
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| url | varchar | 接口路径 |
| method | varchar | 请求方法(GET/POST/PUT/DELETE) |
| permission_code | varchar | 所需权限点编码 |
✅ 每个接口可以绑定多个权限点,满足任意一个即视为拥有权限。
权限系统运行机制
1. 动态加载权限点
实现自定义 FilterInvocationSecurityMetadataSource,在系统启动和权限点发生变更时,自动扫描 auth_url_permission_point 表,将 URL、METHOD -> 权限点集合 的映射加载至内存。
@Component
@RequiredArgsConstructor
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private final AntPathMatcher pathMatcher = new AntPathMatcher();
@Resource
private UrlPermissionMappingService urlPermissionMappingService;
// TODO: 后期可替换为 Redis 或数据库缓存
private static final Map<String, List<PermissionExpressionConfigAttribute>> URL_PERMISSION_MAP = new ConcurrentHashMap<>();
private volatile Map<String, List<PermissionExpressionConfigAttribute>> permissionMap = new ConcurrentHashMap<>();
static {
// 示例数据,正式请从数据库加载
URL_PERMISSION_MAP.put("/api/user/**", List.of(new PermissionExpressionConfigAttribute("user:query")));
URL_PERMISSION_MAP.put("/api/user/updatePassword", List.of(new PermissionExpressionConfigAttribute("user:updatePassword")));
}
@PostConstruct
public void init() {
// 启动时加载一次
reload();
}
public void reload() {
Map<String, List<PermissionExpressionConfigAttribute>> newMap = new HashMap<>();
for (UrlPermissionMapping mapping : urlPermissionMappingService.loadAllUrlPermissionMappings()) {
newMap.computeIfAbsent(mapping.getUrlPattern(), k -> new ArrayList<>())
.add(new PermissionExpressionConfigAttribute(mapping.getPermissionCode()));
}
this.permissionMap = newMap;
}
@Override
public Collection<ConfigAttribute> getAttributes(Object object) {
String requestPath = ((FilterInvocation) object).getRequest().getRequestURI();
// 先尝试精确匹配
List<PermissionExpressionConfigAttribute> exact = permissionMap.get(requestPath);
if (exact != null) {
return new HashSet<>(exact);
}
// 再尝试通配匹配
for (Map.Entry<String, List<PermissionExpressionConfigAttribute>> entry : permissionMap.entrySet()) {
if (pathMatcher.match(entry.getKey(), requestPath)) {
return new HashSet<>(entry.getValue());
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return URL_PERMISSION_MAP.values().stream()
.flatMap(List::stream)
.collect(Collectors.toSet());
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
2. 动态权限校验
实现 AccessDecisionVoter<FilterInvocation>,对每个请求:
- 从
SecurityMetadataSource拿到该接口需要的权限点 - 从
Authentication#getAuthorities()拿到用户权限点集合 - 判断是否命中
public class PermissionExpressionVoter implements AccessDecisionVoter<FilterInvocation> {
@Override
public int vote(Authentication authentication, FilterInvocation filterInvocation,
Collection<ConfigAttribute> attributes) {
Assert.notNull(authentication, "authentication must not be null");
Assert.notNull(filterInvocation, "filterInvocation must not be null");
Assert.notNull(attributes, "attributes must not be null");
Set<String> requiredExpressions = findConfigAttribute(attributes);
// 获取当前登录用户拥有的权限点表达式
Set<String> userPermissions = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
if (CollectionUtils.isEmpty(requiredExpressions)) {
// 如果没有定义表达式,弃权,交给下一个 voter
log.trace("Abstained since did not find a config attribute of instance WebExpressionConfigAttribute");
return ACCESS_ABSTAIN;
}
for (String required : requiredExpressions) {
if (userPermissions.contains(required)) {
return ACCESS_GRANTED;
}
}
log.warn("权限校验失败: 当前用户权限 = {}, 资源需要权限 = {}", userPermissions, requiredExpressions);
return ACCESS_DENIED;
}
private Set<String> findConfigAttribute(Collection<ConfigAttribute> attributes) {
// 取出当前资源对应的权限表达式
return attributes.stream()
.filter(attribute -> attribute instanceof PermissionExpressionConfigAttribute)
.map(ConfigAttribute::getAttribute)
.collect(Collectors.toSet());
}
@Override
public boolean supports(ConfigAttribute attribute) {
return attribute instanceof PermissionExpressionConfigAttribute;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
☑️ 未配置权限点的接口可设置默认放行,也可以走 fallback 权限点逻辑。
总结
该系统实现了:
- 权限点粒度统一、接口权限与角色权限解耦
- 接口权限点支持动态注册与配置
- 权限控制基于 Spring Security 标准扩展机制,具备良好扩展性
TODO(可选增强)
- 支持权限表达式解析(如
@hasAny('user:create', 'admin')) - 支持字段级、按钮级权限点
- 权限点变更自动刷新缓存
- 提供权限控制台(前端联动)
到此这篇关于基于Spring Security的动态权限系统设计与实现的文章就介绍到这了,更多相关SpringSecurity动态权限内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
您可能感兴趣的文章:
- 详解Spring Security 中的四种权限控制方式
- java中自定义Spring Security权限控制管理示例(实战篇)
- spring security动态配置url权限的2种实现方法
- SpringSecurity动态加载用户角色权限实现登录及鉴权功能
- Spring security实现登陆和权限角色控制
- 解决Spring Security的权限配置不生效问题
- SpringBoot整合Security实现权限控制框架(案例详解)
- Spring security实现权限管理示例
- SpringBoot2.0 整合 SpringSecurity 框架实现用户权限安全管理方法
- Spring Security动态权限的实现方法详解
