Files
iotlcm/internal/logic/upstreamimpl/sim_yd_token.go
T
MiFaZhan 96b1edff45 chore: 初始开源发布
iotlcm-upstream-chan:运营商上游通道适配服务(统一 /sim/* 协议)。

- 内联原私有依赖:yunze-public → internal/pub/(10 包 18 文件),
  yunze-pb/pb → internal/pb/(9 文件);Consul 注册改用官方
  gogf/gf/contrib/registry/consul/v2(API 兼容)
- Go module:yz-svr → git.5iot.com/iotlcm-public/iotlcm-upstream-chan
- 修复 mingqian 包测试:实现重构后 `adapter.operations` 字段已移除,
  测试断言按当前实现语义重写(不改产品行为)
- 凭据与内部标识脱敏:MySQL/Redis/Consul 凭据、私网与生产 IP、
  内网域名、真实手机号、注释里的真实 token/transid
- 移除内部资产:CI 触发器、生产通道种子 SQL、真实凭据测试资源文档
- Dockerfile 改用公共基础镜像,移除写死的 registry 凭据
- 补 Apache-2.0 LICENSE,重写 README,LF 行尾 + .gitattributes
2026-10-22 16:08:06 +08:00

225 lines
6.8 KiB
Go

package upstreamimpl
import (
"context"
"encoding/json"
"errors"
"fmt"
"sync"
"time"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model/entity"
"github.com/gogf/gf/v2/frame/g"
)
const ydTokenInvalidCode = "12021"
var errYdTokenUnchanged = errors.New("mobile token recovery returned the rejected token")
type ydResponseMeta struct {
Status string `json:"status"`
Message string `json:"message"`
}
type ydHTTPResponse struct{ body string }
func (r *ydHTTPResponse) ReadAllString() string { return r.body }
type ydRequestFunc func(params g.Map) (string, error)
// ydTokenProvider is the construction seam for channels whose credential is
// shared outside t_sim_channels. Implementations own storage and refresh;
// RecoverToken must use compare-and-swap semantics for rejectedToken.
type ydTokenProvider interface {
GetToken(ctx context.Context) (string, error)
RecoverToken(ctx context.Context, rejectedToken string) (string, error)
}
const (
// expired_only reuses the current token until it actually expires.
ydTokenRefreshModeExpiredOnly = "expired_only"
// proactive enters the configured advance window before requesting a token.
ydTokenRefreshModeProactive = "proactive"
ydTokenDefaultSkewSeconds = int64(600)
)
type ydTokenRefreshPolicy struct {
mode string
skewSecond int64
}
func loadYdTokenRefreshPolicy(ctx context.Context) ydTokenRefreshPolicy {
mode := g.Cfg().MustGet(ctx, "dependencies.token_refresh.mode", ydTokenRefreshModeExpiredOnly).String()
if mode != ydTokenRefreshModeExpiredOnly && mode != ydTokenRefreshModeProactive {
g.Log().Warningf(ctx, "unknown token refresh mode=%s, fallback to %s", mode, ydTokenRefreshModeExpiredOnly)
mode = ydTokenRefreshModeExpiredOnly
}
skew := g.Cfg().MustGet(ctx, "dependencies.token_refresh.skew_seconds", ydTokenDefaultSkewSeconds).Int64()
if skew < 0 {
g.Log().Warningf(ctx, "invalid token refresh skew_seconds=%d, fallback to 0", skew)
skew = 0
}
return ydTokenRefreshPolicy{mode: mode, skewSecond: skew}
}
func (p ydTokenRefreshPolicy) shouldRefresh(channel *entity.TSimChannels, now time.Time) bool {
if channel == nil || channel.AccessToken == "" || channel.AccessTokenExpire == nil {
return true
}
expire := channel.AccessTokenExpire.Time
if p.mode == ydTokenRefreshModeProactive {
return !now.Before(expire.Add(-time.Duration(p.skewSecond) * time.Second))
}
return !now.Before(expire)
}
var (
ydTokenGuards sync.Map
ydTokenStates sync.Map // map[string]*ydTokenState
)
type ydTokenState struct {
mu sync.RWMutex
token string
createdAt time.Time
expiresAt time.Time
}
func ydTokenStateFor(chanKey string) *ydTokenState {
state, _ := ydTokenStates.LoadOrStore(chanKey, &ydTokenState{})
return state.(*ydTokenState)
}
func seedYdTokenState(channel *entity.TSimChannels) {
if channel == nil || channel.ChanKey == "" || channel.AccessToken == "" || channel.AccessTokenExpire == nil {
return
}
state := ydTokenStateFor(channel.ChanKey)
state.mu.Lock()
createdAt := time.Time{}
if channel.AccessTokenCreated != nil {
createdAt = channel.AccessTokenCreated.Time
}
if state.token == "" || createdAt.After(state.createdAt) {
state.token = channel.AccessToken
state.createdAt = createdAt
state.expiresAt = channel.AccessTokenExpire.Time
}
state.mu.Unlock()
}
func currentYdToken(channel *entity.TSimChannels) (string, time.Time) {
if channel == nil {
return "", time.Time{}
}
seedYdTokenState(channel)
state := ydTokenStateFor(channel.ChanKey)
state.mu.RLock()
token, expiresAt := state.token, state.expiresAt
state.mu.RUnlock()
if token == "" || expiresAt.IsZero() || !time.Now().Before(expiresAt) {
return "", expiresAt
}
return token, expiresAt
}
func updateYdTokenState(chanKey, token string, createdAt, expiresAt time.Time) {
state := ydTokenStateFor(chanKey)
state.mu.Lock()
state.token = token
state.createdAt = createdAt
state.expiresAt = expiresAt
state.mu.Unlock()
}
func clearYdTokenStateIfCurrent(chanKey, rejected string) {
state := ydTokenStateFor(chanKey)
state.mu.Lock()
if state.token == rejected {
state.token = ""
state.createdAt = time.Time{}
state.expiresAt = time.Time{}
}
state.mu.Unlock()
}
func withYdTokenGuard(chanKey string, fn func() string) string {
guard, _ := ydTokenGuards.LoadOrStore(chanKey, &sync.Mutex{})
mu := guard.(*sync.Mutex)
mu.Lock()
defer mu.Unlock()
return fn()
}
// validYdToken only checks actual expiry. Proactive refresh decisions belong
// to ydTokenRefreshPolicy so refresh=0 integrations can reuse the token until
// it really expires.
func validYdToken(channel *entity.TSimChannels, now time.Time) string {
if channel == nil || channel.AccessToken == "" || channel.AccessTokenExpire == nil {
return ""
}
if !now.Before(channel.AccessTokenExpire.Time) {
return ""
}
return channel.AccessToken
}
func recoverYdToken(ctx context.Context, chanKey string, channel *entity.TSimChannels, rejectedToken string, refresh func(context.Context) string) (token string, err error) {
withYdTokenGuard(chanKey, func() string {
if channel == nil {
err = errors.New("mobile token recovery has no channel")
return ""
}
seedYdTokenState(channel)
if current, _ := currentYdToken(channel); current != "" && current != rejectedToken {
channel.AccessToken = current
token = current
return ""
}
clearYdTokenStateIfCurrent(chanKey, rejectedToken)
channel.AccessToken = ""
channel.AccessTokenExpire = nil
token = refresh(ctx)
if token == "" {
err = errors.New("mobile token recovery failed to acquire a token")
} else if token == rejectedToken {
err = errYdTokenUnchanged
}
return ""
})
return token, err
}
func doYdRequest(ctx context.Context, operation, chanKey string, getToken func(context.Context) string, recoverToken func(context.Context, string) (string, error), params g.Map, request ydRequestFunc) (*ydHTTPResponse, error) {
var lastBody string
for attempt := 0; attempt < 2; attempt++ {
token := getToken(ctx)
requestParams := make(g.Map, len(params)+1)
for key, value := range params {
requestParams[key] = value
}
requestParams["token"] = token
body, err := request(requestParams)
if err != nil {
return nil, err
}
lastBody = body
var meta ydResponseMeta
if err = json.Unmarshal([]byte(body), &meta); err != nil {
return nil, err
}
if meta.Status != ydTokenInvalidCode || attempt == 1 {
return &ydHTTPResponse{body: body}, nil
}
g.Log().Warningf(ctx, "%s token rejected, invalidate and retry once, chanKey:%s", operation, chanKey)
if _, err = recoverToken(ctx, token); err != nil {
if errors.Is(err, errYdTokenUnchanged) {
g.Log().Warningf(ctx, "%s token recovery returned the rejected token, stop retrying, chanKey:%s", operation, chanKey)
return &ydHTTPResponse{body: body}, nil
}
return nil, fmt.Errorf("%s token recovery failed: %w", operation, err)
}
}
return &ydHTTPResponse{body: lastBody}, nil
}