ApiAuthorizationManager 类描述
ApiAuthorizationManager 是基于 Spring Security 6 实现的动态接口权限授权管理器组件 ,实现 AuthorizationManager<RequestAuthorizationContext> 接口,同时实现 InitializingBean、DisposableBean 完成权限规则的初始化与资源销毁,用于系统动态数据库配置接口权限,无需硬编码配置安全规则。
核心职责
- 动态加载权限规则 :容器启动后通过异步线程,从数据库查询状态为启用的权限表
Permission数据,解析接口路径、请求方式、权限编码,构建PathPatternRequestMatcher请求匹配器,存入线程安全的ConcurrentHashMap缓存。key 为路径 + 请求方式匹配器,value 为权限编码。 - 请求鉴权逻辑 :接收每一个 HTTP 请求,遍历权限缓存匹配当前请求;匹配成功则校验当前登录用户是否拥有对应权限编码;无匹配权限规则的接口,默认要求用户必须登录认证才可访问。
- 生命周期管理:Bean 初始化完成异步加载权限;Bean 销毁时清空权限缓存集合释放资源。
关键细节
- 使用
applicationTaskExecutor应用线程池异步加载权限,避免阻塞 Spring 容器启动流程; - 支持区分 HTTP 请求方法匹配接口,未配置 httpMethod 则匹配任意请求方式;
- 鉴权委托给
AllAuthoritiesAuthorizationManager做权限编码校验;无匹配路由走AuthenticatedAuthorizationManager仅校验登录状态; - 捕获权限加载异常打印错误日志,防止初始化异常导致服务不可用;
- 缓存使用
ConcurrentHashMap保障多线程请求下并发安全。
业务场景
适用于后台管理系统,权限在数据库维护,管理员可在页面配置哪些接口需要什么权限码,无需修改代码重启服务(⚠️当前版本仅启动加载,缺少运行时刷新权限缓存逻辑,修改数据库权限后需要重启服务才生效)。
潜在小问题备注(附带)
pathPatternMap遍历匹配是全量循环,权限数据量大时会存在性能损耗;- 只在 Bean 初始化加载权限,没有对外提供刷新缓存的方法,权限变更无法热更新;
- 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();
}
}