Ver código fonte

feat:企业微信客户端API JS-SDK wx.config 和 wx.agentConfig 方法权限签名 (#817)

* 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.

* 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.

* 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.

* fix gofmt
mahongran 1 ano atrás
pai
commit
9c87d1cb34
4 arquivos alterados com 198 adições e 0 exclusões
  1. 1 0
      credential/default_access_token.go
  2. 118 0
      credential/work_js_ticket.go
  3. 73 0
      work/jsapi/jsapi.go
  4. 6 0
      work/work.go

+ 1 - 0
credential/default_access_token.go

@@ -229,6 +229,7 @@ func (ak *WorkAccessToken) GetAccessTokenContext(ctx context.Context) (accessTok
 
 	// 构建缓存key
 	var accessTokenCacheKey string
+
 	if ak.AgentID != "" {
 		// 如果设置了AgentID,使用新的key格式
 		accessTokenCacheKey = fmt.Sprintf("%s_access_token_%s_%s", ak.cacheKeyPrefix, ak.CorpID, ak.AgentID)

+ 118 - 0
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
+}

+ 73 - 0
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
+}

+ 6 - 0
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)