This commit is contained in:
@@ -208,6 +208,13 @@
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-api-std</artifactId>
|
||||
</dependency>
|
||||
<!-- 前端运行时配置(projman 双写 Nacos 免登录端点 GET /api/env-config?target=xxx)。
|
||||
原自研 EnvConfigController/EnvConfigService/EnvConfigPublicConfig 三件套已上提平台,
|
||||
本工程仅保留 ManageAccessInterceptor 白名单放行。 -->
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-env-config-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- CAS 客户端已由 yh-auth-spring-boot-starter 传递引入(3.5.1):CAS 过滤器注册与
|
||||
SPA 登录跳转统一在框架内,本工程不再直接依赖,避免重复定义过滤器 bean。 -->
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -16,7 +16,9 @@ import java.util.List;
|
||||
/**
|
||||
* 管理端统一防线:NONE 级(普通用户/未登录)一律拦截,HTTP 200 + {code:403, msg:"暂无权限"}。<br>
|
||||
* 与前端 alova 约定一致(status!=200 会被当作网络错误,业务码走 errorHandle default 分支弹 msg)。<br>
|
||||
* 白名单(仅登录即可访问):GetUserInfo / GetUserScope / GetSysDict / RedirectTo。<br>
|
||||
* 白名单:GetUserInfo / GetUserScope / GetSysDict / RedirectTo(仅登录即可访问);
|
||||
* env-config(免登录,SPA 启动阶段在 CAS 登录前就要拉运行时配置,且内容为非敏感前端配置,
|
||||
* 端点由平台 yh-env-config-spring-boot-starter 提供,旁路过滤器见 EnvConfigSsoBypassFilter)。<br>
|
||||
* 注册于 /{platform}/**(api/manage/mobiapi 三前缀均命中),排除 /oapi/**(服务间签名调用,行为不变)。<br>
|
||||
* 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<String> WHITELIST = Arrays.asList(
|
||||
"/*/base/GetUserInfo",
|
||||
"/*/base/GetUserScope",
|
||||
"/*/base/GetSysDict",
|
||||
"/*/base/RedirectTo/*");
|
||||
"/*/base/RedirectTo/*",
|
||||
"/*/env-config");
|
||||
|
||||
private static final AntPathMatcher MATCHER = new AntPathMatcher();
|
||||
|
||||
|
||||
@@ -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)。
|
||||
* <p>
|
||||
* 前端 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> envConfigSsoBypassFilter() {
|
||||
FilterRegistrationBean<EnvConfigSsoBypassFilter> 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 过滤器因此不会执行
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 前端运行时配置端点(免登录)。
|
||||
* <p>
|
||||
* 返回 projman 配置管理维护、双写 Nacos 的 env-config-{front}.json 原文(JSON),
|
||||
* 前端 SPA 启动时请求 {@code GET /userbase/api/env-config?front=front},替代 config/index.js
|
||||
* 与构建期 VITE_* 环境变量,实现接口地址等配置在线改、免发版生效。<br>
|
||||
* {@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);
|
||||
}
|
||||
}
|
||||
@@ -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)。
|
||||
* <p>
|
||||
* 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 静态文件。
|
||||
* <p>
|
||||
* 容错口径:读不到(本地未开 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<String, CacheEntry> 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 "{}";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user