本文介绍一个基于 Spring Boot 2.7.18 和 Spring Security 实现的权限系统,支持接口级权限控制,支持权限点的动态配置与加载。
技术栈
- Spring Boot 2.7.18
- Spring Security
- MyBatis Plus(用于持久化)
- MySQL
核心表结构设计
权限点表auth_permission_point
用于定义所有权限点(如 , ):
| 字段名 | 类型 | 说明 |
|---|
| 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>
复制代码,对每个请求:
- 从拿到该接口需要的权限点
- 从
- 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动态权限内容请搜索晓枫资讯以前的文章或继续浏览下面的相关文章希望大家以后多多支持晓枫资讯! 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |