package com.ruoyi.framework.interceptor;
import com.ruoyi.common.config.RuoYiConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
/**
* 集群版对接中台时的写操作兜底拦截。
*
*
目的:前端隐藏按钮只能减少误触,不能阻止绕过页面直接调用接口。
* 影响范围:访客后台非本地管理模块的新增、编辑、删除、状态变更、注销、导入等写接口。
*/
@Component
public class MiddlePlatformReadonlyInterceptor implements HandlerInterceptor {
private static final String MESSAGE = "集群版对接中台时,当前模块仅支持查询和导出";
private static final String RESET_PASSWORD_MESSAGE = "集群版对接中台时,不支持重置密码";
/** 受中台只读规则约束的访客后台接口前缀。 */
private static final List CONTROLLED_PREFIXES = Arrays.asList(
"/system/visit/area",
"/system/dept",
"/system/employee",
"/system/visitor/record",
"/system/visitor/machine",
"/system/door/device",
"/system/version/update",
"/system/protocol/config",
"/system/platform/admin",
"/system/role/manage"
);
/** 本地仍可维护的白名单模块:账号管理、系统配置、首页、操作日志。 */
private static final List LOCAL_MODULE_PREFIXES = Arrays.asList(
"/system/platform/admin",
"/system/role/manage",
"/system/visitor/config",
"/system/dashboard",
"/system/operation/log"
);
/** 非白名单模块在中台模式下只允许这些查询、导出和模板下载接口通过。 */
private static final List READONLY_SUFFIXES = Arrays.asList(
"/page",
"/detail",
"/tree",
"/overview",
"/roleOptions",
"/permissionTree",
"/export",
"/importTemplate"
);
@Autowired
private RuoYiConfig ruoyiConfig;
/**
* 目的:在请求进入 Controller 前判断中台只读规则,阻止非白名单模块写操作。
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!ruoyiConfig.isConnectMiddlePlatform()) {
return true;
}
String path = request.getRequestURI();
String contextPath = request.getContextPath();
if (contextPath != null && contextPath.length() > 0 && path.startsWith(contextPath)) {
path = path.substring(contextPath.length());
}
if (!isControlledPath(path)) {
return true;
}
String method = request.getMethod();
// 放行预检请求,避免 CORS OPTIONS 被业务只读规则误拦截。
if (isPreflight(method)) {
return true;
}
// 账号管理属于本地白名单,但业务要求中台模式下不允许展示或调用重置密码。
if (path.startsWith("/system/platform/admin/resetPassword")) {
return reject(response, RESET_PASSWORD_MESSAGE);
}
// GET/HEAD 和明确的查询导出接口为只读请求;白名单模块按本地管理逻辑继续放行。
if (isLocalModule(path) || isReadonlyEndpoint(path) || isReadMethod(method)) {
return true;
}
return reject(response);
}
/**
* 目的:判断当前请求是否属于访客后台需要受控的接口范围。
*/
private boolean isControlledPath(String path) {
for (String prefix : CONTROLLED_PREFIXES) {
if (path.startsWith(prefix)) {
return true;
}
}
return false;
}
/**
* 目的:判断当前请求是否属于中台模式仍允许本地维护的白名单模块。
*/
private boolean isLocalModule(String path) {
for (String prefix : LOCAL_MODULE_PREFIXES) {
if (path.startsWith(prefix)) {
return true;
}
}
return false;
}
/**
* 目的:判断 POST 查询、导出和模板下载这类只读接口是否可放行。
*/
private boolean isReadonlyEndpoint(String path) {
for (String suffix : READONLY_SUFFIXES) {
if (path.endsWith(suffix)) {
return true;
}
}
return false;
}
/**
* 目的:识别 HTTP 层面的只读方法,支持原生 GET 查询接口。
*/
private boolean isReadMethod(String method) {
return "GET".equalsIgnoreCase(method) || "HEAD".equalsIgnoreCase(method);
}
/**
* 目的:识别 CORS 预检请求,避免 OPTIONS 被业务规则误拦截。
*/
private boolean isPreflight(String method) {
return "OPTIONS".equalsIgnoreCase(method);
}
/**
* 目的:使用默认只读提示拒绝当前写操作请求。
*/
private boolean reject(HttpServletResponse response) throws Exception {
return reject(response, MESSAGE);
}
/**
* 目的:按指定提示返回统一 Ajax JSON 错误。
*/
private boolean reject(HttpServletResponse response, String message) throws Exception {
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":500,\"msg\":\"" + message + "\"}");
return false;
}
}