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/protocol/config", "/system/version/update", "/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; } }