| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- package dingtalk
- import (
- "context"
- "sync"
- "time"
- )
- // 钉钉 access_token 缓存相关常量
- const (
- // tokenRefreshBuffer token 过期前提前刷新的时间余量
- tokenRefreshBuffer = 2 * time.Minute
- // tokenMaxTTL 兜底 TTL(钉钉 token 有效期一般 7200s)
- tokenMaxTTL = 2 * time.Hour
- )
- // TokenStorage 可插拔的外部 Token 存储(如 Redis),接口保持最小
- // 实现需保证并发安全。返回的 token 为空串视为"未命中/已过期"。
- type TokenStorage interface {
- GetToken(ctx context.Context, clientID string) (token string, err error)
- SetToken(ctx context.Context, clientID string, token string, ttl time.Duration) error
- }
- // tokenEntry 内存 L1 缓存单元
- type tokenEntry struct {
- token string
- expiresAt time.Time
- }
- // tokenManager Token 管理器:L1 内存 + L2 可插拔(如 Redis),并带 singleflight 去重
- type tokenManager struct {
- mu sync.RWMutex
- entries map[string]tokenEntry // key: clientID
- inflight map[string]*inflightCall
- inflightMu sync.Mutex
- storage TokenStorage // 可为 nil
- }
- type inflightCall struct {
- done chan struct{}
- token string
- err error
- }
- var (
- globalTokenManager = &tokenManager{
- entries: make(map[string]tokenEntry),
- inflight: make(map[string]*inflightCall),
- }
- tokenStorageMu sync.RWMutex
- )
- // SetTokenStorage 注入外部 Token 存储(如 Redis),通常在 boot 阶段调用一次
- func SetTokenStorage(storage TokenStorage) {
- tokenStorageMu.Lock()
- defer tokenStorageMu.Unlock()
- globalTokenManager.storage = storage
- }
- // getStorage 安全读取当前 TokenStorage
- func (m *tokenManager) getStorage() TokenStorage {
- tokenStorageMu.RLock()
- defer tokenStorageMu.RUnlock()
- return m.storage
- }
- // getFromMem 从 L1 读取有效 token
- func (m *tokenManager) getFromMem(clientID string) (string, bool) {
- m.mu.RLock()
- defer m.mu.RUnlock()
- e, ok := m.entries[clientID]
- if !ok {
- return "", false
- }
- if time.Now().After(e.expiresAt) {
- return "", false
- }
- return e.token, true
- }
- // setToMem 写入 L1
- func (m *tokenManager) setToMem(clientID, token string, ttl time.Duration) {
- m.mu.Lock()
- defer m.mu.Unlock()
- m.entries[clientID] = tokenEntry{
- token: token,
- expiresAt: time.Now().Add(ttl),
- }
- }
- // Fetch 获取 access_token:按 L1 → L2(storage) → refresh 顺序查找,带 singleflight
- // refreshFn 由调用方(dingtalk.Client)注入,返回 (token, ttl, err)
- func (m *tokenManager) Fetch(
- ctx context.Context,
- clientID string,
- refreshFn func(ctx context.Context) (string, time.Duration, error),
- ) (string, error) {
- // L1
- if tk, ok := m.getFromMem(clientID); ok {
- return tk, nil
- }
- // L2
- if storage := m.getStorage(); storage != nil {
- if tk, err := storage.GetToken(ctx, clientID); err == nil && tk != "" {
- // 回填 L1(保守给一个最大 TTL,由 L2 过期决定真实生命周期)
- m.setToMem(clientID, tk, tokenMaxTTL-tokenRefreshBuffer)
- return tk, nil
- }
- }
- // Refresh (singleflight)
- m.inflightMu.Lock()
- call, exists := m.inflight[clientID]
- if !exists {
- call = &inflightCall{done: make(chan struct{})}
- m.inflight[clientID] = call
- m.inflightMu.Unlock()
- go func() {
- defer func() {
- m.inflightMu.Lock()
- delete(m.inflight, clientID)
- m.inflightMu.Unlock()
- close(call.done)
- }()
- token, ttl, err := refreshFn(ctx)
- if err != nil {
- call.err = err
- return
- }
- effectiveTTL := ttl - tokenRefreshBuffer
- if effectiveTTL <= 0 {
- effectiveTTL = tokenMaxTTL - tokenRefreshBuffer
- }
- m.setToMem(clientID, token, effectiveTTL)
- if storage := m.getStorage(); storage != nil {
- _ = storage.SetToken(ctx, clientID, token, effectiveTTL)
- }
- call.token = token
- }()
- } else {
- m.inflightMu.Unlock()
- }
- select {
- case <-call.done:
- return call.token, call.err
- case <-ctx.Done():
- return "", ctx.Err()
- }
- }
- // Invalidate 主动失效某个 clientID 的缓存(例如收到 40014 InvalidAuthentication 时)
- func (m *tokenManager) Invalidate(ctx context.Context, clientID string) {
- m.mu.Lock()
- delete(m.entries, clientID)
- m.mu.Unlock()
- if storage := m.getStorage(); storage != nil {
- _ = storage.SetToken(ctx, clientID, "", time.Second)
- }
- }
|