fix(admin): 补齐 /api/admin 方法级鉴权并加兜底过滤器
ShopKey/SkipPriceAsin/QueryAsin/ShopManage/ProductCategory 五个 controller 此前无任何 require* 鉴权,SecurityConfig 全局 permitAll 且 nginx 无 ACL, 公网 18080 上可匿名读写店铺密钥等敏感数据。统一补 requireAdminOrInternal; 其中 ShopManage/SkipPrice/QueryAsin 原先信任调用方传入 operatorId/superAdmin, 改为从已认证 principal 推导,消除传 superAdmin=true 越权扩大数据范围的问题。 新增 AdminApiGuardFilter 对 /api/admin/** 兜底要求有效身份(JWT 或内部令牌), 防止后续新增端点遗漏鉴权再裸奔;支持 admin-guard-enabled/exempt-prefixes 配置。
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
package com.nanri.aiimage.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nanri.aiimage.common.api.ApiResponse;
|
||||
import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* /api/admin/** 兜底鉴权过滤器。
|
||||
*
|
||||
* <p>SecurityConfig 全局 permitAll 且无路径级拦截,历史上有多个 /api/admin 控制器遗漏方法级
|
||||
* require*(公网 18080 直连可达即匿名裸奔)。本过滤器作为第二道网:凡进入 /api/admin 的请求
|
||||
* 必须具备"有效身份"(Java JWT,或可信内部令牌 X-Internal-Token + operatorId,复用
|
||||
* AdminAuthSupport),否则直接返回与全局异常一致的 401 响应体(HTTP 200 + {success:false,code})。
|
||||
*
|
||||
* <p>只保证"已认证",管理员/菜单级校验仍由各控制器 require* 负责,避免拦截普通登录用户本可
|
||||
* 访问的 /api/admin 接口(如权限菜单列表仅 requireUser)。
|
||||
*
|
||||
* <p>豁免项:OPTIONS 预检;以及 aiimage.security.admin-guard-exempt-prefixes 配置的前缀
|
||||
* (逗号分隔,用于内部自动化/设备回调等确需匿名可达的端点)。紧急回退:
|
||||
* aiimage.security.admin-guard-enabled=false 关闭本过滤器。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AdminApiGuardFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final String ADMIN_API_PREFIX = "/api/admin";
|
||||
|
||||
private final AdminAuthSupport adminAuthSupport;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Value("${aiimage.security.admin-guard-enabled:true}")
|
||||
private boolean enabled;
|
||||
|
||||
@Value("${aiimage.security.admin-guard-exempt-prefixes:}")
|
||||
private String exemptPrefixes;
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
if (!enabled) {
|
||||
return true;
|
||||
}
|
||||
String uri = request.getRequestURI();
|
||||
if (!(uri.equals(ADMIN_API_PREFIX) || uri.startsWith(ADMIN_API_PREFIX + "/"))) {
|
||||
return true;
|
||||
}
|
||||
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||
return true;
|
||||
}
|
||||
for (String prefix : exemptSet()) {
|
||||
if (uri.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
try {
|
||||
adminAuthSupport.requireUserOrInternal(request);
|
||||
} catch (BusinessException ex) {
|
||||
ApiResponse<Void> body = ex.getCode() == null
|
||||
? ApiResponse.fail(ex.getMessage())
|
||||
: ApiResponse.fail(ex.getCode(), ex.getMessage());
|
||||
log.warn("[admin-guard] {} {} rejected: {}", request.getMethod(), request.getRequestURI(), ex.getMessage());
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write(objectMapper.writeValueAsString(body));
|
||||
return;
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private Set<String> exemptSet() {
|
||||
if (exemptPrefixes == null || exemptPrefixes.isBlank()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return Arrays.stream(exemptPrefixes.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user