Files
junhong_cmp_fiber/internal/gateway/readonly_guard.go
邹进 5cf67b98de
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m36s
feat: 网关只读资产改为数据表,只读资产只允许查询类调用
- 新增 tb_gateway_readonly_card(卡 ICCID / 设备 IMEI)与开关 JUNHONG_GATEWAY_READONLY_ENABLED,
  替换环境变量名单 JUNHONG_GATEWAY_READONLY_ICCIDS
- 只读资产只放行查卡状态、查实名、查流量、查设备信息、查设备槽位、同步设备状态;
  停机、复机、限速、实名认证链接与设备写操作在网关出口本地拦截;查表失败按拦截;不缓存
- 测试环境部署开启只读模式;新增技术方案与清理顺序文档

Change: add-gateway-readonly-prod-import

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:55:52 +09:00

84 lines
2.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gateway
import (
"context"
"encoding/json"
"strings"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// readonlyAllowedPaths 是只读资产允许调用的查询类网关接口,这些调用不查只读表。
var readonlyAllowedPaths = map[string]struct{}{
"/flow-card/status": {},
"/flow-card/realName": {},
"/flow-card/flow": {},
"/device/info": {},
"/device/slot-info": {},
"/device/sync-info": {},
}
// ReadonlyChecker 判断网关请求卡号(卡为 ICCID,设备为 IMEI)是否为只读资产。
type ReadonlyChecker func(ctx context.Context, identifier string) (bool, error)
// WithReadonlyChecker 开启只读模式(支持链式调用):只读资产只允许查询类调用,
// 停机、复机、限速、实名认证链接与设备写操作在本地拦截、不发请求。checker 为 nil 时不生效(生产不开启)。
func (c *Client) WithReadonlyChecker(checker ReadonlyChecker) *Client {
c.readonlyChecker = checker
if checker != nil && c.logger != nil {
c.logger.Warn("Gateway 只读模式已开启:只读资产只允许查询类调用,其余调用一律拦截")
}
return c
}
// rejectReadonlyWrite 判断本次请求是否为只读资产的非查询调用;是则记日志并返回明确失败(不是结果未知),不发请求。
// 查只读表失败也按拦截处理:宁可调用失败,不放过真卡。
func (c *Client) rejectReadonlyWrite(ctx context.Context, path string, params interface{}) error {
if c.readonlyChecker == nil {
return nil
}
if _, ok := readonlyAllowedPaths[path]; ok {
return nil
}
cardNo := requestCardNo(params)
if cardNo == "" {
return nil
}
readonly, err := c.readonlyChecker(ctx, cardNo)
if err == nil && !readonly {
return nil
}
if c.logger != nil {
fields := []zap.Field{zap.String("path", path), zap.String("card_no", cardNo), zap.String("metric", "gateway.readonly_card.blocked")}
if err != nil {
fields = append(fields, zap.Error(err))
c.logger.Error("Gateway 只读表查询失败,按只读拦截,未发送请求", fields...)
} else {
c.logger.Warn("Gateway 只读资产非查询调用已拦截,未发送请求", fields...)
}
}
return newGatewayError(constants.GatewayErrorUpstream, errors.New(errors.CodeGatewayError, "测试环境只读资产,已拦截调用:"+path))
}
// requestCardNo 取请求里的卡号(cardNo 或 iccid 字段,大写),取不到时返回空串。
func requestCardNo(params interface{}) string {
raw, err := json.Marshal(params)
if err != nil {
return ""
}
var fields struct {
CardNo string `json:"cardNo"`
ICCID string `json:"iccid"`
}
if err := json.Unmarshal(raw, &fields); err != nil {
return ""
}
if fields.CardNo != "" {
return strings.ToUpper(strings.TrimSpace(fields.CardNo))
}
return strings.ToUpper(strings.TrimSpace(fields.ICCID))
}