SpringSecuirty自定义接口或者资源权限

ApiAuthorizationManager 类描述

ApiAuthorizationManager 是基于 Spring Security 6 实现的动态接口权限授权管理器组件 ,实现 AuthorizationManager<RequestAuthorizationContext> 接口,同时实现 InitializingBeanDisposableBean 完成权限规则的初始化与资源销毁,用于系统动态数据库配置接口权限,无需硬编码配置安全规则。

核心职责

  1. 动态加载权限规则 :容器启动后通过异步线程,从数据库查询状态为启用的权限表Permission数据,解析接口路径、请求方式、权限编码,构建 PathPatternRequestMatcher 请求匹配器,存入线程安全的 ConcurrentHashMap 缓存。key 为路径 + 请求方式匹配器,value 为权限编码。
  2. 请求鉴权逻辑 :接收每一个 HTTP 请求,遍历权限缓存匹配当前请求;匹配成功则校验当前登录用户是否拥有对应权限编码;无匹配权限规则的接口,默认要求用户必须登录认证才可访问。
  3. 生命周期管理:Bean 初始化完成异步加载权限;Bean 销毁时清空权限缓存集合释放资源。

关键细节

  • 使用applicationTaskExecutor应用线程池异步加载权限,避免阻塞 Spring 容器启动流程;
  • 支持区分 HTTP 请求方法匹配接口,未配置 httpMethod 则匹配任意请求方式;
  • 鉴权委托给AllAuthoritiesAuthorizationManager做权限编码校验;无匹配路由走AuthenticatedAuthorizationManager仅校验登录状态;
  • 捕获权限加载异常打印错误日志,防止初始化异常导致服务不可用;
  • 缓存使用ConcurrentHashMap保障多线程请求下并发安全。

业务场景

适用于后台管理系统,权限在数据库维护,管理员可在页面配置哪些接口需要什么权限码,无需修改代码重启服务(⚠️当前版本仅启动加载,缺少运行时刷新权限缓存逻辑,修改数据库权限后需要重启服务才生效)。

潜在小问题备注(附带)

  1. pathPatternMap遍历匹配是全量循环,权限数据量大时会存在性能损耗;
  2. 只在 Bean 初始化加载权限,没有对外提供刷新缓存的方法,权限变更无法热更新;
  3. for 循环匹配到第一条命中路由直接 return,多个 pattern 匹配同一个 url 时,存在匹配顺序优先级问题。

代码实现

java 复制代码
package com.kongjs.smo.common.security.authorization;

import com.kongjs.smo.common.mybatis.flex.enums.EnableEnum;
import com.kongjs.smo.system.entity.Permission;
import com.kongjs.smo.system.service.PermissionService;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.task.TaskExecutor;
import org.springframework.http.HttpMethod;
import org.springframework.security.authorization.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

@Slf4j
@Component
@SuppressWarnings("all")
public final class ApiAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext>, InitializingBean, DisposableBean {

    private static final AuthorizationDecision DENY = new AuthorizationDecision(false);
    private static final AuthorizationDecision ALLOW = new AuthorizationDecision(true);

    private final Map<RequestMatcher, String> pathPatternMap = new ConcurrentHashMap<>();

    @Resource(name = "applicationTaskExecutor")
    private TaskExecutor applicationTaskExecutor;
    @Resource
    private PermissionService permissionService;

    @Override
    public AuthorizationResult authorize(Supplier<? extends Authentication> supplier, RequestAuthorizationContext context) {
        HttpServletRequest request = context.getRequest();
        Map<String, String> variables = context.getVariables();
        if (!CollectionUtils.isEmpty(pathPatternMap)) {
            for (Map.Entry<RequestMatcher, String> entry : pathPatternMap.entrySet()) {
                if (entry.getKey().matches(request)) {
                    String permission = entry.getValue();
                    return AllAuthoritiesAuthorizationManager.hasAllAuthorities(permission).authorize(supplier, request);
                }
            }
        }
        return AuthenticatedAuthorizationManager.fullyAuthenticated().authorize(supplier, variables);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        applicationTaskExecutor.execute(() -> {
            try {
                List<Permission> permissions = permissionService.list(QueryWrapper.create().eq(Permission::getStatus, EnableEnum.ENABLE.getCode()));
                for (Permission permission : permissions) {
                    String resourcePath = permission.getResourcePath();
                    String permissionCode = permission.getCode();
                    if (!StringUtils.hasText(resourcePath)) {
                        continue;
                    }
                    PathPatternRequestMatcher pathPattern;
                    if (StringUtils.hasText(permission.getHttpMethod())) {
                        HttpMethod httpMethod = HttpMethod.valueOf(permission.getHttpMethod());
                        pathPattern = PathPatternRequestMatcher.pathPattern(httpMethod, resourcePath);
                    } else {
                        pathPattern = PathPatternRequestMatcher.pathPattern(resourcePath);
                    }
                    this.pathPatternMap.put(pathPattern, permissionCode);
                }
            } catch (Exception e) {
                log.error("初始化权限拦截异常: {}", e.getMessage());
            }
        });
    }

    @Override
    public void destroy() throws Exception {
        this.pathPatternMap.clear();
    }
}
相关推荐
Flynt8 小时前
把公司项目迁到 Spring Boot 4.0:编译通过只是开始
java·spring boot·后端
IT_陈寒8 小时前
Redis误用keys命令把生产环境搞崩了,血的教训
前端·人工智能·后端
她的男孩9 小时前
多租户和数据权限怎么共存?扒完拦截器注册链路,我找到 4 个隐蔽的坑
java·后端·架构
用户8356290780519 小时前
使用 Python 设置 Excel 页眉和页脚
后端·python
步行cgn9 小时前
IoC 控制反转:从概念到 Spring 的实现
后端
爱勇宝9 小时前
没有 Fn 键关触控板?我做了一个双击即用的 Windows 小工具
前端·后端·程序员
沙湖遇雨10 小时前
8.ByteBuf 的数据模型、生命周期和内存分配
后端
by组态10 小时前
Ricon组态系统API参考手册
前端·后端·物联网