96b1edff45
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
225 lines
6.8 KiB
Go
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
|
|
}
|