package logic import ( "context" "crypto/aes" "crypto/cipher" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "strings" "fusenapi/server/feishu-sync/internal/svc" "github.com/zeromicro/go-zero/core/logx" ) type WebhookLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewWebhookLogic(ctx context.Context, svcCtx *svc.ServiceContext) *WebhookLogic { return &WebhookLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } type EncryptWebhookMsg struct { Encrypt string `json:"encrypt"` //加密的消息 } type WebhookMsg struct { Type string `json:"type"` Challenge string `json:"challenge"` Header map[string]interface{} `json:"header"` Event map[string]interface{} `json:"event"` } // webhook消息事件header(body参数)基础信息 type BaseWebhookMsgHeaderType struct { EventId string `json:"event_id"` //事件id(可作为消息唯一性确认) EventType string `json:"event_type"` //事件类型 CreateTime string `json:"create_time"` //创建时间 Token string `json:"token"` //事件token AppId string `json:"app_id"` //app id TenantKey string `json:"tenant_key"` //租户key } func (l *WebhookLogic) Webhook(w http.ResponseWriter, r *http.Request) { bodyBytes, err := io.ReadAll(r.Body) if err != nil { logx.Error("读取请求body失败", err) return } defer r.Body.Close() //计算签名 timestamp := r.Header.Get("X-Lark-Request-Timestamp") nonce := r.Header.Get("X-Lark-Request-Nonce") encryptKey := "DmiHQ2bHhKiR3KK4tIjLShbs13eErxKA" signature := r.Header.Get("X-Lark-Signature") sign := l.CalculateFeiShuWebhookSignature(timestamp, nonce, encryptKey, bodyBytes) if signature != sign { logx.Error("非法的消息,签名验证不通过", sign, "====", signature) return } var encryptMsg EncryptWebhookMsg if err = json.Unmarshal(bodyBytes, &encryptMsg); err != nil { logx.Error("反序列化body失败", err) return } if encryptMsg.Encrypt == "" { logx.Error("消息加密信息是空的") return } //解密 realMsgBytes, err := l.DecryptFeiShuWebhookMsg(encryptMsg.Encrypt, encryptKey) if err != nil { logx.Error(err) return } //如果只是验证http连接的消息 var webhookMsg WebhookMsg if err = json.Unmarshal(realMsgBytes, &webhookMsg); err != nil { logx.Error("反序列化请求body失败", err) return } //验证连接(直接返回) if webhookMsg.Type == "url_verification" { challengeRsp := map[string]string{ "challenge": webhookMsg.Challenge, } b, _ := json.Marshal(challengeRsp) w.Write(b) return } headerByte, err := json.Marshal(webhookMsg.Header) if err != nil { logx.Error("序列化请求体header失败:", err) return } var msgHeader BaseWebhookMsgHeaderType if err = json.Unmarshal(headerByte, &msgHeader); err != nil { logx.Error("反序列化请求体中的header失败", err) return } logx.Info("触发webhook:", msgHeader.EventType) switch msgHeader.EventType { case "contact.custom_attr_event.updated_v3": //成员字段管理属性变更事件 case "contact.department.created_v3": //部门新建 case "contact.department.deleted_v3": //部门删除 case "contact.department.updated_v3": //部门信息变化 case "contact.employee_type_enum.actived_v3": //启动人员类型事件 case "contact.employee_type_enum.created_v3": //新建人员类型事件 case "contact.employee_type_enum.deactivated_v3": //停用人员类型事件 case "contact.employee_type_enum.deleted_v3": //删除人员类型事件 case "contact.employee_type_enum.updated_v3": //修改人员类型名称事件 case "contact.scope.updated_v3": //通讯录范围权限被更新 case "contact.user.created_v3": //员工入职 case "contact.user.deleted_v3": //员工离职 case "contact.user.updated_v3": //员工信息变化 } return } // 计算签名 func (l *WebhookLogic) CalculateFeiShuWebhookSignature(timestamp, nonce, encryptKey string, body []byte) string { var b strings.Builder b.WriteString(timestamp) b.WriteString(nonce) b.WriteString(encryptKey) b.Write(body) //bodystring 指整个请求体,不要在反序列化后再计算 bs := []byte(b.String()) h := sha256.New() h.Write(bs) bs = h.Sum(nil) sig := fmt.Sprintf("%x", bs) return sig } // 解密事件消息 func (l *WebhookLogic) DecryptFeiShuWebhookMsg(encrypt string, encryptKey string) ([]byte, error) { h := sha256.New() _, err := h.Write([]byte(encryptKey)) if err != nil { return nil, err } key := hex.EncodeToString(h.Sum(nil)) buf, err := base64.StdEncoding.DecodeString(encrypt) if err != nil { return nil, fmt.Errorf("base64StdEncode Error[%v]", err) } if len(buf) < aes.BlockSize { return nil, errors.New("cipher too short") } keyBs := sha256.Sum256([]byte(key)) block, err := aes.NewCipher(keyBs[:sha256.Size]) if err != nil { return nil, fmt.Errorf("AESNewCipher Error[%v]", err) } iv := buf[:aes.BlockSize] buf = buf[aes.BlockSize:] // CBC mode always works in whole blocks. if len(buf)%aes.BlockSize != 0 { return nil, errors.New("ciphertext is not a multiple of the block size") } mode := cipher.NewCBCDecrypter(block, iv) mode.CryptBlocks(buf, buf) n := strings.Index(string(buf), "{") if n == -1 { n = 0 } m := strings.LastIndex(string(buf), "}") if m == -1 { m = len(buf) - 1 } return buf[n : m+1], nil }