v0.1
This commit is contained in:
@@ -2,18 +2,43 @@ package esign
|
||||
|
||||
import "fmt"
|
||||
|
||||
type EsignContractConfig struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
ExpireDays int `json:"expireDays" yaml:"expire_days"`
|
||||
RetryCount int `json:"retryCount" yaml:"retry_count"`
|
||||
}
|
||||
|
||||
type EsignAuthConfig struct {
|
||||
OrgAuthModes []string `json:"orgAuthModes" yaml:"org_auth_modes"`
|
||||
DefaultAuthMode string `json:"defaultAuthMode" yaml:"default_auth_mode"`
|
||||
PsnAuthModes []string `json:"psnAuthModes" yaml:"psn_auth_modes"`
|
||||
WillingnessAuthModes []string `json:"willingnessAuthModes" yaml:"willingness_auth_modes"`
|
||||
RedirectUrl string `json:"redirectUrl" yaml:"redirect_url"`
|
||||
}
|
||||
|
||||
type EsignSignConfig struct {
|
||||
AutoFinish bool `json:"autoFinish" yaml:"auto_finish"`
|
||||
SignFieldStyle int `json:"signFieldStyle" yaml:"sign_field_style"`
|
||||
ClientType string `json:"clientType" yaml:"client_type"`
|
||||
RedirectUrl string `json:"redirectUrl" yaml:"redirect_url"`
|
||||
}
|
||||
|
||||
// Config e签宝服务配置结构体
|
||||
// 包含应用ID、密钥、服务器URL和模板ID等基础配置信息
|
||||
// 新增Contract、Auth、Sign配置
|
||||
type Config struct {
|
||||
AppID string `json:"appId"` // 应用ID
|
||||
AppSecret string `json:"appSecret"` // 应用密钥
|
||||
ServerURL string `json:"serverUrl"` // 服务器URL
|
||||
TemplateID string `json:"templateId"` // 模板ID
|
||||
AppID string `json:"appId" yaml:"app_id"`
|
||||
AppSecret string `json:"appSecret" yaml:"app_secret"`
|
||||
ServerURL string `json:"serverUrl" yaml:"server_url"`
|
||||
TemplateID string `json:"templateId" yaml:"template_id"`
|
||||
Contract *EsignContractConfig `json:"contract" yaml:"contract"`
|
||||
Auth *EsignAuthConfig `json:"auth" yaml:"auth"`
|
||||
Sign *EsignSignConfig `json:"sign" yaml:"sign"`
|
||||
}
|
||||
|
||||
// NewConfig 创建新的配置实例
|
||||
// 提供配置验证和默认值设置
|
||||
func NewConfig(appID, appSecret, serverURL, templateID string) (*Config, error) {
|
||||
func NewConfig(appID, appSecret, serverURL, templateID string, contract *EsignContractConfig, auth *EsignAuthConfig, sign *EsignSignConfig) (*Config, error) {
|
||||
if appID == "" {
|
||||
return nil, fmt.Errorf("应用ID不能为空")
|
||||
}
|
||||
@@ -26,12 +51,15 @@ func NewConfig(appID, appSecret, serverURL, templateID string) (*Config, error)
|
||||
if templateID == "" {
|
||||
return nil, fmt.Errorf("模板ID不能为空")
|
||||
}
|
||||
|
||||
|
||||
return &Config{
|
||||
AppID: appID,
|
||||
AppSecret: appSecret,
|
||||
ServerURL: serverURL,
|
||||
TemplateID: templateID,
|
||||
Contract: contract,
|
||||
Auth: auth,
|
||||
Sign: sign,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -60,7 +88,7 @@ const (
|
||||
AuthModeBank = "PSN_BANK" // 银行卡认证
|
||||
|
||||
// 意愿认证模式
|
||||
WillingnessAuthSMS = "CODE_SMS" // 短信验证码
|
||||
WillingnessAuthSMS = "CODE_SMS" // 短信验证码
|
||||
WillingnessAuthEmail = "CODE_EMAIL" // 邮箱验证码
|
||||
|
||||
// 证件类型常量
|
||||
@@ -69,7 +97,7 @@ const (
|
||||
|
||||
// 签署区样式常量
|
||||
SignFieldStyleNormal = 1 // 普通签章
|
||||
SignFieldStyleSeam = 2 // 骑缝签章
|
||||
SignFieldStyleSeam = 2 // 骑缝签章
|
||||
|
||||
// 签署人类型常量
|
||||
SignerTypePerson = 0 // 个人
|
||||
@@ -80,4 +108,4 @@ const (
|
||||
|
||||
// 客户端类型常量
|
||||
ClientTypeAll = "ALL" // 所有客户端
|
||||
)
|
||||
)
|
||||
|
||||
@@ -13,6 +13,24 @@ func Example() {
|
||||
"your_app_secret",
|
||||
"https://smlopenapi.esign.cn",
|
||||
"your_template_id",
|
||||
&EsignContractConfig{
|
||||
Name: "测试合同",
|
||||
ExpireDays: 30,
|
||||
RetryCount: 3,
|
||||
},
|
||||
&EsignAuthConfig{
|
||||
OrgAuthModes: []string{"ORG"},
|
||||
DefaultAuthMode: "ORG",
|
||||
PsnAuthModes: []string{"PSN"},
|
||||
WillingnessAuthModes: []string{"WILLINGNESS"},
|
||||
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
|
||||
},
|
||||
&EsignSignConfig{
|
||||
AutoFinish: true,
|
||||
SignFieldStyle: 1,
|
||||
ClientType: "ALL",
|
||||
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal("配置创建失败:", err)
|
||||
@@ -122,7 +140,22 @@ func Example() {
|
||||
// ExampleBasicUsage 基础用法示例
|
||||
func ExampleBasicUsage() {
|
||||
// 最简单的用法 - 一行代码完成合同签署
|
||||
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id")
|
||||
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id", &EsignContractConfig{
|
||||
Name: "测试合同",
|
||||
ExpireDays: 30,
|
||||
RetryCount: 3,
|
||||
}, &EsignAuthConfig{
|
||||
OrgAuthModes: []string{"ORG"},
|
||||
DefaultAuthMode: "ORG",
|
||||
PsnAuthModes: []string{"PSN"},
|
||||
WillingnessAuthModes: []string{"WILLINGNESS"},
|
||||
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
|
||||
}, &EsignSignConfig{
|
||||
AutoFinish: true,
|
||||
SignFieldStyle: 1,
|
||||
ClientType: "ALL",
|
||||
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
|
||||
})
|
||||
client := NewClient(config)
|
||||
|
||||
// 快速合同签署
|
||||
@@ -143,7 +176,22 @@ func ExampleBasicUsage() {
|
||||
|
||||
// ExampleWithCustomData 自定义数据示例
|
||||
func ExampleWithCustomData() {
|
||||
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id")
|
||||
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id", &EsignContractConfig{
|
||||
Name: "测试合同",
|
||||
ExpireDays: 30,
|
||||
RetryCount: 3,
|
||||
}, &EsignAuthConfig{
|
||||
OrgAuthModes: []string{"ORG"},
|
||||
DefaultAuthMode: "ORG",
|
||||
PsnAuthModes: []string{"PSN"},
|
||||
WillingnessAuthModes: []string{"WILLINGNESS"},
|
||||
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
|
||||
}, &EsignSignConfig{
|
||||
AutoFinish: true,
|
||||
SignFieldStyle: 1,
|
||||
ClientType: "ALL",
|
||||
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
|
||||
})
|
||||
client := NewClient(config)
|
||||
|
||||
// 使用自定义模板数据
|
||||
@@ -171,7 +219,22 @@ func ExampleWithCustomData() {
|
||||
|
||||
// ExampleEnterpriseAuth 企业认证示例
|
||||
func ExampleEnterpriseAuth() {
|
||||
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id")
|
||||
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id", &EsignContractConfig{
|
||||
Name: "测试合同",
|
||||
ExpireDays: 30,
|
||||
RetryCount: 3,
|
||||
}, &EsignAuthConfig{
|
||||
OrgAuthModes: []string{"ORG"},
|
||||
DefaultAuthMode: "ORG",
|
||||
PsnAuthModes: []string{"PSN"},
|
||||
WillingnessAuthModes: []string{"WILLINGNESS"},
|
||||
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
|
||||
}, &EsignSignConfig{
|
||||
AutoFinish: true,
|
||||
SignFieldStyle: 1,
|
||||
ClientType: "ALL",
|
||||
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
|
||||
})
|
||||
client := NewClient(config)
|
||||
|
||||
// 企业认证
|
||||
|
||||
@@ -34,8 +34,8 @@ func (s *FileOpsService) UpdateConfig(config *Config) {
|
||||
func (s *FileOpsService) DownloadSignedFile(signFlowId string) (*DownloadSignedFileResponse, error) {
|
||||
fmt.Println("开始下载已签署文件及附属材料...")
|
||||
|
||||
// 发送API请求
|
||||
urlPath := fmt.Sprintf("/v3/sign-flow/%s/attachments", signFlowId)
|
||||
// 按照最新e签宝文档,接口路径应为 /v3/sign-flow/{signFlowId}/file-download-url
|
||||
urlPath := fmt.Sprintf("/v3/sign-flow/%s/file-download-url", signFlowId)
|
||||
responseBody, err := s.httpClient.Request("GET", urlPath, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("下载已签署文件失败: %v", err)
|
||||
|
||||
@@ -44,8 +44,8 @@ func (h *HTTPClient) UpdateConfig(config *Config) {
|
||||
func (h *HTTPClient) Request(method, urlPath string, body []byte) ([]byte, error) {
|
||||
// 生成签名所需参数
|
||||
timestamp := getCurrentTimestamp()
|
||||
nonce := generateNonce()
|
||||
date := getCurrentDate()
|
||||
// date := getCurrentDate()
|
||||
date := ""
|
||||
|
||||
// 计算Content-MD5
|
||||
contentMD5 := ""
|
||||
@@ -53,14 +53,14 @@ func (h *HTTPClient) Request(method, urlPath string, body []byte) ([]byte, error
|
||||
contentMD5 = getContentMD5(body)
|
||||
}
|
||||
|
||||
// 根据Java示例,Headers为空字符串
|
||||
headers := ""
|
||||
|
||||
// 生成签名
|
||||
// 生成签名(用原始urlPath)
|
||||
signature := generateSignature(h.config.AppSecret, method, "*/*", contentMD5, "application/json", date, headers, urlPath)
|
||||
|
||||
// 创建HTTP请求
|
||||
url := h.config.ServerURL + urlPath
|
||||
// 实际请求url用encode后的urlPath
|
||||
encodedURLPath := encodeURLQueryParams(urlPath)
|
||||
url := h.config.ServerURL + encodedURLPath
|
||||
req, err := http.NewRequest(method, url, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
|
||||
@@ -69,12 +69,11 @@ func (h *HTTPClient) Request(method, urlPath string, body []byte) ([]byte, error
|
||||
// 设置请求头
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Content-MD5", contentMD5)
|
||||
req.Header.Set("Date", date)
|
||||
// req.Header.Set("Date", date)
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("X-Tsign-Open-App-Id", h.config.AppID)
|
||||
req.Header.Set("X-Tsign-Open-Auth-Mode", "Signature")
|
||||
req.Header.Set("X-Tsign-Open-Ca-Timestamp", timestamp)
|
||||
req.Header.Set("X-Tsign-Open-Nonce", nonce)
|
||||
req.Header.Set("X-Tsign-Open-Ca-Signature", signature)
|
||||
|
||||
// 发送请求
|
||||
@@ -197,3 +196,24 @@ func sortURLQueryParams(urlPath string) string {
|
||||
}
|
||||
return basePath
|
||||
}
|
||||
|
||||
// encodeURLQueryParams 对urlPath中的query参数值进行encode
|
||||
func encodeURLQueryParams(urlPath string) string {
|
||||
if !strings.Contains(urlPath, "?") {
|
||||
return urlPath
|
||||
}
|
||||
parts := strings.SplitN(urlPath, "?", 2)
|
||||
basePath := parts[0]
|
||||
queryString := parts[1]
|
||||
values, err := url.ParseQuery(queryString)
|
||||
if err != nil {
|
||||
return urlPath
|
||||
}
|
||||
var encodedPairs []string
|
||||
for key, vals := range values {
|
||||
for _, val := range vals {
|
||||
encodedPairs = append(encodedPairs, key+"="+url.QueryEscape(val))
|
||||
}
|
||||
}
|
||||
return basePath + "?" + strings.Join(encodedPairs, "&")
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ func (s *OrgAuthService) GetAuthURL(req *OrgAuthRequest) (string, string, string
|
||||
},
|
||||
},
|
||||
ClientType: ClientTypeAll,
|
||||
RedirectConfig: &RedirectConfig{
|
||||
RedirectUrl: s.config.Auth.RedirectUrl,
|
||||
},
|
||||
}
|
||||
|
||||
// 序列化请求数据
|
||||
|
||||
@@ -43,7 +43,7 @@ func (s *SignFlowService) Create(req *CreateSignFlowRequest) (string, error) {
|
||||
Docs: []DocInfo{
|
||||
{
|
||||
FileId: req.FileID,
|
||||
FileName: "天远数据API合作协议.pdf",
|
||||
FileName: s.config.Contract.Name,
|
||||
},
|
||||
},
|
||||
SignFlowConfig: s.buildSignFlowConfig(),
|
||||
@@ -141,9 +141,9 @@ func (s *SignFlowService) buildPartyASigner(fileID string) SignerInfo {
|
||||
AutoSign: true,
|
||||
SignFieldStyle: SignFieldStyleNormal,
|
||||
SignFieldPosition: &SignFieldPosition{
|
||||
PositionPage: "1",
|
||||
PositionPage: "8",
|
||||
PositionX: 200,
|
||||
PositionY: 200,
|
||||
PositionY: 430,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -188,9 +188,9 @@ func (s *SignFlowService) buildPartyBSigner(fileID, signerAccount, signerName, t
|
||||
AutoSign: false,
|
||||
SignFieldStyle: SignFieldStyleNormal,
|
||||
SignFieldPosition: &SignFieldPosition{
|
||||
PositionPage: "1",
|
||||
PositionX: 458,
|
||||
PositionY: 200,
|
||||
PositionPage: "8",
|
||||
PositionX: 450,
|
||||
PositionY: 430,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -201,9 +201,9 @@ func (s *SignFlowService) buildPartyBSigner(fileID, signerAccount, signerName, t
|
||||
// buildSignFlowConfig 构建签署流程配置
|
||||
func (s *SignFlowService) buildSignFlowConfig() SignFlowConfig {
|
||||
return SignFlowConfig{
|
||||
SignFlowTitle: "天远数据API合作协议签署",
|
||||
SignFlowExpireTime: calculateExpireTime(7), // 7天后过期
|
||||
AutoFinish: true, // 所有签署方完成后自动完结
|
||||
SignFlowTitle: s.config.Contract.Name,
|
||||
SignFlowExpireTime: calculateExpireTime(s.config.Contract.ExpireDays),
|
||||
AutoFinish: s.config.Sign.AutoFinish,
|
||||
AuthConfig: &AuthConfig{
|
||||
PsnAvailableAuthModes: []string{AuthModeMobile3},
|
||||
WillingnessAuthModes: []string{WillingnessAuthSMS},
|
||||
@@ -211,5 +211,8 @@ func (s *SignFlowService) buildSignFlowConfig() SignFlowConfig {
|
||||
ContractConfig: &ContractConfig{
|
||||
AllowToRescind: false,
|
||||
},
|
||||
RedirectConfig: &RedirectConfig{
|
||||
RedirectUrl: s.config.Sign.RedirectUrl,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func (s *TemplateService) Fill(components []Component) (*FillTemplate, error) {
|
||||
fmt.Println("开始填写模板生成文件...")
|
||||
|
||||
// 生成带时间戳的文件名
|
||||
fileName := generateFileName("天远数据API合作协议", "pdf")
|
||||
fileName := generateFileName(s.config.Contract.Name, "pdf")
|
||||
|
||||
// 构建请求数据
|
||||
requestData := FillTemplateRequest{
|
||||
|
||||
@@ -5,10 +5,40 @@ import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// appendSignDataString 拼接待签名字符串
|
||||
func appendSignDataString(httpMethod, accept, contentMD5, contentType, date, headers, pathAndParameters string) string {
|
||||
if accept == "" {
|
||||
accept = "*/*"
|
||||
}
|
||||
if contentType == "" {
|
||||
contentType = "application/json; charset=UTF-8"
|
||||
}
|
||||
// 前四项
|
||||
signStr := httpMethod + "\n" + accept + "\n" + contentMD5 + "\n" + contentType + "\n"
|
||||
// 处理 date
|
||||
if date == "" {
|
||||
signStr += "\n"
|
||||
} else {
|
||||
signStr += date + "\n"
|
||||
}
|
||||
// 处理 headers
|
||||
if headers == "" {
|
||||
signStr += pathAndParameters
|
||||
} else {
|
||||
signStr += headers + "\n" + pathAndParameters
|
||||
}
|
||||
return signStr
|
||||
}
|
||||
|
||||
// generateSignature 生成e签宝API请求签名
|
||||
// 使用HMAC-SHA256算法对请求参数进行签名
|
||||
//
|
||||
@@ -24,8 +54,8 @@ import (
|
||||
//
|
||||
// 返回: Base64编码的签名字符串
|
||||
func generateSignature(appSecret, httpMethod, accept, contentMD5, contentType, date, headers, pathAndParameters string) string {
|
||||
// 构建待签名字符串,按照e签宝API规范拼接
|
||||
signStr := httpMethod + "\n" + accept + "\n" + contentMD5 + "\n" + contentType + "\n" + date + "\n" + headers + pathAndParameters
|
||||
// 构建待签名字符串,按照e签宝API规范拼接(兼容Python实现细节)
|
||||
signStr := appendSignDataString(httpMethod, accept, contentMD5, contentType, date, headers, pathAndParameters)
|
||||
|
||||
// 使用HMAC-SHA256计算签名
|
||||
h := hmac.New(sha256.New, []byte(appSecret))
|
||||
@@ -102,3 +132,66 @@ func generateFileName(baseName, extension string) string {
|
||||
func calculateExpireTime(days int) int64 {
|
||||
return time.Now().AddDate(0, 0, days).UnixMilli()
|
||||
}
|
||||
|
||||
// verifySignature 验证e签宝回调签名
|
||||
func VerifySignature(callbackData interface{}, headers map[string]string, queryParams map[string]string, appSecret string) error {
|
||||
// 1. 获取签名相关参数
|
||||
signature, ok := headers["X-Tsign-Open-Signature"]
|
||||
if !ok {
|
||||
return fmt.Errorf("缺少签名头: X-Tsign-Open-Signature")
|
||||
}
|
||||
|
||||
timestamp, ok := headers["X-Tsign-Open-Timestamp"]
|
||||
if !ok {
|
||||
return fmt.Errorf("缺少时间戳头: X-Tsign-Open-Timestamp")
|
||||
}
|
||||
|
||||
// 2. 构建查询参数字符串
|
||||
var queryKeys []string
|
||||
for key := range queryParams {
|
||||
queryKeys = append(queryKeys, key)
|
||||
}
|
||||
sort.Strings(queryKeys) // 按ASCII码升序排序
|
||||
|
||||
var queryValues []string
|
||||
for _, key := range queryKeys {
|
||||
queryValues = append(queryValues, queryParams[key])
|
||||
}
|
||||
queryString := strings.Join(queryValues, "")
|
||||
|
||||
// 3. 获取请求体数据
|
||||
bodyData, err := getRequestBodyString(callbackData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取请求体数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 构建验签数据
|
||||
data := timestamp + queryString + bodyData
|
||||
|
||||
// 5. 计算签名
|
||||
expectedSignature := calculateSignature(data, appSecret)
|
||||
|
||||
// 6. 比较签名
|
||||
if strings.ToLower(expectedSignature) != strings.ToLower(signature) {
|
||||
return fmt.Errorf("签名验证失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateSignature 计算HMAC-SHA256签名
|
||||
func calculateSignature(data, secret string) string {
|
||||
h := hmac.New(sha256.New, []byte(secret))
|
||||
h.Write([]byte(data))
|
||||
return strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
|
||||
}
|
||||
|
||||
// getRequestBodyString 获取请求体字符串
|
||||
func getRequestBodyString(callbackData interface{}) (string, error) {
|
||||
// 将map转换为JSON字符串
|
||||
jsonBytes, err := json.Marshal(callbackData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("JSON序列化失败: %w", err)
|
||||
}
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user