feat(msgcenter): 消息跳转收口(C1/C2)——出参增 showJump 判权(authorized-codes 每请求懒加载一次、UserBase 不可达放行、无权 jumpUrl 同置 null 双保险);jump-bases 手工基址表三处退役(字段/yml/兜底分支)+ jumpApp 命中严格二选一不回落发送方 appKey,注册表基址并入尾斜杠归一
This commit is contained in:
@@ -10,9 +10,11 @@ import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* UserMsg 消息中心签名代理(§3.5 / BP-R3-1):出站走 {@link YschOpenCall} 泛化签名直调
|
||||
@@ -21,8 +23,11 @@ import java.util.Map;
|
||||
* <ul>
|
||||
* <li>列表/角标:/oapi/msg/list、/oapi/msg/unreadCount、/oapi/msg/todo/list;</li>
|
||||
* <li>已读:/oapi/msg/read、/oapi/msg/readAll(按当前登录人代理,userId 服务端取);</li>
|
||||
* <li>jumpUrl 改写:绝对 URL 直通;相对路径按「注册表 by-code/by-key 取 pc_url(B2 主链路)
|
||||
* → jump-bases 手工表兜底」解析基址,归一化后拼 "#" + jumpPath(裸相对路径如 /task/123)。</li>
|
||||
* <li>jumpUrl 改写:绝对 URL 直通;相对路径按应用注册表取 pc_url——jumpApp(值域=app_code)
|
||||
* 主查,jumpApp 为空才按发送方 appKey 回落(严格二选一,pc_url 空就是空不猜测,
|
||||
* 手工基址表 jump-bases 已退役),归一化后拼 "#" + jumpPath;</li>
|
||||
* <li>showJump 判权(C1):jumpApp 有值时按 /oapi/app/authorized-codes 集合判定,
|
||||
* 无权 → showJump=false 且 jumpUrl 一并置 null;jumpApp 为空或 UserBase 不可达不判权放行。</li>
|
||||
* </ul>
|
||||
* 2026-09-20 事故修复:原实现用共享 IDHttp 出站,其 AppSignInterceptor 只剥<b>本应用</b>
|
||||
* base-path(/userbase),目标 URL 带 /usermsg ctx 时签名路径多一层前缀 → UserMsg 验签 401,
|
||||
@@ -77,12 +82,13 @@ public class MsgCenterProxyService {
|
||||
return openCall.post(TARGET_USERMSG, path, query, body, JsonNode.class);
|
||||
}
|
||||
|
||||
/** 消息分页列表(已过滤删除/过期):{total, list[]},元素含改写后的 jumpUrl */
|
||||
/** 消息分页列表(已过滤删除/过期):{total, list[]},元素含改写后的 jumpUrl 与 showJump 判权出参 */
|
||||
public Map<String, Object> list(Integer userId, Integer pageIndex, Integer pageSize, Boolean onlyUnread) {
|
||||
Map<String, Object> empty = emptyPage();
|
||||
if (!enabled()) {
|
||||
return empty;
|
||||
}
|
||||
PerRequestAuthz authz = new PerRequestAuthz(userId);
|
||||
try {
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
query.put("userId", String.valueOf(userId));
|
||||
@@ -93,7 +99,7 @@ public class MsgCenterProxyService {
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
if (data != null && data.path("content").isArray()) {
|
||||
for (JsonNode row : data.get("content")) {
|
||||
rows.add(rewrite(row));
|
||||
rows.add(rewrite(row, authz));
|
||||
}
|
||||
}
|
||||
Map<String, Object> out = new HashMap<>();
|
||||
@@ -228,8 +234,8 @@ public class MsgCenterProxyService {
|
||||
|
||||
// ===== 出参改写 =====
|
||||
|
||||
/** MsgListVo 逐字段透传 + jumpUrl 改写(jumpType=1 且查到基址时)+ 应用名补全 */
|
||||
private Map<String, Object> rewrite(JsonNode row) {
|
||||
/** MsgListVo 逐字段透传 + jumpUrl 改写(jumpType=1 且查到基址时)+ showJump 判权 + 应用名补全 */
|
||||
private Map<String, Object> rewrite(JsonNode row, PerRequestAuthz authz) {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("msgId", row.has("msgId") ? row.get("msgId").asLong() : null);
|
||||
out.put("title", text(row, "title"));
|
||||
@@ -245,26 +251,38 @@ public class MsgCenterProxyService {
|
||||
out.put("bizId", text(row, "bizId"));
|
||||
out.put("sendTime", text(row, "sendTime"));
|
||||
out.put("isRead", row.has("isRead") && row.get("isRead").isNumber() ? row.get("isRead").asInt() : 0);
|
||||
out.put("jumpUrl", jumpUrl(row));
|
||||
String jumpUrl = jumpUrl(row);
|
||||
// showJump(C1):jumpApp 有值才判权;无权 → false 且 jumpUrl 一并置 null(双保险防前端漏改);
|
||||
// jumpApp 为空 / UserBase 不可达(判权结果 null)→ 不判权放行
|
||||
Boolean allowed = null;
|
||||
String jumpApp = text(row, "jumpApp");
|
||||
if (jumpUrl != null && StringUtils.hasText(jumpApp)) {
|
||||
allowed = authz.authorized(jumpApp.trim());
|
||||
}
|
||||
boolean showJump = jumpUrl != null && !Boolean.FALSE.equals(allowed);
|
||||
if (Boolean.FALSE.equals(allowed)) {
|
||||
jumpUrl = null;
|
||||
}
|
||||
out.put("jumpUrl", jumpUrl);
|
||||
out.put("showJump", showJump);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基址解析(B2 跳转基址统一):jumpType=1 且 jumpPath 非空时——
|
||||
* 基址解析(B2 跳转基址统一,C2 收口):jumpType=1 且 jumpPath 非空时——
|
||||
* <ol>
|
||||
* <li>绝对 URL(http/https)直接透传,不二次拼基址(存量兼容);</li>
|
||||
* <li>注册表:归属应用 jumpApp(值域=app_code)主查 → appKey 数字兜底(自发场景发送方=页面归属),
|
||||
* 取 pc_url 按 §3.2 归一化(丢 fragment、保留 index.htm(l))后拼 "#";</li>
|
||||
* <li>兜底:jump-bases 手工表(jumpApp/bizType 双维度 key),join 形态维持旧 "/#";</li>
|
||||
* <li>均无 → null(前端不渲染跳转入口)。</li>
|
||||
* <li>应用注册表严格二选一:jumpApp(值域=app_code)有值 → 只认 by-code 结果;
|
||||
* jumpApp 为空才按发送方 appKey 走 by-key(自发场景发送方=页面归属)。
|
||||
* pc_url 按 §3.2 归一化(丢 fragment、去尾斜杠、保留 index.htm(l))后拼 "#";</li>
|
||||
* <li>查无 pc_url → null(前端不渲染跳转入口)——pc_url 空就是空,不猜测、不再回落发送方首页。</li>
|
||||
* </ol>
|
||||
* 注册表是多端地址(pc/app/小程序/钉钉)唯一可持续来源,手工表仅为过渡兜底。
|
||||
* 注册表是多端地址(pc/app/小程序/钉钉)唯一来源;jump-bases 手工基址表已退役(C2)。
|
||||
*/
|
||||
private String jumpUrl(JsonNode row) {
|
||||
Integer jumpType = row.has("jumpType") && row.get("jumpType").isNumber() ? row.get("jumpType").asInt() : null;
|
||||
String jumpPath = text(row, "jumpPath");
|
||||
String jumpApp = text(row, "jumpApp");
|
||||
String bizType = text(row, "bizType");
|
||||
if (jumpType == null || jumpType != 1 || !StringUtils.hasText(jumpPath)) {
|
||||
return null;
|
||||
}
|
||||
@@ -272,45 +290,33 @@ public class MsgCenterProxyService {
|
||||
if (path.startsWith("http://") || path.startsWith("https://")) {
|
||||
return path;
|
||||
}
|
||||
String p = path.startsWith("/") ? path : "/" + path;
|
||||
String appKey = text(row, "appKey");
|
||||
|
||||
if (!userbaseDown()) {
|
||||
String pcUrl = StringUtils.hasText(jumpApp) ? pcUrlByCode(jumpApp.trim()) : null;
|
||||
if (!StringUtils.hasText(pcUrl) && StringUtils.hasText(appKey)) {
|
||||
pcUrl = pcUrlByKey(appKey.trim());
|
||||
}
|
||||
if (StringUtils.hasText(pcUrl)) {
|
||||
String base = pcUrl.trim();
|
||||
int hash = base.indexOf('#');
|
||||
if (hash >= 0) {
|
||||
base = base.substring(0, hash);
|
||||
}
|
||||
return base + "#" + p;
|
||||
}
|
||||
}
|
||||
|
||||
String base = StringUtils.hasText(jumpApp)
|
||||
? properties.getJumpBases().get(jumpApp.trim()) : null;
|
||||
if (!StringUtils.hasText(base) && StringUtils.hasText(bizType)) {
|
||||
base = properties.getJumpBases().get(bizType);
|
||||
}
|
||||
if (!StringUtils.hasText(base)) {
|
||||
if (userbaseDown()) {
|
||||
return null;
|
||||
}
|
||||
String b = base.trim();
|
||||
while (b.endsWith("/")) {
|
||||
b = b.substring(0, b.length() - 1);
|
||||
String p = path.startsWith("/") ? path : "/" + path;
|
||||
String appKey = text(row, "appKey");
|
||||
String pcUrl = StringUtils.hasText(jumpApp) ? pcUrlByCode(jumpApp.trim())
|
||||
: (StringUtils.hasText(appKey) ? pcUrlByKey(appKey.trim()) : null);
|
||||
if (!StringUtils.hasText(pcUrl)) {
|
||||
return null;
|
||||
}
|
||||
return b + "/#" + p;
|
||||
String base = pcUrl.trim();
|
||||
int hash = base.indexOf('#');
|
||||
if (hash >= 0) {
|
||||
base = base.substring(0, hash);
|
||||
}
|
||||
while (base.endsWith("/")) {
|
||||
base = base.substring(0, base.length() - 1);
|
||||
}
|
||||
return base + "#" + p;
|
||||
}
|
||||
|
||||
/** 注册表 pc_url 查询(by-code 主查)。UserBase 不可达/查无 → null 走手工表兜底,失败进短路窗口 */
|
||||
/** 注册表 pc_url 查询(by-code 主查)。UserBase 不可达/查无 → null,失败进短路窗口 */
|
||||
private String pcUrlByCode(String appCode) {
|
||||
return lookupPcUrl("/oapi/app/by-code/" + appCode);
|
||||
}
|
||||
|
||||
/** 注册表 pc_url 查询(by-key 兜底,appKey 是 app_key 列非主键 id) */
|
||||
/** 注册表 pc_url 查询(发送方 appKey 回落,appKey 是 app_key 列非主键 id) */
|
||||
private String pcUrlByKey(String appKey) {
|
||||
return lookupPcUrl("/oapi/app/by-key/" + appKey);
|
||||
}
|
||||
@@ -323,7 +329,7 @@ public class MsgCenterProxyService {
|
||||
return ok ? app.get("pcUrl").asText() : null;
|
||||
} catch (Exception e) {
|
||||
userbaseDownUntil = System.currentTimeMillis() + USERBASE_RETRY_MS;
|
||||
log.warn("[msgcenter] UserBase 查址不可达({}ms 内走手工表兜底)path={}:{}", USERBASE_RETRY_MS, path, e);
|
||||
log.warn("[msgcenter] UserBase 查址不可达({}ms 内跳转入口整体隐藏)path={}:{}", USERBASE_RETRY_MS, path, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -332,6 +338,61 @@ public class MsgCenterProxyService {
|
||||
return System.currentTimeMillis() < userbaseDownUntil;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单次请求内的判权懒加载器:列表内首个带 jumpApp 的行触发一次出站,之后行间共用
|
||||
* (禁按行重复出站);UserBase 不可达 → 判权结果 null(放行),失败进 30s 短路窗口。
|
||||
*/
|
||||
private class PerRequestAuthz {
|
||||
private final Integer userId;
|
||||
private boolean fetched;
|
||||
private Set<String> codes;
|
||||
|
||||
PerRequestAuthz(Integer userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
/** @return true=有权 false=无权 null=不可判(UserBase 不可达)→ 调用方放行 */
|
||||
Boolean authorized(String appCode) {
|
||||
if (!fetched) {
|
||||
fetched = true;
|
||||
codes = fetchAuthorizedCodes(userId);
|
||||
}
|
||||
return codes == null ? null : codes.contains(appCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉当前用户有权应用的 app_code 集合(UserBase /oapi/app/authorized-codes)。
|
||||
* 复用 userbaseDownUntil 30s 短路:短路中不出站;请求失败同样进短路窗口。
|
||||
*
|
||||
* @return app_code 集合;UserBase 不可达 → null(调用方不判权放行,绝不批量隐藏)
|
||||
*/
|
||||
private Set<String> fetchAuthorizedCodes(Integer userId) {
|
||||
if (userId == null || userbaseDown()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
query.put("userId", String.valueOf(userId));
|
||||
JsonNode data = openCall.get(TARGET_USERBASE, "/oapi/app/authorized-codes", query, JsonNode.class);
|
||||
userbaseDownUntil = 0L;
|
||||
Set<String> codes = new HashSet<>();
|
||||
if (data != null && data.isArray()) {
|
||||
for (JsonNode n : data) {
|
||||
if (n != null && n.isTextual() && StringUtils.hasText(n.asText())) {
|
||||
codes.add(n.asText().trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
return codes;
|
||||
} catch (Exception e) {
|
||||
userbaseDownUntil = System.currentTimeMillis() + USERBASE_RETRY_MS;
|
||||
log.warn("[msgcenter] UserBase 判权不可达({}ms 内不判权放行)userId={}:{}",
|
||||
USERBASE_RETRY_MS, userId, e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> emptyPage() {
|
||||
Map<String, Object> out = new HashMap<>();
|
||||
out.put("total", 0);
|
||||
|
||||
Reference in New Issue
Block a user