From 07e803b4400d666d71a2c5a4be2a905c973bac2d Mon Sep 17 00:00:00 2001 From: chenxing Date: Mon, 7 Sep 2026 18:17:02 +0800 Subject: [PATCH] =?UTF-8?q?env-config=E4=BF=AE=E8=AE=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 7 ++ scripts/deploy-via-docker.js | 18 ++-- .../interceptor/ManageAccessInterceptor.java | 10 +- .../config/EnvConfigPublicConfig.java | 51 ---------- .../controller/api/EnvConfigController.java | 35 ------- .../service/impl/EnvConfigService.java | 92 ------------------- 6 files changed, 23 insertions(+), 190 deletions(-) delete mode 100644 src/main/java/com/yunhai/userbase/config/EnvConfigPublicConfig.java delete mode 100644 src/main/java/com/yunhai/userbase/controller/api/EnvConfigController.java delete mode 100644 src/main/java/com/yunhai/userbase/service/impl/EnvConfigService.java diff --git a/pom.xml b/pom.xml index 4935f10..e1a700f 100644 --- a/pom.xml +++ b/pom.xml @@ -208,6 +208,13 @@ com.yunhai.platform yh-api-std + + + com.yunhai.platform + yh-env-config-spring-boot-starter + diff --git a/scripts/deploy-via-docker.js b/scripts/deploy-via-docker.js index af308de..3538dee 100644 --- a/scripts/deploy-via-docker.js +++ b/scripts/deploy-via-docker.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -// Deploy front/dist/prod to host /data/www/userbase/front using the Docker daemon +// Deploy front/dist to host /data/www/userbase/front using the Docker daemon // that act_runner already mounts into the job container (/var/run/docker.sock). // // Why this script exists: @@ -153,8 +153,8 @@ async function flushDebug() { function resolveDistPath() { const candidates = [ - path.join(process.cwd(), 'front', 'dist', 'prod'), - '/workspace/admin/userbase/front/dist/prod', + path.join(process.cwd(), 'front', 'dist'), + '/workspace/admin/userbase/front/dist', ]; for (const c of candidates) { if (fs.existsSync(c)) return c; @@ -201,13 +201,13 @@ async function main() { } catch (e) { dbg('ls err: ' + e.message); } - throw new Error('built dist/prod not found under any candidate path'); + throw new Error('built dist not found under any candidate path'); } dbg('dist dir=' + distDir); dbg('dist listing: ' + fs.readdirSync(distDir).join(' ')); // 3) tar of the built assets, fetched from THIS container's own filesystem. - // We fetch WITHOUT a trailing slash so the tar contains a top-level "prod/" + // We fetch WITHOUT a trailing slash so the tar contains a top-level "dist/" // folder; we then mv it into place (deterministic, no reliance on the // trailing-slash "contents only" quirk which this moby build ignores). const tar = await getArchive(jobId, distDir); @@ -226,10 +226,10 @@ async function main() { throw new Error('put archive failed: ' + put.status + ' ' + put.body); } - // 5) move staging/prod -> HOST_STATIC/front (deterministic top-level landing). + // 5) move staging/dist -> HOST_STATIC/front (deterministic top-level landing). const mv = await runExec( HELPER_ID, - `rm -rf ${HOST_STATIC}/front && mv ${STAGING}/prod ${HOST_STATIC}/front && rm -rf ${STAGING}` + `rm -rf ${HOST_STATIC}/front && mv ${STAGING}/dist ${HOST_STATIC}/front && rm -rf ${STAGING}` ); dbg('mv status=' + mv.status + ' out=' + mv.body.replace(/\n/g, ' | ')); @@ -237,12 +237,12 @@ async function main() { const verify = await runExec( HELPER_ID, `echo "count=$(find ${HOST_STATIC}/front -type f | wc -l)"; ` + - `echo "has_stray_prod=$(test -d ${HOST_STATIC}/front/prod && echo YES || echo NO)"; ` + + `echo "has_stray_dist=$(test -d ${HOST_STATIC}/front/dist && echo YES || echo NO)"; ` + `ls -la ${HOST_STATIC}/front | head -20` ); dbg('verify output: ' + verify.body.replace(/\n/g, ' | ')); - dbg('DEPLOY OK -> ' + HOST_STATIC + '/front (top-level, no prod/ nesting)'); + dbg('DEPLOY OK -> ' + HOST_STATIC + '/front (top-level, no dist/ nesting)'); } main() diff --git a/src/main/java/com/yunhai/userbase/common/interceptor/ManageAccessInterceptor.java b/src/main/java/com/yunhai/userbase/common/interceptor/ManageAccessInterceptor.java index 794d5ec..f4a706b 100644 --- a/src/main/java/com/yunhai/userbase/common/interceptor/ManageAccessInterceptor.java +++ b/src/main/java/com/yunhai/userbase/common/interceptor/ManageAccessInterceptor.java @@ -16,7 +16,9 @@ import java.util.List; /** * 管理端统一防线:NONE 级(普通用户/未登录)一律拦截,HTTP 200 + {code:403, msg:"暂无权限"}。
* 与前端 alova 约定一致(status!=200 会被当作网络错误,业务码走 errorHandle default 分支弹 msg)。
- * 白名单(仅登录即可访问):GetUserInfo / GetUserScope / GetSysDict / RedirectTo。
+ * 白名单:GetUserInfo / GetUserScope / GetSysDict / RedirectTo(仅登录即可访问); + * env-config(免登录,SPA 启动阶段在 CAS 登录前就要拉运行时配置,且内容为非敏感前端配置, + * 端点由平台 yh-env-config-spring-boot-starter 提供,旁路过滤器见 EnvConfigSsoBypassFilter)。
* 注册于 /{platform}/**(api/manage/mobiapi 三前缀均命中),排除 /oapi/**(服务间签名调用,行为不变)。
* SUPER/BUREAU/SCHOOL 之间的细粒度差异由各 Controller 内 guard 实现。 * @@ -28,13 +30,15 @@ public class ManageAccessInterceptor implements HandlerInterceptor { /** * 白名单(Ant 风格,* 匹配 platform 前缀): - * 登录后骨架所需的 4 个只读接口——用户信息、访问范围、系统字典、跳转代理。 + * 登录后骨架所需的 4 个只读接口——用户信息、访问范围、系统字典、跳转代理; + * env-config 免登录(端点与默认路径见平台 yh-env-config-spring-boot-starter)。 */ private static final List WHITELIST = Arrays.asList( "/*/base/GetUserInfo", "/*/base/GetUserScope", "/*/base/GetSysDict", - "/*/base/RedirectTo/*"); + "/*/base/RedirectTo/*", + "/*/env-config"); private static final AntPathMatcher MATCHER = new AntPathMatcher(); diff --git a/src/main/java/com/yunhai/userbase/config/EnvConfigPublicConfig.java b/src/main/java/com/yunhai/userbase/config/EnvConfigPublicConfig.java deleted file mode 100644 index 17efd4f..0000000 --- a/src/main/java/com/yunhai/userbase/config/EnvConfigPublicConfig.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.yunhai.userbase.config; - -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; - -/** - * {@code GET /api/env-config} 免登录放行(方案:前端动态配置方案-env-config接入落地.md §3.3)。 - *

- * 前端 SPA 启动阶段(用户尚未登录)就要拉运行时配置,而 yh-auth 的 CAS 过滤器 - * (HIGHEST+4..+9,url-pattern /api/*)会把未登录请求 302 到 CAS 登录页,导致配置拉取失败。 - * 本过滤器注册在所有 CAS 过滤器之前(HIGHEST+3),精确匹配该端点路径后直接 forward: - * forward 属 FORWARD 派发,只经过为 FORWARD 注册的过滤器(CAS 四件套均仅注册 REQUEST 派发), - * 请求直达 DispatcherServlet 的 {@link com.yunhai.userbase.controller.api.EnvConfigController}。 - * 其余 /api/** 请求完全不受影响,登录态拦截行为逐字节不变。 - */ -@Configuration -public class EnvConfigPublicConfig { - - /** 免登录端点的 context 相对路径(与 EnvConfigController 的 /{platform}/env-config 中 platform=api 对应) */ - public static final String PUBLIC_PATH = "/api/env-config"; - - @Bean - public FilterRegistrationBean envConfigSsoBypassFilter() { - FilterRegistrationBean bean = new FilterRegistrationBean<>(); - bean.setFilter(new EnvConfigSsoBypassFilter()); - // 精确 URL 匹配,只作用于这一个端点;须早于 CasSsoApiRedirectFilter(HIGHEST+4) - bean.addUrlPatterns(PUBLIC_PATH); - bean.setOrder(Ordered.HIGHEST_PRECEDENCE + 3); - return bean; - } - - static class EnvConfigSsoBypassFilter implements Filter { - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - // forward 同一 URI(query 参数随原请求保留);本过滤器仅注册 REQUEST 派发,不会被再次触发 - ((HttpServletRequest) request).getRequestDispatcher(PUBLIC_PATH).forward(request, response); - // 不调用 chain.doFilter:REQUEST 链上排在后面的 CAS 过滤器因此不会执行 - } - } -} diff --git a/src/main/java/com/yunhai/userbase/controller/api/EnvConfigController.java b/src/main/java/com/yunhai/userbase/controller/api/EnvConfigController.java deleted file mode 100644 index 1a926f6..0000000 --- a/src/main/java/com/yunhai/userbase/controller/api/EnvConfigController.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.yunhai.userbase.controller.api; - -import com.yunhai.userbase.config.Config; -import com.yunhai.userbase.service.impl.EnvConfigService; -import io.swagger.v3.oas.annotations.Operation; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -/** - * 前端运行时配置端点(免登录)。 - *

- * 返回 projman 配置管理维护、双写 Nacos 的 env-config-{front}.json 原文(JSON), - * 前端 SPA 启动时请求 {@code GET /userbase/api/env-config?front=front},替代 config/index.js - * 与构建期 VITE_* 环境变量,实现接口地址等配置在线改、免发版生效。
- * {@code /api/*} 前缀是 Caddy 反代到后端的既有路由,免登录放行由 - * {@link com.yunhai.userbase.config.EnvConfigPublicConfig} 的旁路过滤器实现。 - */ -@RestController -@RequestMapping(value = Config.API_PATH + "/env-config") -public class EnvConfigController { - - @Autowired - private EnvConfigService envConfigService; - - /** 配置内容为非敏感信息(前端 JS 最终也会持有),按方案约定不做登录鉴权 */ - @Operation(summary = "前端运行时配置(projman 双写 Nacos,免登录)", operationId = "envConfig") - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) - public String get(@RequestParam(value = "front", defaultValue = "front") String front) { - return envConfigService.get(front); - } -} diff --git a/src/main/java/com/yunhai/userbase/service/impl/EnvConfigService.java b/src/main/java/com/yunhai/userbase/service/impl/EnvConfigService.java deleted file mode 100644 index 1422c75..0000000 --- a/src/main/java/com/yunhai/userbase/service/impl/EnvConfigService.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.yunhai.userbase.service.impl; - -import com.alibaba.cloud.nacos.NacosConfigManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 前端运行时配置读取(方案:Projman/前端动态配置方案-env-config接入落地.md)。 - *

- * projman 配置管理中的 env-config-{前端目标名}.json 会双写到 Nacos - * (dataId = front-{应用名}-{目标名},group = FRONT_GROUP,type = json), - * 本服务按前端目标名读取并原样返回 JSON 串,经 {@code GET /api/env-config?front=xxx} 提供给前端 SPA, - * 替代构建期写死的 VITE_* 环境变量与 config/index.js 静态文件。 - *

- * 容错口径:读不到(本地未开 Nacos / dataId 不存在 / 超时)一律返回 {@code "{}"}, - * 前端拿空对象后走 import.meta.env 构建期兜底,不影响启动。 - */ -@Service -public class EnvConfigService { - - private static final Logger log = LoggerFactory.getLogger(EnvConfigService.class); - - /** 前端目标名白名单:projman 前端目标创建时即限制为简单标识符,此处同口径校验,防 dataId 拼接注入 */ - private static final String FRONT_NAME_PATTERN = "^[A-Za-z0-9_-]{1,64}$"; - - /** Nacos dataId 前缀:front-{应用名}-,与 projman AppConfigFileService 双写规则一致 */ - @Value("${yh.env-config.data-id-prefix:front-userbase-}") - private String dataIdPrefix; - - /** Nacos group,与 projman 双写侧 FRONT_GROUP 常量一致 */ - @Value("${yh.env-config.group:FRONT_GROUP}") - private String group; - - /** 结果缓存 TTL:前端 SPA 每次打开页面都会拉一次,缓存削峰避免轮询打穿 Nacos */ - private static final long CACHE_TTL_MS = 30_000L; - - private final Map cache = new ConcurrentHashMap<>(); - - private static class CacheEntry { - final String content; - final long ts; - CacheEntry(String content, long ts) { - this.content = content; - this.ts = ts; - } - } - - /** 本地 profile 未启用 Nacos 时为 null(NACOS_CONFIG_ENABLED=false 时不注册该 bean) */ - @Autowired(required = false) - private NacosConfigManager nacosConfigManager; - - /** 读取前端目标配置 JSON;任何失败返回 "{}"(前端走构建期兜底) */ - public String get(String front) { - if (front == null || !front.trim().matches(FRONT_NAME_PATTERN)) { - return "{}"; - } - String key = front.trim(); - CacheEntry cached = cache.get(key); - long now = System.currentTimeMillis(); - if (cached != null && now - cached.ts < CACHE_TTL_MS) { - return cached.content; - } - String content = readFromNacos(key); - cache.put(key, new CacheEntry(content, now)); - return content; - } - - private String readFromNacos(String front) { - if (nacosConfigManager == null) { - // 本地开发未接 Nacos:无运行时配置,前端走 import.meta.env 兜底 - return "{}"; - } - try { - String content = nacosConfigManager.getConfigService() - .getConfig(dataIdPrefix + front, group, 3000); - if (content == null || content.trim().isEmpty()) { - return "{}"; - } - return content.trim(); - } catch (Exception e) { - // Nacos 不可达时 getConfig 自身有本地快照兜底;仍失败则返回空对象,不抛错不影响前端启动 - log.warn("[env-config] 读取 Nacos 配置失败 dataId={} group={}: {}", dataIdPrefix + front, group, e.getMessage()); - return "{}"; - } - } -}