185 lines
5.6 KiB
Go
185 lines
5.6 KiB
Go
|
|
package fadada
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"encoding/json"
|
|||
|
|
"fmt"
|
|||
|
|
"net/http"
|
|||
|
|
"strings"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// 常见回调事件名(以官方实际推送为准,解析时兼容多种字段)
|
|||
|
|
const (
|
|||
|
|
CallbackEventSignTaskSigned = "sign-task-signed"
|
|||
|
|
CallbackEventSignTaskFinished = "sign-task-finished"
|
|||
|
|
CallbackEventSignTaskCanceled = "sign-task-canceled"
|
|||
|
|
CallbackEventCorpAuthorize = "corp-authorize"
|
|||
|
|
CallbackEventCorpAuthSuccess = "corp-authorize-success"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// SuccessCallbackBody 回调应答,约定返回成功
|
|||
|
|
var SuccessCallbackBody = []byte(`{"msg":"success"}`)
|
|||
|
|
|
|||
|
|
// VerifyCallback 校验法大大回调签名(HMAC-SHA256,与出站请求同一套算法)
|
|||
|
|
func (c *Client) VerifyCallback(headers map[string]string, bizContent string) error {
|
|||
|
|
if c.config == nil || c.config.AppSecret == "" {
|
|||
|
|
return fmt.Errorf("法大大 app_secret 未配置,无法验签")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
normalized := normalizeHeaderMap(headers)
|
|||
|
|
sign := firstHeader(normalized, "X-FASC-Sign", "X-Fasc-Sign")
|
|||
|
|
if sign == "" {
|
|||
|
|
return fmt.Errorf("缺少回调签名头 X-FASC-Sign")
|
|||
|
|
}
|
|||
|
|
timestamp := firstHeader(normalized, "X-FASC-Timestamp", "X-Fasc-Timestamp")
|
|||
|
|
if timestamp == "" {
|
|||
|
|
return fmt.Errorf("缺少回调时间戳头 X-FASC-Timestamp")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
headMap := map[string]string{
|
|||
|
|
"X-FASC-App-Id": firstHeader(normalized, "X-FASC-App-Id", "X-Fasc-App-Id"),
|
|||
|
|
"X-FASC-Sign-Type": firstHeader(normalized, "X-FASC-Sign-Type", "X-Fasc-Sign-Type"),
|
|||
|
|
"X-FASC-Timestamp": timestamp,
|
|||
|
|
"X-FASC-Nonce": firstHeader(normalized, "X-FASC-Nonce", "X-Fasc-Nonce"),
|
|||
|
|
"X-FASC-Api-SubVersion": firstHeader(normalized, "X-FASC-Api-SubVersion", "X-Fasc-Api-SubVersion"),
|
|||
|
|
"bizContent": bizContent,
|
|||
|
|
}
|
|||
|
|
if event := firstHeader(normalized, "X-FASC-Event", "X-Fasc-Event"); event != "" {
|
|||
|
|
headMap["X-FASC-Event"] = event
|
|||
|
|
}
|
|||
|
|
if token := firstHeader(normalized, "X-FASC-AccessToken", "X-Fasc-AccessToken"); token != "" {
|
|||
|
|
headMap["X-FASC-AccessToken"] = token
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
expected := SignByMap(headMap, timestamp, c.config.AppSecret)
|
|||
|
|
if !strings.EqualFold(expected, sign) {
|
|||
|
|
return fmt.Errorf("法大大回调签名校验失败")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
appID := firstHeader(normalized, "X-FASC-App-Id", "X-Fasc-App-Id")
|
|||
|
|
if appID != "" && c.config.AppID != "" && appID != c.config.AppID {
|
|||
|
|
return fmt.Errorf("法大大回调 AppId 不匹配")
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// VerifyCallbackFromHTTP 从 http.Header 验签
|
|||
|
|
func (c *Client) VerifyCallbackFromHTTP(h http.Header, bizContent string) error {
|
|||
|
|
headers := make(map[string]string, len(h))
|
|||
|
|
for k, vals := range h {
|
|||
|
|
if len(vals) > 0 {
|
|||
|
|
headers[k] = vals[0]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return c.VerifyCallback(headers, bizContent)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ParseCallback 解析 bizContent 为统一 CallbackEvent
|
|||
|
|
func (c *Client) ParseCallback(headers map[string]string, bizContent string) (*CallbackEvent, error) {
|
|||
|
|
bizContent = strings.TrimSpace(bizContent)
|
|||
|
|
if bizContent == "" {
|
|||
|
|
return nil, fmt.Errorf("回调 bizContent 不能为空")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
raw := make(map[string]interface{})
|
|||
|
|
if err := json.Unmarshal([]byte(bizContent), &raw); err != nil {
|
|||
|
|
return nil, fmt.Errorf("解析回调 bizContent 失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
normalized := normalizeHeaderMap(headers)
|
|||
|
|
event := firstHeader(normalized, "X-FASC-Event", "X-Fasc-Event")
|
|||
|
|
if event == "" {
|
|||
|
|
event = stringValue(raw, "event", "eventType", "type")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ev := &CallbackEvent{
|
|||
|
|
Event: event,
|
|||
|
|
SignTaskID: stringValue(raw, "signTaskId", "sign_task_id"),
|
|||
|
|
SignTaskStatus: stringValue(raw, "signTaskStatus", "sign_task_status"),
|
|||
|
|
ClientCorpID: stringValue(raw, "clientCorpId", "client_corp_id"),
|
|||
|
|
OpenCorpID: stringValue(raw, "openCorpId", "open_corp_id"),
|
|||
|
|
AuthResult: stringValue(raw, "authResult", "auth_result", "result"),
|
|||
|
|
EventTime: stringValue(raw, "eventTime", "event_time", "timestamp"),
|
|||
|
|
Raw: raw,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if data, ok := raw["data"].(map[string]interface{}); ok {
|
|||
|
|
if ev.SignTaskID == "" {
|
|||
|
|
ev.SignTaskID = stringValue(data, "signTaskId", "sign_task_id")
|
|||
|
|
}
|
|||
|
|
if ev.SignTaskStatus == "" {
|
|||
|
|
ev.SignTaskStatus = stringValue(data, "signTaskStatus", "sign_task_status")
|
|||
|
|
}
|
|||
|
|
if ev.ClientCorpID == "" {
|
|||
|
|
ev.ClientCorpID = stringValue(data, "clientCorpId", "client_corp_id")
|
|||
|
|
}
|
|||
|
|
if ev.OpenCorpID == "" {
|
|||
|
|
ev.OpenCorpID = stringValue(data, "openCorpId", "open_corp_id")
|
|||
|
|
}
|
|||
|
|
if ev.AuthResult == "" {
|
|||
|
|
ev.AuthResult = stringValue(data, "authResult", "auth_result", "result")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return ev, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// IsSignCompletedCallback 是否签署完成类回调
|
|||
|
|
func (e *CallbackEvent) IsSignCompletedCallback() bool {
|
|||
|
|
if e == nil {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
if IsSignTaskCompletedStatus(e.SignTaskStatus) {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
switch e.Event {
|
|||
|
|
case CallbackEventSignTaskFinished, CallbackEventSignTaskSigned:
|
|||
|
|
return true
|
|||
|
|
default:
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func normalizeHeaderMap(headers map[string]string) map[string]string {
|
|||
|
|
out := make(map[string]string, len(headers))
|
|||
|
|
for k, v := range headers {
|
|||
|
|
out[http.CanonicalHeaderKey(k)] = v
|
|||
|
|
out[k] = v
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func firstHeader(headers map[string]string, keys ...string) string {
|
|||
|
|
for _, key := range keys {
|
|||
|
|
if v := strings.TrimSpace(headers[key]); v != "" {
|
|||
|
|
return v
|
|||
|
|
}
|
|||
|
|
if v := strings.TrimSpace(headers[http.CanonicalHeaderKey(key)]); v != "" {
|
|||
|
|
return v
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func stringValue(m map[string]interface{}, keys ...string) string {
|
|||
|
|
for _, key := range keys {
|
|||
|
|
if v, ok := m[key]; ok && v != nil {
|
|||
|
|
switch t := v.(type) {
|
|||
|
|
case string:
|
|||
|
|
if strings.TrimSpace(t) != "" {
|
|||
|
|
return strings.TrimSpace(t)
|
|||
|
|
}
|
|||
|
|
case float64:
|
|||
|
|
return fmt.Sprintf("%.0f", t)
|
|||
|
|
case json.Number:
|
|||
|
|
return t.String()
|
|||
|
|
default:
|
|||
|
|
s := strings.TrimSpace(fmt.Sprint(t))
|
|||
|
|
if s != "" && s != "<nil>" {
|
|||
|
|
return s
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return ""
|
|||
|
|
}
|