From 988ea49a033cb7aa839c57b196526e848feb51a3 Mon Sep 17 00:00:00 2001 From: mahongran <623893204@qq.com> Date: Fri, 3 Jan 2025 14:13:06 +0800 Subject: [PATCH 1/4] feat: enhance WorkAccessToken to include AgentID for improved token management - Added AgentID field to WorkAccessToken struct. - Updated NewWorkAccessToken function to accept AgentID as a parameter. - Modified access token cache key to incorporate AgentID, ensuring unique cache entries per agent. This change improves the handling of access tokens in a multi-agent environment. --- credential/default_access_token.go | 8 ++++++-- work/kf/client.go | 2 +- work/work.go | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/credential/default_access_token.go b/credential/default_access_token.go index ec88a89dc..9f5ba8237 100644 --- a/credential/default_access_token.go +++ b/credential/default_access_token.go @@ -189,19 +189,21 @@ func (ak *StableAccessToken) GetAccessTokenDirectly(ctx context.Context, forceRe type WorkAccessToken struct { CorpID string CorpSecret string + AgentID string cacheKeyPrefix string cache cache.Cache accessTokenLock *sync.Mutex } // NewWorkAccessToken new WorkAccessToken -func NewWorkAccessToken(corpID, corpSecret, cacheKeyPrefix string, cache cache.Cache) AccessTokenContextHandle { +func NewWorkAccessToken(corpID, corpSecret, agentID, cacheKeyPrefix string, cache cache.Cache) AccessTokenContextHandle { if cache == nil { panic("cache the not exist") } return &WorkAccessToken{ CorpID: corpID, CorpSecret: corpSecret, + AgentID: agentID, cache: cache, cacheKeyPrefix: cacheKeyPrefix, accessTokenLock: new(sync.Mutex), @@ -218,7 +220,9 @@ func (ak *WorkAccessToken) GetAccessTokenContext(ctx context.Context) (accessTok // 加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token ak.accessTokenLock.Lock() defer ak.accessTokenLock.Unlock() - accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.CorpID) + + // 修改缓存key,加入agentID + accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s_%s", ak.cacheKeyPrefix, ak.CorpID, ak.AgentID) val := ak.cache.Get(accessTokenCacheKey) if val != nil { accessToken = val.(string) diff --git a/work/kf/client.go b/work/kf/client.go index d83a5fcd5..335819822 100644 --- a/work/kf/client.go +++ b/work/kf/client.go @@ -24,7 +24,7 @@ func NewClient(cfg *config.Config) (client *Client, err error) { } // 初始化 AccessToken Handle - defaultAkHandle := credential.NewWorkAccessToken(cfg.CorpID, cfg.CorpSecret, credential.CacheKeyWorkPrefix, cfg.Cache) + defaultAkHandle := credential.NewWorkAccessToken(cfg.CorpID, cfg.CorpSecret, cfg.AgentID, credential.CacheKeyWorkPrefix, cfg.Cache) ctx := &context.Context{ Config: cfg, AccessTokenHandle: defaultAkHandle, diff --git a/work/work.go b/work/work.go index 24c57736a..69a2baec4 100644 --- a/work/work.go +++ b/work/work.go @@ -24,7 +24,7 @@ type Work struct { // NewWork init work func NewWork(cfg *config.Config) *Work { - defaultAkHandle := credential.NewWorkAccessToken(cfg.CorpID, cfg.CorpSecret, credential.CacheKeyWorkPrefix, cfg.Cache) + defaultAkHandle := credential.NewWorkAccessToken(cfg.CorpID, cfg.CorpSecret, cfg.AgentID, credential.CacheKeyWorkPrefix, cfg.Cache) ctx := &context.Context{ Config: cfg, AccessTokenHandle: defaultAkHandle, From dd5f6801612802d0a50c067bfea463c4cd6d7cb7 Mon Sep 17 00:00:00 2001 From: mahongran <623893204@qq.com> Date: Fri, 3 Jan 2025 18:22:44 +0800 Subject: [PATCH 2/4] refactor: enhance WorkAccessToken to improve cache key handling - Updated the AgentID field in WorkAccessToken struct to clarify its optional nature for distinguishing applications. - Modified the access token cache key construction to support both new and legacy formats based on the presence of AgentID. - Added comments for better understanding of the cache key logic and its compatibility with historical versions. This change improves the flexibility and clarity of access token management in multi-agent scenarios. --- credential/default_access_token.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/credential/default_access_token.go b/credential/default_access_token.go index 9f5ba8237..39e055bde 100644 --- a/credential/default_access_token.go +++ b/credential/default_access_token.go @@ -189,7 +189,7 @@ func (ak *StableAccessToken) GetAccessTokenDirectly(ctx context.Context, forceRe type WorkAccessToken struct { CorpID string CorpSecret string - AgentID string + AgentID string // 可选,用于区分不同应用 cacheKeyPrefix string cache cache.Cache accessTokenLock *sync.Mutex @@ -203,7 +203,7 @@ func NewWorkAccessToken(corpID, corpSecret, agentID, cacheKeyPrefix string, cach return &WorkAccessToken{ CorpID: corpID, CorpSecret: corpSecret, - AgentID: agentID, + AgentID: agentID, // agentID可以为空,兼容历史版本 cache: cache, cacheKeyPrefix: cacheKeyPrefix, accessTokenLock: new(sync.Mutex), @@ -221,8 +221,18 @@ func (ak *WorkAccessToken) GetAccessTokenContext(ctx context.Context) (accessTok ak.accessTokenLock.Lock() defer ak.accessTokenLock.Unlock() - // 修改缓存key,加入agentID - accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s_%s", ak.cacheKeyPrefix, ak.CorpID, ak.AgentID) + // 构建缓存key + var accessTokenCacheKey string + // 每个应用有独立的secret,获取到的access_token只能本应用使用,所以每个应用的access_token应该分开来获取 + // API文档:https://developer.work.weixin.qq.com/document/path/91039 + if ak.AgentID != "" { + // 如果设置了AgentID,使用新的key格式 + accessTokenCacheKey = fmt.Sprintf("%s_access_token_%s_%s", ak.cacheKeyPrefix, ak.CorpID, ak.AgentID) + } else { + // 兼容历史版本的key格式 + accessTokenCacheKey = fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.CorpID) + } + val := ak.cache.Get(accessTokenCacheKey) if val != nil { accessToken = val.(string) From 4b412ef727e6eb7eed49b374a8f4a5eb60675f96 Mon Sep 17 00:00:00 2001 From: mahongran <623893204@qq.com> Date: Sat, 4 Jan 2025 10:15:16 +0800 Subject: [PATCH 3/4] feat(work): add JsSdk method for JavaScript SDK integration - Introduced a new JsSdk method in the Work struct to facilitate the creation of a Js instance. - This addition enhances the functionality of the Work module by enabling JavaScript SDK support. This change improves the integration capabilities for developers working with the WeChat Work API. --- credential/work_js_ticket.go | 118 +++++++++++++++++++++++++++++++++++ work/jsapi/jsapi.go | 73 ++++++++++++++++++++++ work/work.go | 6 ++ 3 files changed, 197 insertions(+) create mode 100644 credential/work_js_ticket.go create mode 100644 work/jsapi/jsapi.go diff --git a/credential/work_js_ticket.go b/credential/work_js_ticket.go new file mode 100644 index 000000000..cb8c599d9 --- /dev/null +++ b/credential/work_js_ticket.go @@ -0,0 +1,118 @@ +package credential + +import ( + "encoding/json" + "fmt" + "sync" + "time" + + "github.com/silenceper/wechat/v2/cache" + "github.com/silenceper/wechat/v2/util" +) + +// TicketType ticket类型 +type TicketType int + +const ( + // TicketTypeCorpJs 企业jsapi ticket + TicketTypeCorpJs TicketType = iota + // TicketTypeAgentJs 应用jsapi ticket + TicketTypeAgentJs +) + +// 企业微信相关的 ticket URL +const ( + // 企业微信 jsapi ticket + getWorkJsTicketURL = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=%s" + // 企业微信应用 jsapi ticket + getWorkAgentJsTicketURL = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get?access_token=%s&type=agent_config" +) + +// WorkJsTicket 企业微信js ticket获取 +type WorkJsTicket struct { + corpID string + agentID string + cacheKeyPrefix string + cache cache.Cache + jsAPITicketLock *sync.Mutex +} + +// NewWorkJsTicket new WorkJsTicket +func NewWorkJsTicket(corpID, agentID, cacheKeyPrefix string, cache cache.Cache) *WorkJsTicket { + return &WorkJsTicket{ + corpID: corpID, + agentID: agentID, + cache: cache, + cacheKeyPrefix: cacheKeyPrefix, + jsAPITicketLock: new(sync.Mutex), + } +} + +// GetTicket 根据类型获取相应的jsapi_ticket +func (js *WorkJsTicket) GetTicket(accessToken string, ticketType TicketType) (ticketStr string, err error) { + var cacheKey string + switch ticketType { + case TicketTypeCorpJs: + cacheKey = fmt.Sprintf("%s_corp_jsapi_ticket_%s", js.cacheKeyPrefix, js.corpID) + case TicketTypeAgentJs: + if js.agentID == "" { + err = fmt.Errorf("agentID is empty") + return + } + cacheKey = fmt.Sprintf("%s_agent_jsapi_ticket_%s_%s", js.cacheKeyPrefix, js.corpID, js.agentID) + default: + err = fmt.Errorf("unsupported ticket type: %v", ticketType) + return + } + + if val := js.cache.Get(cacheKey); val != nil { + return val.(string), nil + } + + js.jsAPITicketLock.Lock() + defer js.jsAPITicketLock.Unlock() + + // 双检,防止重复从微信服务器获取 + if val := js.cache.Get(cacheKey); val != nil { + return val.(string), nil + } + + var ticket ResTicket + ticket, err = js.getTicketFromServer(accessToken, ticketType) + if err != nil { + return + } + expires := ticket.ExpiresIn - 1500 + err = js.cache.Set(cacheKey, ticket.Ticket, time.Duration(expires)*time.Second) + ticketStr = ticket.Ticket + return +} + +// getTicketFromServer 从服务器中获取ticket +func (js *WorkJsTicket) getTicketFromServer(accessToken string, ticketType TicketType) (ticket ResTicket, err error) { + var url string + switch ticketType { + case TicketTypeCorpJs: + url = fmt.Sprintf(getWorkJsTicketURL, accessToken) + case TicketTypeAgentJs: + url = fmt.Sprintf(getWorkAgentJsTicketURL, accessToken) + default: + err = fmt.Errorf("unsupported ticket type: %v", ticketType) + return + } + + var response []byte + response, err = util.HTTPGet(url) + if err != nil { + return + } + err = json.Unmarshal(response, &ticket) + if err != nil { + return + } + if ticket.ErrCode != 0 { + err = fmt.Errorf("getTicket Error : errcode=%d , errmsg=%s", ticket.ErrCode, ticket.ErrMsg) + return + } + return +} diff --git a/work/jsapi/jsapi.go b/work/jsapi/jsapi.go new file mode 100644 index 000000000..395837818 --- /dev/null +++ b/work/jsapi/jsapi.go @@ -0,0 +1,73 @@ +package jsapi + +import ( + "strconv" + + "github.com/silenceper/wechat/v2/credential" + "github.com/silenceper/wechat/v2/util" + "github.com/silenceper/wechat/v2/work/context" +) + +// Js struct +type Js struct { + *context.Context + jsTicket *credential.WorkJsTicket +} + +// NewJs init +func NewJs(context *context.Context) *Js { + js := new(Js) + js.Context = context + js.jsTicket = credential.NewWorkJsTicket( + context.Config.CorpID, + context.Config.AgentID, + credential.CacheKeyWorkPrefix, + context.Cache, + ) + return js +} + +// Config 返回给用户使用的配置 +type Config struct { + Timestamp int64 `json:"timestamp"` + NonceStr string `json:"nonce_str"` + Signature string `json:"signature"` +} + +// GetConfig 获取企业微信JS配置 https://developer.work.weixin.qq.com/document/path/90514 +func (js *Js) GetConfig(uri string) (config *Config, err error) { + config = new(Config) + var accessToken string + accessToken, err = js.GetAccessToken() + if err != nil { + return + } + var ticketStr string + ticketStr, err = js.jsTicket.GetTicket(accessToken, credential.TicketTypeCorpJs) + if err != nil { + return + } + config.NonceStr = util.RandomStr(16) + config.Timestamp = util.GetCurrTS() + config.Signature = util.Signature(ticketStr, config.NonceStr, strconv.FormatInt(config.Timestamp, 10), uri) + return +} + +// GetAgentConfig 获取企业微信应用JS配置 https://developer.work.weixin.qq.com/document/path/94313 +func (js *Js) GetAgentConfig(uri string) (config *Config, err error) { + config = new(Config) + var accessToken string + accessToken, err = js.GetAccessToken() + if err != nil { + return + } + var ticketStr string + ticketStr, err = js.jsTicket.GetTicket(accessToken, credential.TicketTypeAgentJs) + if err != nil { + return + } + config.NonceStr = util.RandomStr(16) + config.Timestamp = util.GetCurrTS() + config.Signature = util.Signature(ticketStr, config.NonceStr, strconv.FormatInt(config.Timestamp, 10), uri) + return +} diff --git a/work/work.go b/work/work.go index 69a2baec4..fc7352adb 100644 --- a/work/work.go +++ b/work/work.go @@ -9,6 +9,7 @@ import ( "github.com/silenceper/wechat/v2/work/context" "github.com/silenceper/wechat/v2/work/externalcontact" "github.com/silenceper/wechat/v2/work/invoice" + "github.com/silenceper/wechat/v2/work/jsapi" "github.com/silenceper/wechat/v2/work/kf" "github.com/silenceper/wechat/v2/work/material" "github.com/silenceper/wechat/v2/work/message" @@ -52,6 +53,11 @@ func (wk *Work) GetKF() (*kf.Client, error) { return kf.NewClient(wk.ctx.Config) } +// JsSdk get JsSdk +func (wk *Work) JsSdk() *jsapi.Js { + return jsapi.NewJs(wk.ctx) +} + // GetExternalContact get external_contact func (wk *Work) GetExternalContact() *externalcontact.Client { return externalcontact.NewClient(wk.ctx) From 1ee33a0e6ebc307c73b9bec6141d463cb356e929 Mon Sep 17 00:00:00 2001 From: mahongran <623893204@qq.com> Date: Tue, 14 Jan 2025 10:48:49 +0800 Subject: [PATCH 4/4] fix gofmt --- credential/default_access_token.go | 1 - 1 file changed, 1 deletion(-) diff --git a/credential/default_access_token.go b/credential/default_access_token.go index b7c4233de..c60d9119f 100644 --- a/credential/default_access_token.go +++ b/credential/default_access_token.go @@ -195,7 +195,6 @@ type WorkAccessToken struct { accessTokenLock *sync.Mutex } - // NewWorkAccessToken new WorkAccessToken (保持向后兼容) func NewWorkAccessToken(corpID, corpSecret, agentID, cacheKeyPrefix string, cache cache.Cache) AccessTokenContextHandle { // 调用新方法,保持兼容性