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

ApiAuthorizationManager 类描述

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

核心职责

  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();
    }
}
相关推荐
金銀銅鐵5 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路5 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x6 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn7 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈8 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780519 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123459 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒9 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记9 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598789 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端