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();
    }
}
相关推荐
天涯明月19932 小时前
SGLang深度研究报告
人工智能·后端·推理·sglang
程序员爱钓鱼2 小时前
Rust where详解:让泛型与Trait约束更加清晰
前端·后端·rust
程序员爱钓鱼4 小时前
Go 编程实战:函数 Function——参数、返回值与代码复用
后端·面试·go
To_OC10 小时前
跑了3个Docker容器后,我终于搞懂镜像和容器到底啥关系
后端·docker·容器
东风破_10 小时前
Docker 基础:为什么需要容器?
前端·后端·nginx
东风破_10 小时前
Docker 实战:使用 Nginx 作为容器入口代理 Node 服务
前端·后端·nginx
Lyyaoo.11 小时前
Spring Boot Validation 声明式参数校验
spring boot·后端·mysql
剩下了什么11 小时前
float32 与 float64 精度陷阱:如何在 Go 中避免错误使用
开发语言·后端·golang
Rain的Java大神之路11 小时前
介绍一下分布式事务
java·分布式·后端·spring·spring cloud·架构·springcloud