token生成成功
This commit is contained in:
@@ -45,7 +45,7 @@ FILE_UPLOAD_INSECURE_SKIP_VERIFY=true
|
|||||||
# 政务云文件上传限频:两次 POST 最短间隔(默认 10s;0 关闭;支持 10s 或纯数字秒)
|
# 政务云文件上传限频:两次 POST 最短间隔(默认 10s;0 关闭;支持 10s 或纯数字秒)
|
||||||
FILE_UPLOAD_MIN_INTERVAL=10s
|
FILE_UPLOAD_MIN_INTERVAL=10s
|
||||||
|
|
||||||
# uploadToken Base64:padded(默认,含=)| raw(无填充,联调仍 403 可试)
|
# uploadToken Base64(FileAuth.java):padded(默认,含=)| raw(无填充,联调仍 403 可试)
|
||||||
# FILEAUTH_B64=padded
|
# FILEAUTH_B64=padded
|
||||||
|
|
||||||
# 应用日志(默认 {程序目录}/log/transit/,含 pull/push/app 按日文件;每次执行有 BEGIN/END 分隔符)
|
# 应用日志(默认 {程序目录}/log/transit/,含 pull/push/app 按日文件;每次执行有 BEGIN/END 分隔符)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# FileAuth 上传凭证(Ver2.0 §2.3.3)
|
# FileAuth 上传凭证(FileAuth.java)
|
||||||
|
|
||||||
生产仅使用 **§2.3.3**(HMAC 对 **encodedPutPolicy** 字符串)。FileAuth.java 旧算法(HMAC 对 policy JSON)**不可用**,不再维护。
|
生产与 [`docs/FileAuth.java`](FileAuth.java) 中 `createUploadToken` **完全一致**(HMAC 对 **policy JSON 明文**)。
|
||||||
|
|
||||||
## 生产 putPolicy(upload profile)
|
## 生产 putPolicy
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"scope":"<bucket>","deadline":<unix>}
|
{"scope":"<bucket>","deadline":<unix>}
|
||||||
@@ -10,29 +10,24 @@
|
|||||||
|
|
||||||
- `scope`:**仅 bucket 名**(如 `other-doc`),**不要** `bucket:filename`
|
- `scope`:**仅 bucket 名**(如 `other-doc`),**不要** `bucket:filename`
|
||||||
- **无** `returnBody`
|
- **无** `returnBody`
|
||||||
- HTTP 上传为 **PDF 二进制体**(`Content-Type: application/pdf`),**不使用** multipart;与 scope 无关
|
- HTTP 上传为 **PDF 二进制体**(`Content-Type: application/pdf`),**不使用** multipart
|
||||||
|
|
||||||
## 五步
|
## 生成步骤
|
||||||
|
|
||||||
| 步 | 说明 |
|
| 步 | 说明 |
|
||||||
|----|------|
|
|----|------|
|
||||||
| 1 | 构造 putPolicy(upload profile 见上) |
|
| 1 | 构造 putPolicy JSON(字段顺序:`scope` → `deadline`) |
|
||||||
| 2 | `encodedPutPolicy = urlsafe_base64(putPolicy)` |
|
| 2 | `sign = urlsafe_base64( HMAC-SHA1(secret, UTF-8(policyJSON)) )` |
|
||||||
| 3 | `sign = HMAC-SHA1(secret, UTF-8(encodedPutPolicy))` |
|
| 3 | `policyB64 = urlsafe_base64( UTF-8(policyJSON) )` |
|
||||||
| 4 | `encodedSign = urlsafe_base64(sign)` |
|
| 4 | `uploadToken = AccessKey + ':' + sign + ':' + policyB64` |
|
||||||
| 5 | `uploadToken = AccessKey + ':' + encodedSign + ':' + encodedPutPolicy` |
|
|
||||||
|
|
||||||
## Base64 填充
|
## Base64 填充
|
||||||
|
|
||||||
| 环境变量 | 说明 |
|
| 环境变量 | 说明 |
|
||||||
|----------|------|
|
|----------|------|
|
||||||
| `FILEAUTH_B64=padded`(默认) | URL-safe Base64 **含 `=`**(与文档教学样例一致) |
|
| `FILEAUTH_B64=padded`(默认) | URL-safe Base64 **含 `=`**(对应 Java `Base64.URL_SAFE \| NO_WRAP`) |
|
||||||
| `FILEAUTH_B64=raw` | 无填充(联调仍 403 时可试) |
|
| `FILEAUTH_B64=raw` | 无填充(联调仍 403 时可试) |
|
||||||
|
|
||||||
## 文档样例(doc profile,仅单测)
|
|
||||||
|
|
||||||
教学样例含 `returnBody` 与 `scope=my-bucket:sunflower.jpg`,见 `auth_ver233_test.go` / `FileAuthServiceTest::test_create_upload_token_doc_golden`。
|
|
||||||
|
|
||||||
## 错误对照
|
## 错误对照
|
||||||
|
|
||||||
| HTTP body | 含义 |
|
| HTTP body | 含义 |
|
||||||
@@ -42,10 +37,10 @@
|
|||||||
|
|
||||||
## Web 调试
|
## Web 调试
|
||||||
|
|
||||||
- `/fileauth`:生成 upload profile token;`?doc=1` 为文档黄金样例
|
- `/fileauth`:生成 upload profile token
|
||||||
- `/upload`:A=Runner 默认 token;B=本次上传 `tokenInfo`(`scope` 应为 bucket)
|
- `/upload`:A=Runner 默认 token;B=本次上传 `tokenInfo`(`scope` 应为 bucket)
|
||||||
|
|
||||||
实现:[`internal/fileauth`](../internal/fileauth/ver233.go)
|
实现:[`internal/fileauth/upload.go`](../internal/fileauth/upload.go)
|
||||||
|
|
||||||
## 上传频率
|
## 上传频率
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Package fileauth 实现监管文件服务上传凭证(Ver2.0 §2.3.3,生产 scope=仅 bucket)。
|
// Package fileauth 实现监管文件服务上传凭证(与 docs/FileAuth.java 一致,生产 scope=仅 bucket)。
|
||||||
package fileauth
|
package fileauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
const defaultUploadExpires = 3600 * time.Second
|
const defaultUploadExpires = 3600 * time.Second
|
||||||
|
|
||||||
// CreateUploadToken 生成上传凭证(§2.3.3 upload profile,scope=仅 bucket)。
|
// CreateUploadToken 生成上传凭证(FileAuth.java upload profile,scope=仅 bucket)。
|
||||||
func CreateUploadToken(accessKey, secret, bucket string, expires time.Duration) (string, error) {
|
func CreateUploadToken(accessKey, secret, bucket string, expires time.Duration) (string, error) {
|
||||||
if expires <= 0 {
|
if expires <= 0 {
|
||||||
expires = defaultUploadExpires
|
expires = defaultUploadExpires
|
||||||
|
|||||||
88
internal/fileauth/auth_test.go
Normal file
88
internal/fileauth/auth_test.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package fileauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
SetB64Mode(B64ModePadded)
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalPutPolicyUpload_productionShape(t *testing.T) {
|
||||||
|
got, err := MarshalPutPolicyUpload("other-doc", 1735689600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := `{"scope":"other-doc","deadline":1735689600}`
|
||||||
|
if string(got) != want {
|
||||||
|
t.Fatalf("upload policy mismatch\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildUploadTokenParts_javaAlgorithm(t *testing.T) {
|
||||||
|
const (
|
||||||
|
accessKey = "accessKey"
|
||||||
|
secret = "secret"
|
||||||
|
bucket = "other-doc"
|
||||||
|
deadline = int64(1735689600)
|
||||||
|
policyJSON = `{"scope":"other-doc","deadline":1735689600}`
|
||||||
|
)
|
||||||
|
|
||||||
|
mac := hmac.New(sha1.New, []byte(secret))
|
||||||
|
_, _ = mac.Write([]byte(policyJSON))
|
||||||
|
wantSign := base64.URLEncoding.EncodeToString(mac.Sum(nil))
|
||||||
|
wantPolicyB64 := base64.URLEncoding.EncodeToString([]byte(policyJSON))
|
||||||
|
wantToken := accessKey + ":" + wantSign + ":" + wantPolicyB64
|
||||||
|
|
||||||
|
parts, err := BuildUploadTokenPartsForUpload(accessKey, secret, bucket, deadline)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if parts.UploadToken != wantToken {
|
||||||
|
t.Fatalf("token mismatch\ngot: %s\nwant: %s", parts.UploadToken, wantToken)
|
||||||
|
}
|
||||||
|
if parts.SignInput != policyJSON {
|
||||||
|
t.Fatalf("signInput=%q want policy JSON", parts.SignInput)
|
||||||
|
}
|
||||||
|
if parts.EncodedSign != wantSign {
|
||||||
|
t.Fatalf("encodedSign=%q want %q", parts.EncodedSign, wantSign)
|
||||||
|
}
|
||||||
|
if parts.EncodedPutPolicy != wantPolicyB64 {
|
||||||
|
t.Fatalf("policyB64=%q want %q", parts.EncodedPutPolicy, wantPolicyB64)
|
||||||
|
}
|
||||||
|
if parts.Algorithm != algorithmJavaUpload {
|
||||||
|
t.Fatalf("algorithm=%q", parts.Algorithm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateUploadTokenAtFile_ignoresFilenameInScope(t *testing.T) {
|
||||||
|
parts, err := BuildUploadTokenPartsAtFile("ak", "sec", "other-doc", "different.pdf", 1735689600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if parts.PolicyJSON != `{"scope":"other-doc","deadline":1735689600}` {
|
||||||
|
t.Fatalf("filename must not affect scope: %q", parts.PolicyJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadToken_paddedVsRawB64(t *testing.T) {
|
||||||
|
SetB64Mode(B64ModePadded)
|
||||||
|
padded, err := BuildUploadTokenPartsForUpload("ak", "sec", "b", 1735689600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
SetB64Mode(B64ModeRaw)
|
||||||
|
raw, err := BuildUploadTokenPartsForUpload("ak", "sec", "b", 1735689600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
SetB64Mode(B64ModePadded)
|
||||||
|
if padded.UploadToken == raw.UploadToken {
|
||||||
|
t.Fatal("padded vs raw token should differ")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
package fileauth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
SetB64Mode(B64ModePadded)
|
|
||||||
os.Exit(m.Run())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarshalPutPolicyDoc_docExample(t *testing.T) {
|
|
||||||
got, err := MarshalPutPolicyDoc("my-bucket:sunflower.jpg", 1451491200)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if string(got) != DocExamplePolicyJSON {
|
|
||||||
t.Fatalf("putPolicy mismatch\ngot: %s\nwant: %s", got, DocExamplePolicyJSON)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarshalPutPolicyUpload_scopeBucketOnly(t *testing.T) {
|
|
||||||
got, err := MarshalPutPolicyUpload("other-doc", 1735689600)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
want := `{"scope":"other-doc","deadline":1735689600}`
|
|
||||||
if string(got) != want {
|
|
||||||
t.Fatalf("upload policy mismatch\ngot: %s\nwant: %s", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVer233_encodedPutPolicy_docExample(t *testing.T) {
|
|
||||||
got := urlSafeBase64Ver233([]byte(DocExamplePolicyJSON))
|
|
||||||
if got != DocExampleEncodedPutPolicy {
|
|
||||||
t.Fatalf("encodedPutPolicy mismatch\ngot: %s\nwant: %s", got, DocExampleEncodedPutPolicy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVer233_signHex_docExample(t *testing.T) {
|
|
||||||
hex := HMACSignHex("MY_SECRET_KEY", DocExampleEncodedPutPolicy)
|
|
||||||
if hex != DocExampleSignHex {
|
|
||||||
t.Fatalf("sign hex mismatch\ngot: %s\nwant: %s", hex, DocExampleSignHex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVer233_encodedSign_docExample(t *testing.T) {
|
|
||||||
sign, err := signPolicyVer233("MY_SECRET_KEY", DocExampleEncodedPutPolicy)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if sign != DocExampleEncodedSign {
|
|
||||||
t.Fatalf("encodedSign mismatch\ngot: %s\nwant: %s", sign, DocExampleEncodedSign)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVer233_fullToken_docExample(t *testing.T) {
|
|
||||||
parts, err := BuildUploadTokenFromPolicy("MY_ACCESS_KEY", "MY_SECRET_KEY", []byte(DocExamplePolicyJSON), "ver233-doc")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if parts.UploadToken != DocExampleUploadToken {
|
|
||||||
t.Fatalf("token mismatch\ngot: %s\nwant: %s", parts.UploadToken, DocExampleUploadToken)
|
|
||||||
}
|
|
||||||
if parts.SignInput != DocExampleEncodedPutPolicy {
|
|
||||||
t.Fatal("signInput should be encodedPutPolicy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildUploadTokenPartsForUpload_productionShape(t *testing.T) {
|
|
||||||
parts, err := BuildUploadTokenPartsForUpload("ak", "sec", "other-doc", 1735689600)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if parts.Algorithm != "ver233-upload" {
|
|
||||||
t.Fatalf("algorithm=%q", parts.Algorithm)
|
|
||||||
}
|
|
||||||
if parts.SignInput != parts.EncodedPutPolicy {
|
|
||||||
t.Fatal("ver233 signInput must be encodedPutPolicy")
|
|
||||||
}
|
|
||||||
if parts.PolicyJSON != `{"scope":"other-doc","deadline":1735689600}` {
|
|
||||||
t.Fatalf("policy=%q", parts.PolicyJSON)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateUploadTokenAtFile_ignoresFilenameInScope(t *testing.T) {
|
|
||||||
parts, err := BuildUploadTokenPartsAtFile("ak", "sec", "other-doc", "different.pdf", 1735689600)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if parts.PolicyJSON != `{"scope":"other-doc","deadline":1735689600}` {
|
|
||||||
t.Fatalf("filename must not affect scope: %q", parts.PolicyJSON)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVer233_uploadRawB64_differsFromPadded(t *testing.T) {
|
|
||||||
SetB64Mode(B64ModePadded)
|
|
||||||
padded, err := BuildUploadTokenPartsForUpload("ak", "sec", "b", 1735689600)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
SetB64Mode(B64ModeRaw)
|
|
||||||
raw, err := BuildUploadTokenPartsForUpload("ak", "sec", "b", 1735689600)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
SetB64Mode(B64ModePadded)
|
|
||||||
if padded.UploadToken == raw.UploadToken {
|
|
||||||
t.Fatal("padded vs raw token should differ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,12 +7,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UploadTokenParts 上传凭证分步结果(监管 2.3:AccessKey:encodedSign:encodedPutPolicy)。
|
// UploadTokenParts 上传凭证分步结果(FileAuth.java:AccessKey:encodedSign:policyB64)。
|
||||||
type UploadTokenParts struct {
|
type UploadTokenParts struct {
|
||||||
AccessKey string
|
AccessKey string
|
||||||
PolicyJSON string
|
PolicyJSON string
|
||||||
EncodedPutPolicy string
|
EncodedPutPolicy string // 第三段:policy JSON 的 URL-safe Base64
|
||||||
SignInput string // §2.3.3:encodedPutPolicy 字符串
|
SignInput string // HMAC 输入:policy JSON 明文
|
||||||
EncodedSign string
|
EncodedSign string
|
||||||
UploadToken string
|
UploadToken string
|
||||||
DeadlineUnix int64
|
DeadlineUnix int64
|
||||||
@@ -27,41 +27,7 @@ type FormatCheck struct {
|
|||||||
Detail string `json:"detail"`
|
Detail string `json:"detail"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocExample 监管 2.3 教学示例(含 returnBody,仅格式对照,非 transit 生产 policy)。
|
// BuildUploadTokenParts 分步生成上传凭证(生产 upload profile,scope=仅 bucket)。
|
||||||
type DocExample struct {
|
|
||||||
Note string `json:"note"`
|
|
||||||
UploadTokenSample string `json:"uploadTokenSample"`
|
|
||||||
AccessKeyPart string `json:"accessKeyPart"`
|
|
||||||
EncodedSignPart string `json:"encodedSignPart"`
|
|
||||||
EncodedPolicyPart string `json:"encodedPolicyPart"`
|
|
||||||
PolicyJSONDecoded string `json:"policyJsonDecoded"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DocExampleRegulatory 内置监管文档第 5 步示例 token(含 returnBody 的完整 putPolicy)。
|
|
||||||
const DocExampleRegulatory = "MY_ACCESS_KEY:wQ4ofysef1R7IKnrziqtomqyDvI=:eyJzY29wZSI6Im15LWJ1Y2tldDpzdW5mbG93ZXIuanBnIiwiZGVhZGxpbmUiOjE0NTE0OTEyMDAsInJldHVybkJvZHkiOiJ7XCJuYW1lXCI6JChmbmFtZSksXCJzaXplXCI6JChmc2l6ZSksXCJ3XCI6JChpbWFnZUluZm8ud2lkdGgpLFwiaFwiOiQoaW1hZ2VJbmZvLmhlaWdodCksXCJoYXNoXCI6JChldGFnKX0ifQ=="
|
|
||||||
|
|
||||||
const docExamplePolicyJSON = `{"scope":"my-bucket:sunflower.jpg","deadline":1451491200,"returnBody":"{\"name\":$(fname),\"size\":$(fsize),\"w\":$(imageInfo.width),\"h\":$(imageInfo.height),\"hash\":$(etag)}"}`
|
|
||||||
|
|
||||||
// RegulatoryDocExample 返回文档教学示例的拆分与解码(只读对照)。
|
|
||||||
func RegulatoryDocExample() DocExample {
|
|
||||||
ak, sign, pol, ok := SplitUploadToken(DocExampleRegulatory)
|
|
||||||
policyDecoded := ""
|
|
||||||
if ok {
|
|
||||||
if s, err := DecodePutPolicyB64(pol); err == nil {
|
|
||||||
policyDecoded = s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return DocExample{
|
|
||||||
Note: "监管 §2.3.3 教学示例(doc profile);生产为 upload profile(scope 仅 bucket,无 returnBody)",
|
|
||||||
UploadTokenSample: DocExampleRegulatory,
|
|
||||||
AccessKeyPart: ak,
|
|
||||||
EncodedSignPart: sign,
|
|
||||||
EncodedPolicyPart: pol,
|
|
||||||
PolicyJSONDecoded: policyDecoded,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildUploadTokenParts 分步生成上传凭证(§2.3.3 upload profile,scope=仅 bucket)。
|
|
||||||
func BuildUploadTokenParts(accessKey, secret, bucket string, deadlineUnix int64) (UploadTokenParts, error) {
|
func BuildUploadTokenParts(accessKey, secret, bucket string, deadlineUnix int64) (UploadTokenParts, error) {
|
||||||
return BuildUploadTokenPartsForUpload(accessKey, secret, bucket, deadlineUnix)
|
return BuildUploadTokenPartsForUpload(accessKey, secret, bucket, deadlineUnix)
|
||||||
}
|
}
|
||||||
@@ -94,7 +60,7 @@ func ValidateUploadTokenFormat(token, expectedAccessKey string) (allOK bool, che
|
|||||||
{
|
{
|
||||||
ID: "three_parts",
|
ID: "three_parts",
|
||||||
OK: ok,
|
OK: ok,
|
||||||
Detail: "uploadToken = AccessKey + ':' + encodedSign + ':' + encodedPutPolicy(按前两处冒号拆分)",
|
Detail: "uploadToken = AccessKey + ':' + encodedSign + ':' + policyB64(按前两处冒号拆分)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "access_key_non_empty",
|
ID: "access_key_non_empty",
|
||||||
@@ -109,7 +75,7 @@ func ValidateUploadTokenFormat(token, expectedAccessKey string) (allOK bool, che
|
|||||||
{
|
{
|
||||||
ID: "encoded_policy_non_empty",
|
ID: "encoded_policy_non_empty",
|
||||||
OK: ok && pol != "",
|
OK: ok && pol != "",
|
||||||
Detail: "第 3 段 encodedPutPolicy(URL-safe Base64 policy JSON)非空",
|
Detail: "第 3 段 policyB64(URL-safe Base64 policy JSON)非空",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if expectedAccessKey != "" {
|
if expectedAccessKey != "" {
|
||||||
@@ -137,7 +103,7 @@ func ValidateUploadTokenFormat(token, expectedAccessKey string) (allOK bool, che
|
|||||||
return allOK, checks
|
return allOK, checks
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodePutPolicyB64 解码 encodedPutPolicy 为 JSON 字符串(美化缩进)。
|
// DecodePutPolicyB64 解码 policyB64 为 JSON 字符串(美化缩进)。
|
||||||
func DecodePutPolicyB64(encoded string) (string, error) {
|
func DecodePutPolicyB64(encoded string) (string, error) {
|
||||||
encoded = strings.TrimSpace(encoded)
|
encoded = strings.TrimSpace(encoded)
|
||||||
if encoded == "" {
|
if encoded == "" {
|
||||||
@@ -145,7 +111,6 @@ func DecodePutPolicyB64(encoded string) (string, error) {
|
|||||||
}
|
}
|
||||||
raw, err := base64.RawURLEncoding.DecodeString(encoded)
|
raw, err := base64.RawURLEncoding.DecodeString(encoded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 兼容带 padding 的标准 url-safe
|
|
||||||
raw, err = base64.URLEncoding.DecodeString(encoded)
|
raw, err = base64.URLEncoding.DecodeString(encoded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -161,8 +126,3 @@ func DecodePutPolicyB64(encoded string) (string, error) {
|
|||||||
}
|
}
|
||||||
return string(out), nil
|
return string(out), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeDocExamplePolicy 返回内置文档示例 policy 明文(便于测试)。
|
|
||||||
func DecodeDocExamplePolicy() string {
|
|
||||||
return docExamplePolicyJSON
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -32,15 +32,15 @@ func TestBuildUploadTokenParts_matchesCreateUploadTokenAt(t *testing.T) {
|
|||||||
if strings.Contains(parts.PolicyJSON, "returnBody") {
|
if strings.Contains(parts.PolicyJSON, "returnBody") {
|
||||||
t.Fatalf("upload profile must not include returnBody: %q", parts.PolicyJSON)
|
t.Fatalf("upload profile must not include returnBody: %q", parts.PolicyJSON)
|
||||||
}
|
}
|
||||||
if parts.Algorithm != "ver233-upload" {
|
if parts.Algorithm != algorithmJavaUpload {
|
||||||
t.Fatalf("algorithm=%q", parts.Algorithm)
|
t.Fatalf("algorithm=%q", parts.Algorithm)
|
||||||
}
|
}
|
||||||
if parts.SignInput != parts.EncodedPutPolicy {
|
if parts.SignInput != parts.PolicyJSON {
|
||||||
t.Fatal("ver233 signInput must be encodedPutPolicy")
|
t.Fatal("signInput must be policy JSON")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateUploadTokenFormat_golden(t *testing.T) {
|
func TestValidateUploadTokenFormat_production(t *testing.T) {
|
||||||
parts, err := BuildUploadTokenParts("accessKey", "secret", "other-doc", 1735689600)
|
parts, err := BuildUploadTokenParts("accessKey", "secret", "other-doc", 1735689600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -50,36 +50,3 @@ func TestValidateUploadTokenFormat_golden(t *testing.T) {
|
|||||||
t.Fatalf("validate failed: %+v", checks)
|
t.Fatalf("validate failed: %+v", checks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSplitUploadToken_docExample(t *testing.T) {
|
|
||||||
ak, sign, pol, ok := SplitUploadToken(DocExampleRegulatory)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("split failed")
|
|
||||||
}
|
|
||||||
if ak != "MY_ACCESS_KEY" {
|
|
||||||
t.Fatalf("ak=%q", ak)
|
|
||||||
}
|
|
||||||
if sign != "wQ4ofysef1R7IKnrziqtomqyDvI=" {
|
|
||||||
t.Fatalf("sign=%q", sign)
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(pol, "eyJ") {
|
|
||||||
t.Fatalf("policy=%q", pol)
|
|
||||||
}
|
|
||||||
decoded, err := DecodePutPolicyB64(pol)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(decoded, "returnBody") {
|
|
||||||
t.Fatalf("decoded missing returnBody: %s", decoded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegulatoryDocExample(t *testing.T) {
|
|
||||||
ex := RegulatoryDocExample()
|
|
||||||
if ex.AccessKeyPart != "MY_ACCESS_KEY" {
|
|
||||||
t.Fatalf("ak=%q", ex.AccessKeyPart)
|
|
||||||
}
|
|
||||||
if ex.PolicyJSONDecoded == "" {
|
|
||||||
t.Fatal("empty decoded policy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package fileauth
|
|
||||||
|
|
||||||
// PolicyProfile putPolicy 形态:upload=生产(scope 仅 bucket);doc=监管教学样例单测。
|
|
||||||
type PolicyProfile string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProfileUpload PolicyProfile = "upload"
|
|
||||||
ProfileDoc PolicyProfile = "doc"
|
|
||||||
)
|
|
||||||
132
internal/fileauth/upload.go
Normal file
132
internal/fileauth/upload.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package fileauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const algorithmJavaUpload = "fileauth-java-upload"
|
||||||
|
|
||||||
|
// MarshalPutPolicyUpload 生产 putPolicy:scope=仅 bucket,无 returnBody(与 FileAuth.java HashMap 字段顺序一致)。
|
||||||
|
func MarshalPutPolicyUpload(bucket string, deadlineUnix int64) ([]byte, error) {
|
||||||
|
bucket = strings.TrimSpace(bucket)
|
||||||
|
if bucket == "" {
|
||||||
|
return nil, errors.New("empty bucket")
|
||||||
|
}
|
||||||
|
if deadlineUnix <= 0 {
|
||||||
|
return nil, errors.New("invalid deadline")
|
||||||
|
}
|
||||||
|
scopeJSON, err := json.Marshal(bucket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []byte(`{"scope":` + string(scopeJSON) + `,"deadline":` + strconv.FormatInt(deadlineUnix, 10) + `}`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// signPolicyJava 与 FileAuth.java sign(byte[]) 一致:HMAC-SHA1(secret, policyJSON) → URL-safe Base64。
|
||||||
|
func signPolicyJava(secret string, policyJSON []byte) (string, error) {
|
||||||
|
mac := hmac.New(sha1.New, []byte(secret))
|
||||||
|
if _, err := mac.Write(policyJSON); err != nil {
|
||||||
|
return "", fmt.Errorf("hmac: %w", err)
|
||||||
|
}
|
||||||
|
return encodeURLSafe(mac.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildUploadTokenFromPolicy 按 FileAuth.java createUploadToken 生成 token。
|
||||||
|
func BuildUploadTokenFromPolicy(accessKey, secret string, putPolicyJSON []byte, algorithm string) (UploadTokenParts, error) {
|
||||||
|
accessKey = strings.TrimSpace(accessKey)
|
||||||
|
secret = strings.TrimSpace(secret)
|
||||||
|
if accessKey == "" || secret == "" {
|
||||||
|
return UploadTokenParts{}, errors.New("empty key or secret")
|
||||||
|
}
|
||||||
|
if len(putPolicyJSON) == 0 {
|
||||||
|
return UploadTokenParts{}, errors.New("empty putPolicy")
|
||||||
|
}
|
||||||
|
if algorithm == "" {
|
||||||
|
algorithm = algorithmJavaUpload
|
||||||
|
}
|
||||||
|
|
||||||
|
policyJSON := string(putPolicyJSON)
|
||||||
|
sign, err := signPolicyJava(secret, putPolicyJSON)
|
||||||
|
if err != nil {
|
||||||
|
return UploadTokenParts{}, err
|
||||||
|
}
|
||||||
|
policyB64 := encodeURLSafe(putPolicyJSON)
|
||||||
|
token := accessKey + ":" + sign + ":" + policyB64
|
||||||
|
|
||||||
|
var policy struct {
|
||||||
|
Deadline int64 `json:"deadline"`
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(putPolicyJSON, &policy)
|
||||||
|
|
||||||
|
return UploadTokenParts{
|
||||||
|
AccessKey: accessKey,
|
||||||
|
PolicyJSON: policyJSON,
|
||||||
|
EncodedPutPolicy: policyB64,
|
||||||
|
SignInput: policyJSON,
|
||||||
|
EncodedSign: sign,
|
||||||
|
UploadToken: token,
|
||||||
|
DeadlineUnix: policy.Deadline,
|
||||||
|
Algorithm: algorithm,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildUploadTokenPartsForUpload 生产路径:scope=仅 bucket。
|
||||||
|
func BuildUploadTokenPartsForUpload(accessKey, secret, bucket string, deadlineUnix int64) (UploadTokenParts, error) {
|
||||||
|
accessKey = strings.TrimSpace(accessKey)
|
||||||
|
secret = strings.TrimSpace(secret)
|
||||||
|
bucket = strings.TrimSpace(bucket)
|
||||||
|
if accessKey == "" || secret == "" {
|
||||||
|
return UploadTokenParts{}, errors.New("empty key or secret")
|
||||||
|
}
|
||||||
|
if bucket == "" {
|
||||||
|
return UploadTokenParts{}, errors.New("empty bucket")
|
||||||
|
}
|
||||||
|
if deadlineUnix <= 0 {
|
||||||
|
return UploadTokenParts{}, errors.New("invalid deadline")
|
||||||
|
}
|
||||||
|
jsonPolicy, err := MarshalPutPolicyUpload(bucket, deadlineUnix)
|
||||||
|
if err != nil {
|
||||||
|
return UploadTokenParts{}, err
|
||||||
|
}
|
||||||
|
parts, err := BuildUploadTokenFromPolicy(accessKey, secret, jsonPolicy, algorithmJavaUpload)
|
||||||
|
if err != nil {
|
||||||
|
return UploadTokenParts{}, err
|
||||||
|
}
|
||||||
|
now := time.Now().Unix()
|
||||||
|
expires := deadlineUnix - now
|
||||||
|
if expires < 0 {
|
||||||
|
expires = 0
|
||||||
|
}
|
||||||
|
parts.ExpiresSec = expires
|
||||||
|
return parts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildUploadTokenPartsAtFile filename 不参与 scope(HTTP 为 PDF 二进制体,非 multipart)。
|
||||||
|
func BuildUploadTokenPartsAtFile(accessKey, secret, bucket, filename string, deadlineUnix int64) (UploadTokenParts, error) {
|
||||||
|
_ = filename
|
||||||
|
return BuildUploadTokenPartsForUpload(accessKey, secret, bucket, deadlineUnix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUploadTokenAtFile 生产上传凭证。
|
||||||
|
func CreateUploadTokenAtFile(accessKey, secret, bucket, filename string, deadlineUnix int64) (string, error) {
|
||||||
|
parts, err := BuildUploadTokenPartsAtFile(accessKey, secret, bucket, filename, deadlineUnix)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return parts.UploadToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HMACSignHex 返回 HMAC-SHA1(policyJSON) 原始字节的十六进制(调试用)。
|
||||||
|
func HMACSignHex(secret, policyJSON string) string {
|
||||||
|
mac := hmac.New(sha1.New, []byte(secret))
|
||||||
|
_, _ = mac.Write([]byte(policyJSON))
|
||||||
|
return hex.EncodeToString(mac.Sum(nil))
|
||||||
|
}
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
package fileauth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReturnBodyTemplate 监管 Ver2.0 §2.3.3 文档示例中的 returnBody 固定模板。
|
|
||||||
const ReturnBodyTemplate = `{"name":$(fname),"size":$(fsize),"w":$(imageInfo.width),"h":$(imageInfo.height),"hash":$(etag)}`
|
|
||||||
|
|
||||||
// DocExamplePolicyJSON 文档教学样例 putPolicy(固定向量单测用)。
|
|
||||||
const DocExamplePolicyJSON = `{"scope":"my-bucket:sunflower.jpg","deadline":1451491200,"returnBody":"{\"name\":$(fname),\"size\":$(fsize),\"w\":$(imageInfo.width),\"h\":$(imageInfo.height),\"hash\":$(etag)}"}`
|
|
||||||
|
|
||||||
// DocExampleEncodedPutPolicy 文档 §2.3.3 步骤 2 给出的 encodedPutPolicy。
|
|
||||||
const DocExampleEncodedPutPolicy = "eyJzY29wZSI6Im15LWJ1Y2tldDpzdW5mbG93ZXIuanBnIiwiZGVhZGxpbmUiOjE0NTE0OTEyMDAsInJldHVybkJvZHkiOiJ7XCJuYW1lXCI6JChmbmFtZSksXCJzaXplXCI6JChmc2l6ZSksXCJ3XCI6JChpbWFnZUluZm8ud2lkdGgpLFwiaFwiOiQoaW1hZ2VJbmZvLmhlaWdodCksXCJoYXNoXCI6JChldGFnKX0ifQ=="
|
|
||||||
|
|
||||||
// DocExampleSignHex 文档样例 HMAC 原始字节十六进制(SecretKey=MY_SECRET_KEY)。
|
|
||||||
const DocExampleSignHex = "c10e287f2b1e7f547b20a9ebce2aada26ab20ef2"
|
|
||||||
|
|
||||||
// DocExampleEncodedSign 文档样例 encodedSign。
|
|
||||||
const DocExampleEncodedSign = "wQ4ofysef1R7IKnrziqtomqyDvI="
|
|
||||||
|
|
||||||
// DocExampleUploadToken 文档样例完整 uploadToken(AccessKey=MY_ACCESS_KEY)。
|
|
||||||
const DocExampleUploadToken = "MY_ACCESS_KEY:wQ4ofysef1R7IKnrziqtomqyDvI=:" + DocExampleEncodedPutPolicy
|
|
||||||
|
|
||||||
// MarshalPutPolicyDoc 文档 profile:scope 可含文件名 + returnBody。
|
|
||||||
func MarshalPutPolicyDoc(scope string, deadlineUnix int64) ([]byte, error) {
|
|
||||||
scope = strings.TrimSpace(scope)
|
|
||||||
if scope == "" {
|
|
||||||
return nil, errors.New("empty scope")
|
|
||||||
}
|
|
||||||
if deadlineUnix <= 0 {
|
|
||||||
return nil, errors.New("invalid deadline")
|
|
||||||
}
|
|
||||||
scopeJSON, err := json.Marshal(scope)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rbJSON, err := json.Marshal(ReturnBodyTemplate)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return []byte(`{"scope":` + string(scopeJSON) + `,"deadline":` + strconv.FormatInt(deadlineUnix, 10) + `,"returnBody":` + string(rbJSON) + `}`), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalPutPolicyUpload 生产 profile:scope=仅 bucket,无 returnBody。
|
|
||||||
func MarshalPutPolicyUpload(bucket string, deadlineUnix int64) ([]byte, error) {
|
|
||||||
bucket = strings.TrimSpace(bucket)
|
|
||||||
if bucket == "" {
|
|
||||||
return nil, errors.New("empty bucket")
|
|
||||||
}
|
|
||||||
if deadlineUnix <= 0 {
|
|
||||||
return nil, errors.New("invalid deadline")
|
|
||||||
}
|
|
||||||
scopeJSON, err := json.Marshal(bucket)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return []byte(`{"scope":` + string(scopeJSON) + `,"deadline":` + strconv.FormatInt(deadlineUnix, 10) + `}`), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalPutPolicyVer233 兼容旧名;等同 doc profile。
|
|
||||||
func MarshalPutPolicyVer233(scope string, deadlineUnix int64) ([]byte, error) {
|
|
||||||
return MarshalPutPolicyDoc(scope, deadlineUnix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// signPolicyVer233 §2.3.3:HMAC-SHA1(secret, UTF-8(encodedPutPolicy)) → urlSafeB64。
|
|
||||||
func signPolicyVer233(secret, encodedPutPolicy string) (string, error) {
|
|
||||||
mac := hmac.New(sha1.New, []byte(secret))
|
|
||||||
if _, err := mac.Write([]byte(encodedPutPolicy)); err != nil {
|
|
||||||
return "", fmt.Errorf("hmac: %w", err)
|
|
||||||
}
|
|
||||||
return encodeURLSafe(mac.Sum(nil)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// urlSafeBase64Ver233 供单测;使用当前 B64 模式(doc 单测应 SetB64Mode(padded))。
|
|
||||||
func urlSafeBase64Ver233(data []byte) string {
|
|
||||||
return encodeURLSafe(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildUploadTokenFromPolicy 按 §2.3.3 五步由 putPolicy JSON 生成 token。
|
|
||||||
func BuildUploadTokenFromPolicy(accessKey, secret string, putPolicyJSON []byte, algorithm string) (UploadTokenParts, error) {
|
|
||||||
accessKey = strings.TrimSpace(accessKey)
|
|
||||||
secret = strings.TrimSpace(secret)
|
|
||||||
if accessKey == "" || secret == "" {
|
|
||||||
return UploadTokenParts{}, errors.New("empty key or secret")
|
|
||||||
}
|
|
||||||
if len(putPolicyJSON) == 0 {
|
|
||||||
return UploadTokenParts{}, errors.New("empty putPolicy")
|
|
||||||
}
|
|
||||||
if algorithm == "" {
|
|
||||||
algorithm = "ver233"
|
|
||||||
}
|
|
||||||
|
|
||||||
encodedPutPolicy := encodeURLSafe(putPolicyJSON)
|
|
||||||
sign, err := signPolicyVer233(secret, encodedPutPolicy)
|
|
||||||
if err != nil {
|
|
||||||
return UploadTokenParts{}, err
|
|
||||||
}
|
|
||||||
token := accessKey + ":" + sign + ":" + encodedPutPolicy
|
|
||||||
|
|
||||||
var policy struct {
|
|
||||||
Deadline int64 `json:"deadline"`
|
|
||||||
}
|
|
||||||
_ = json.Unmarshal(putPolicyJSON, &policy)
|
|
||||||
|
|
||||||
return UploadTokenParts{
|
|
||||||
AccessKey: accessKey,
|
|
||||||
PolicyJSON: string(putPolicyJSON),
|
|
||||||
EncodedPutPolicy: encodedPutPolicy,
|
|
||||||
SignInput: encodedPutPolicy,
|
|
||||||
EncodedSign: sign,
|
|
||||||
UploadToken: token,
|
|
||||||
DeadlineUnix: policy.Deadline,
|
|
||||||
Algorithm: algorithm,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildUploadTokenPartsForUpload 生产路径:scope=仅 bucket,§2.3.3 验签。
|
|
||||||
func BuildUploadTokenPartsForUpload(accessKey, secret, bucket string, deadlineUnix int64) (UploadTokenParts, error) {
|
|
||||||
accessKey = strings.TrimSpace(accessKey)
|
|
||||||
secret = strings.TrimSpace(secret)
|
|
||||||
bucket = strings.TrimSpace(bucket)
|
|
||||||
if accessKey == "" || secret == "" {
|
|
||||||
return UploadTokenParts{}, errors.New("empty key or secret")
|
|
||||||
}
|
|
||||||
if bucket == "" {
|
|
||||||
return UploadTokenParts{}, errors.New("empty bucket")
|
|
||||||
}
|
|
||||||
if deadlineUnix <= 0 {
|
|
||||||
return UploadTokenParts{}, errors.New("invalid deadline")
|
|
||||||
}
|
|
||||||
jsonPolicy, err := MarshalPutPolicyUpload(bucket, deadlineUnix)
|
|
||||||
if err != nil {
|
|
||||||
return UploadTokenParts{}, err
|
|
||||||
}
|
|
||||||
parts, err := BuildUploadTokenFromPolicy(accessKey, secret, jsonPolicy, "ver233-upload")
|
|
||||||
if err != nil {
|
|
||||||
return UploadTokenParts{}, err
|
|
||||||
}
|
|
||||||
now := time.Now().Unix()
|
|
||||||
expires := deadlineUnix - now
|
|
||||||
if expires < 0 {
|
|
||||||
expires = 0
|
|
||||||
}
|
|
||||||
parts.ExpiresSec = expires
|
|
||||||
return parts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildUploadTokenPartsAtFile filename 不参与 scope(HTTP 为 PDF 二进制体,非 multipart)。
|
|
||||||
func BuildUploadTokenPartsAtFile(accessKey, secret, bucket, filename string, deadlineUnix int64) (UploadTokenParts, error) {
|
|
||||||
_ = filename
|
|
||||||
return BuildUploadTokenPartsForUpload(accessKey, secret, bucket, deadlineUnix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateUploadTokenAtFile 生产上传凭证。
|
|
||||||
func CreateUploadTokenAtFile(accessKey, secret, bucket, filename string, deadlineUnix int64) (string, error) {
|
|
||||||
parts, err := BuildUploadTokenPartsAtFile(accessKey, secret, bucket, filename, deadlineUnix)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return parts.UploadToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildUploadTokenPartsDoc 文档黄金样例(固定 scope/deadline)。
|
|
||||||
func BuildUploadTokenPartsDoc(accessKey, secret string) (UploadTokenParts, error) {
|
|
||||||
return BuildUploadTokenFromPolicy(accessKey, secret, []byte(DocExamplePolicyJSON), "ver233-doc")
|
|
||||||
}
|
|
||||||
|
|
||||||
// HMACSignHex 返回 HMAC-SHA1 原始字节的十六进制(调试用)。
|
|
||||||
func HMACSignHex(secret, encodedPutPolicy string) string {
|
|
||||||
mac := hmac.New(sha1.New, []byte(secret))
|
|
||||||
_, _ = mac.Write([]byte(encodedPutPolicy))
|
|
||||||
return hex.EncodeToString(mac.Sum(nil))
|
|
||||||
}
|
|
||||||
@@ -30,3 +30,11 @@
|
|||||||
[push] 2026/05/28 15:14:40 chromedp: html_len=87 data_url_len=152
|
[push] 2026/05/28 15:14:40 chromedp: html_len=87 data_url_len=152
|
||||||
[push] 2026/05/28 15:14:41 chromedp: pdf bytes=5205
|
[push] 2026/05/28 15:14:41 chromedp: pdf bytes=5205
|
||||||
[push] 2026/05/28 15:14:41 file upload throttle: wait 79ms (min interval 80ms)
|
[push] 2026/05/28 15:14:41 file upload throttle: wait 79ms (min interval 80ms)
|
||||||
|
[push] 2026/05/28 16:42:27 chromedp: using browser C:\Program Files\Google\Chrome\Application\chrome.exe
|
||||||
|
[push] 2026/05/28 16:42:27 chromedp: html_len=87 data_url_len=152
|
||||||
|
[push] 2026/05/28 16:42:28 chromedp: pdf bytes=5205
|
||||||
|
[push] 2026/05/28 16:42:28 file upload throttle: wait 80ms (min interval 80ms)
|
||||||
|
[push] 2026/05/28 16:42:39 chromedp: using browser C:\Program Files\Google\Chrome\Application\chrome.exe
|
||||||
|
[push] 2026/05/28 16:42:39 chromedp: html_len=87 data_url_len=152
|
||||||
|
[push] 2026/05/28 16:42:39 chromedp: pdf bytes=5205
|
||||||
|
[push] 2026/05/28 16:42:40 file upload throttle: wait 79ms (min interval 80ms)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func uploadHTTPClient() *http.Client {
|
|||||||
// UploadPDF 上传 PDF 至监管文件服务(通常经 forward-go),成功时返回 record.fileId。
|
// UploadPDF 上传 PDF 至监管文件服务(通常经 forward-go),成功时返回 record.fileId。
|
||||||
//
|
//
|
||||||
// uploadURL:FILE_UPLOAD_VIA_FORWARD=true 时为 forward 地址;否则为云端 config 返回的政务云 URL。
|
// uploadURL:FILE_UPLOAD_VIA_FORWARD=true 时为 forward 地址;否则为云端 config 返回的政务云 URL。
|
||||||
// uploadToken:transit 按 §2.3.3 本地生成(scope=仅 bucket),放在 X-Authorization 请求头。
|
// uploadToken:transit 按 FileAuth.java 本地生成(scope=仅 bucket),放在 X-Authorization 请求头。
|
||||||
// filename 仅用于本机落盘命名,不参与 HTTP 请求体。
|
// filename 仅用于本机落盘命名,不参与 HTTP 请求体。
|
||||||
// forwardToken:非空时设置 X-Forward-Token(与 forward-go FORWARD_SHARED_SECRET 对应)。
|
// forwardToken:非空时设置 X-Forward-Token(与 forward-go FORWARD_SHARED_SECRET 对应)。
|
||||||
func UploadPDF(pdf []byte, filename, uploadURL, uploadToken, forwardToken string) (string, error) {
|
func UploadPDF(pdf []byte, filename, uploadURL, uploadToken, forwardToken string) (string, error) {
|
||||||
@@ -122,7 +122,7 @@ func formatUploadHTTPError(status int, raw []byte, uploadURL string) error {
|
|||||||
case strings.EqualFold(body, "forbidden"):
|
case strings.EqualFold(body, "forbidden"):
|
||||||
msg += ";可能为 forward-go 拒绝:检查 ALLOW_IPS、FORWARD_SHARED_SECRET 与 X-Forward-Token"
|
msg += ";可能为 forward-go 拒绝:检查 ALLOW_IPS、FORWARD_SHARED_SECRET 与 X-Forward-Token"
|
||||||
case body == "404":
|
case body == "404":
|
||||||
msg += ";政务云 uploadToken 验签失败(非 forward forbidden):核对 §2.3.3 scope 是否为仅 bucket 名、HMAC 输入为 encodedPutPolicy"
|
msg += ";政务云 uploadToken 验签失败(非 forward forbidden):核对 FileAuth.java scope 是否为仅 bucket 名、HMAC 输入为 policy JSON"
|
||||||
msg += upload403Hints(body)
|
msg += upload403Hints(body)
|
||||||
default:
|
default:
|
||||||
if extra := jsonMessageField(raw); extra != "" {
|
if extra := jsonMessageField(raw); extra != "" {
|
||||||
@@ -146,9 +146,9 @@ func upload403Hints(body string) string {
|
|||||||
hints = append(hints, "政务云文件上传约 10 秒 1 次,transit 已全局限频;若仍 403 请拉长 FILE_UPLOAD_MIN_INTERVAL 或稍后重试")
|
hints = append(hints, "政务云文件上传约 10 秒 1 次,transit 已全局限频;若仍 403 请拉长 FILE_UPLOAD_MIN_INTERVAL 或稍后重试")
|
||||||
}
|
}
|
||||||
hints = append(hints,
|
hints = append(hints,
|
||||||
"按 §2.3.3 排查:①NTP 时钟(deadline 为上传完成校验时间)",
|
"按 FileAuth.java 排查:①NTP 时钟(deadline 为上传完成校验时间)",
|
||||||
"②HY_APP_KEY/HY_APP_SECRET/HY_FILE_BUCKET(policy scope 仅 bucket 名,不含文件名)",
|
"②HY_APP_KEY/HY_APP_SECRET/HY_FILE_BUCKET(policy scope 仅 bucket 名,不含文件名)",
|
||||||
"③token=AccessKey:encodedSign:encodedPutPolicy;sign=HMAC-SHA1(secret, UTF-8(encodedPutPolicy))",
|
"③token=AccessKey:encodedSign:policyB64;sign=HMAC-SHA1(secret, UTF-8(policyJSON))",
|
||||||
"④仍失败可设 FILEAUTH_B64=raw 后重启 transit",
|
"④仍失败可设 FILEAUTH_B64=raw 后重启 transit",
|
||||||
"⑤/config 重新拉取",
|
"⑤/config 重新拉取",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ func TestUploadPDF_forbiddenGovMessage(t *testing.T) {
|
|||||||
if err == nil || !strings.Contains(err.Error(), "token expired") {
|
if err == nil || !strings.Contains(err.Error(), "token expired") {
|
||||||
t.Fatalf("err=%v", err)
|
t.Fatalf("err=%v", err)
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "2.3") {
|
if !strings.Contains(err.Error(), "FileAuth.java") {
|
||||||
t.Fatalf("want 2.3 troubleshooting hints, got %v", err)
|
t.Fatalf("want FileAuth.java troubleshooting hints, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ func BuildConfigView(snap syncer.ConfigSnapshot, cfg config.Config) ConfigViewRe
|
|||||||
|
|
||||||
FileUploadURL: snap.FileUploadURL,
|
FileUploadURL: snap.FileUploadURL,
|
||||||
|
|
||||||
SourceNote: "uploadToken:Runner 按 §2.3.3 本地生成;appKey/secret/bucket 云端非空优先",
|
SourceNote: "uploadToken:Runner 按 FileAuth.java 本地生成;appKey/secret/bucket 云端非空优先",
|
||||||
|
|
||||||
ConfigRefreshed: formatTime(snap.RefreshedAt),
|
ConfigRefreshed: formatTime(snap.RefreshedAt),
|
||||||
},
|
},
|
||||||
@@ -244,7 +244,7 @@ func BuildConfigView(snap syncer.ConfigSnapshot, cfg config.Config) ConfigViewRe
|
|||||||
|
|
||||||
if cloudToken != "" && localToken != "" && cloudToken != localToken {
|
if cloudToken != "" && localToken != "" && cloudToken != localToken {
|
||||||
|
|
||||||
view.Diagnostics = append(view.Diagnostics, "云端预生成 uploadToken 与 Runner 本地 token 不一致(已使用本地 §2.3.3,以 Runner 为准)")
|
view.Diagnostics = append(view.Diagnostics, "云端预生成 uploadToken 与 Runner 本地 token 不一致(已使用本地 FileAuth.java,以 Runner 为准)")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ type FileAuthGenerateResponse struct {
|
|||||||
FormatChecks []fileauth.FormatCheck `json:"formatChecks"`
|
FormatChecks []fileauth.FormatCheck `json:"formatChecks"`
|
||||||
RunnerTokenMatch bool `json:"runnerTokenMatch"`
|
RunnerTokenMatch bool `json:"runnerTokenMatch"`
|
||||||
RunnerTokenPreview string `json:"runnerTokenPreview"`
|
RunnerTokenPreview string `json:"runnerTokenPreview"`
|
||||||
DocGoldenMatch bool `json:"docGoldenMatch"`
|
|
||||||
DocExample fileauth.DocExample `json:"docExample"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleAPIFileAuthGenerate(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAPIFileAuthGenerate(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -62,26 +60,17 @@ func (s *Server) handleAPIFileAuthGenerate(w http.ResponseWriter, r *http.Reques
|
|||||||
bucket = strings.TrimSpace(snap.EffectiveFileBucket)
|
bucket = strings.TrimSpace(snap.EffectiveFileBucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
isDoc := r.URL.Query().Get("doc") == "1"
|
|
||||||
if isDoc {
|
|
||||||
accessKey = "MY_ACCESS_KEY"
|
|
||||||
secret = "MY_SECRET_KEY"
|
|
||||||
}
|
|
||||||
|
|
||||||
if accessKey == "" || secret == "" {
|
if accessKey == "" || secret == "" {
|
||||||
writeErr(w, 400, "生效 appKey/appSecret 未配置,请在 /config 重新拉取或配置 HY_APP_KEY/HY_APP_SECRET")
|
writeErr(w, 400, "生效 appKey/appSecret 未配置,请在 /config 重新拉取或配置 HY_APP_KEY/HY_APP_SECRET")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !isDoc && bucket == "" {
|
if bucket == "" {
|
||||||
writeErr(w, 400, "fileBucket(scope) 为空,请配置 HY_FILE_BUCKET")
|
writeErr(w, 400, "fileBucket(scope) 为空,请配置 HY_FILE_BUCKET")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
deadlineUnix := time.Now().Unix() + 3600
|
deadlineUnix := time.Now().Unix() + 3600
|
||||||
if isDoc {
|
if ds := strings.TrimSpace(r.URL.Query().Get("deadline")); ds != "" {
|
||||||
deadlineUnix = 1451491200
|
|
||||||
}
|
|
||||||
if ds := strings.TrimSpace(r.URL.Query().Get("deadline")); ds != "" && !isDoc {
|
|
||||||
n, err := strconv.ParseInt(ds, 10, 64)
|
n, err := strconv.ParseInt(ds, 10, 64)
|
||||||
if err != nil || n <= 0 {
|
if err != nil || n <= 0 {
|
||||||
writeErr(w, 400, "invalid deadline (unix seconds)")
|
writeErr(w, 400, "invalid deadline (unix seconds)")
|
||||||
@@ -90,34 +79,21 @@ func (s *Server) handleAPIFileAuthGenerate(w http.ResponseWriter, r *http.Reques
|
|||||||
deadlineUnix = n
|
deadlineUnix = n
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts fileauth.UploadTokenParts
|
parts, err := fileauth.BuildUploadTokenPartsForUpload(accessKey, secret, bucket, deadlineUnix)
|
||||||
var err error
|
|
||||||
if isDoc {
|
|
||||||
parts, err = fileauth.BuildUploadTokenPartsDoc(accessKey, secret)
|
|
||||||
} else {
|
|
||||||
parts, err = fileauth.BuildUploadTokenPartsForUpload(accessKey, secret, bucket, deadlineUnix)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErr(w, 500, err.Error())
|
writeErr(w, 500, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
scope := bucket
|
|
||||||
if isDoc {
|
|
||||||
scope = "my-bucket:sunflower.jpg"
|
|
||||||
}
|
|
||||||
|
|
||||||
formatOK, checks := fileauth.ValidateUploadTokenFormat(parts.UploadToken, accessKey)
|
formatOK, checks := fileauth.ValidateUploadTokenFormat(parts.UploadToken, accessKey)
|
||||||
runnerTok := strings.TrimSpace(sr.UploadToken())
|
runnerTok := strings.TrimSpace(sr.UploadToken())
|
||||||
runnerMatch := !isDoc && runnerTok != "" && runnerTok == parts.UploadToken
|
runnerMatch := runnerTok != "" && runnerTok == parts.UploadToken
|
||||||
|
|
||||||
docGolden := isDoc && parts.UploadToken == fileauth.DocExampleUploadToken
|
|
||||||
|
|
||||||
writeJSON(w, FileAuthGenerateResponse{
|
writeJSON(w, FileAuthGenerateResponse{
|
||||||
AccessKey: parts.AccessKey,
|
AccessKey: parts.AccessKey,
|
||||||
SecretSet: secret != "",
|
SecretSet: secret != "",
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Scope: scope,
|
Scope: bucket,
|
||||||
B64Mode: fileauth.B64Mode(),
|
B64Mode: fileauth.B64Mode(),
|
||||||
PolicyJSON: parts.PolicyJSON,
|
PolicyJSON: parts.PolicyJSON,
|
||||||
EncodedPutPolicy: parts.EncodedPutPolicy,
|
EncodedPutPolicy: parts.EncodedPutPolicy,
|
||||||
@@ -133,8 +109,6 @@ func (s *Server) handleAPIFileAuthGenerate(w http.ResponseWriter, r *http.Reques
|
|||||||
FormatChecks: checks,
|
FormatChecks: checks,
|
||||||
RunnerTokenMatch: runnerMatch,
|
RunnerTokenMatch: runnerMatch,
|
||||||
RunnerTokenPreview: maskPreview(runnerTok),
|
RunnerTokenPreview: maskPreview(runnerTok),
|
||||||
DocGoldenMatch: docGolden,
|
|
||||||
DocExample: fileauth.RegulatoryDocExample(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
syncer "xk-hy-transit-go/internal/sync"
|
syncer "xk-hy-transit-go/internal/sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UploadTokenInfo 本次上传使用的 §2.3.3 凭证(Web 调试展示)。
|
// UploadTokenInfo 本次上传使用的 FileAuth.java 凭证(Web 调试展示)。
|
||||||
type UploadTokenInfo struct {
|
type UploadTokenInfo struct {
|
||||||
Algorithm string `json:"algorithm"`
|
Algorithm string `json:"algorithm"`
|
||||||
PolicyJSON string `json:"policyJson"`
|
PolicyJSON string `json:"policyJson"`
|
||||||
|
|||||||
@@ -1,403 +1,161 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
<title>上传凭证 FileAuth</title>
|
||||||
<title>上传凭证 §2.3.3</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/static.css">
|
<link rel="stylesheet" href="/static/static.css">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
.step { margin-bottom: 1rem; }
|
.step { margin-bottom: 1rem; }
|
||||||
|
|
||||||
.step h4 { margin: 0 0 0.35rem; color: var(--accent); font-size: 0.95rem; }
|
.step h4 { margin: 0 0 0.35rem; color: var(--accent); font-size: 0.95rem; }
|
||||||
|
|
||||||
.step pre { margin: 0; max-height: 12rem; overflow: auto; }
|
.step pre { margin: 0; max-height: 12rem; overflow: auto; }
|
||||||
|
|
||||||
.formula { color: var(--muted); font-size: 0.9rem; margin-bottom: 1rem; }
|
.formula { color: var(--muted); font-size: 0.9rem; margin-bottom: 1rem; }
|
||||||
|
|
||||||
.check-ok { color: var(--ok); }
|
.check-ok { color: var(--ok); }
|
||||||
|
|
||||||
.check-fail { color: var(--err); }
|
.check-fail { color: var(--err); }
|
||||||
|
|
||||||
label.chk { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.9rem; }
|
label.chk { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.9rem; }
|
||||||
|
|
||||||
.diff { border-left: 3px solid var(--warn); padding-left: 0.75rem; margin-top: 0.5rem; }
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
|
<h1>上传凭证(FileAuth.java)</h1>
|
||||||
<h1>上传凭证(Ver2.0 §2.3.3)</h1>
|
|
||||||
|
|
||||||
<nav class="nav" id="main-nav"></nav>
|
<nav class="nav" id="main-nav"></nav>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<p class="formula">
|
<p class="formula">
|
||||||
|
与 <code>docs/FileAuth.java</code> 一致:
|
||||||
五步:<code>putPolicy</code> → <code>encodedPutPolicy</code>(URL-safe Base64)→
|
<code>sign = HMAC-SHA1(secret, policyJSON)</code> →
|
||||||
|
<code>uploadToken = AccessKey:encodedSign:policyB64</code>(policyB64 = urlsafe_base64(policyJSON))
|
||||||
<code>HMAC-SHA1(secret, encodedPutPolicy)</code> → <code>encodedSign</code> →
|
|
||||||
|
|
||||||
<code>uploadToken = AccessKey:encodedSign:encodedPutPolicy</code>
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
|
|
||||||
<label>deadline(Unix 秒)<input type="text" id="deadline" placeholder="默认 now+3600" style="width:11rem"></label>
|
<label>deadline(Unix 秒)<input type="text" id="deadline" placeholder="默认 now+3600" style="width:11rem"></label>
|
||||||
|
|
||||||
<label>bucket <input type="text" id="bucket" placeholder="Runner fileBucket" style="width:10rem"></label>
|
<label>bucket <input type="text" id="bucket" placeholder="Runner fileBucket" style="width:10rem"></label>
|
||||||
|
<button class="primary" id="btn-gen">生成</button>
|
||||||
<button class="primary" id="btn-gen">生成(upload profile)</button>
|
|
||||||
|
|
||||||
<button id="btn-doc">文档黄金样例</button>
|
|
||||||
|
|
||||||
<button id="btn-refresh">刷新 Runner 并对比</button>
|
<button id="btn-refresh">刷新 Runner 并对比</button>
|
||||||
|
|
||||||
<label class="chk"><input type="checkbox" id="show-full"> 显示完整 token</label>
|
<label class="chk"><input type="checkbox" id="show-full"> 显示完整 token</label>
|
||||||
|
|
||||||
<button type="button" id="btn-copy">复制完整 token</button>
|
<button type="button" id="btn-copy">复制完整 token</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div id="err-box"></div>
|
<div id="err-box"></div>
|
||||||
|
|
||||||
|
<div class="panel" id="panel-upload">
|
||||||
|
<h3>生产 upload profile(scope=仅 bucket)</h3>
|
||||||
<div class="panel" id="panel-ver233">
|
|
||||||
|
|
||||||
<h3>A. §2.3.3 生产算法(transit / xk-api)</h3>
|
|
||||||
|
|
||||||
<div class="step">
|
<div class="step">
|
||||||
|
<h4>1. putPolicy</h4>
|
||||||
<h4>1. putPolicy(scope=仅 bucket,无 returnBody)</h4>
|
|
||||||
|
|
||||||
<pre id="policy-json">—</pre>
|
<pre id="policy-json">—</pre>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="step">
|
<div class="step">
|
||||||
|
<h4>2. sign = HMAC-SHA1(secret, <strong>policyJSON</strong> UTF-8) → encodedSign</h4>
|
||||||
<h4>2. encodedPutPolicy = urlsafe_base64(putPolicy)</h4>
|
<p class="formula" style="margin:0 0 0.35rem">signInput:<code id="sign-input">—</code> · hex:<code id="sign-hex">—</code></p>
|
||||||
|
|
||||||
<pre id="encoded-policy">—</pre>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="step">
|
|
||||||
|
|
||||||
<h4>3. sign = HMAC-SHA1(secret, <strong>encodedPutPolicy</strong> UTF-8)</h4>
|
|
||||||
|
|
||||||
<p class="formula" style="margin:0 0 0.35rem">signInput(签名字节串):<code id="sign-input">—</code> · hex:<code id="sign-hex">—</code></p>
|
|
||||||
|
|
||||||
<pre id="encoded-sign">—</pre>
|
<pre id="encoded-sign">—</pre>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="step">
|
<div class="step">
|
||||||
|
<h4>3. policyB64 = urlsafe_base64(putPolicy)</h4>
|
||||||
<h4>4–5. uploadToken = AccessKey + ':' + encodedSign + ':' + encodedPutPolicy</h4>
|
<pre id="encoded-policy">—</pre>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<h4>4. uploadToken = AccessKey + ':' + encodedSign + ':' + policyB64</h4>
|
||||||
<pre id="upload-token">—</pre>
|
<pre id="upload-token">—</pre>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<p>算法:<code id="algorithm">—</code> · B64:<code id="b64-mode">—</code> · AccessKey:<code id="access-key">—</code> · scope:<code id="scope-val">—</code> · deadline:<code id="deadline-val">—</code></p>
|
||||||
<p>算法:<code id="algorithm">—</code> · B64:<code id="b64-mode">—</code> · AccessKey:<code id="access-key">—</code> · scope:<code id="scope-val">—</code> · deadline:<code id="deadline-val">—</code>
|
|
||||||
|
|
||||||
· <span id="doc-golden">—</span></p>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
<h3>格式校验</h3>
|
||||||
<h3>B. 格式校验</h3>
|
|
||||||
|
|
||||||
<p id="format-summary">—</p>
|
<p id="format-summary">—</p>
|
||||||
|
|
||||||
<ul id="format-checks"></ul>
|
<ul id="format-checks"></ul>
|
||||||
|
<p>与 Runner 当前 token:<span id="runner-match">—</span>(<code id="runner-preview">—</code>)</p>
|
||||||
<p>与 Runner 当前 token(默认 prescription.pdf):<span id="runner-match">—</span>(<code id="runner-preview">—</code>)</p>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="panel">
|
|
||||||
|
|
||||||
<h3>C. 监管文档教学示例(doc profile,只读)</h3>
|
|
||||||
|
|
||||||
<p id="doc-note" class="formula">—</p>
|
|
||||||
|
|
||||||
<div class="step">
|
|
||||||
|
|
||||||
<h4>示例 token 三段</h4>
|
|
||||||
|
|
||||||
<pre id="doc-token-parts">—</pre>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="step">
|
|
||||||
|
|
||||||
<h4>encodedPutPolicy 解码</h4>
|
|
||||||
|
|
||||||
<pre id="doc-policy">—</pre>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
if (typeof renderMainNav === 'function') renderMainNav('fileauth');
|
if (typeof renderMainNav === 'function') renderMainNav('fileauth');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let lastData = null;
|
let lastData = null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function tokenDisplay(data) {
|
function tokenDisplay(data) {
|
||||||
|
|
||||||
if (!data) return '—';
|
if (!data) return '—';
|
||||||
|
|
||||||
const show = document.getElementById('show-full').checked;
|
const show = document.getElementById('show-full').checked;
|
||||||
|
|
||||||
return show ? data.uploadToken : (data.uploadTokenMasked || data.uploadToken);
|
return show ? data.uploadToken : (data.uploadTokenMasked || data.uploadToken);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function renderChecks(checks) {
|
function renderChecks(checks) {
|
||||||
|
|
||||||
const ul = document.getElementById('format-checks');
|
const ul = document.getElementById('format-checks');
|
||||||
|
|
||||||
ul.innerHTML = (checks || []).map(c => {
|
ul.innerHTML = (checks || []).map(c => {
|
||||||
|
|
||||||
const cls = c.ok ? 'check-ok' : 'check-fail';
|
const cls = c.ok ? 'check-ok' : 'check-fail';
|
||||||
|
|
||||||
const mark = c.ok ? '✓' : '✗';
|
const mark = c.ok ? '✓' : '✗';
|
||||||
|
|
||||||
return `<li class="${cls}">${mark} <strong>${esc(c.id)}</strong> — ${esc(c.detail)}</li>`;
|
return `<li class="${cls}">${mark} <strong>${esc(c.id)}</strong> — ${esc(c.detail)}</li>`;
|
||||||
|
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function render(data) {
|
function render(data) {
|
||||||
|
|
||||||
lastData = data;
|
lastData = data;
|
||||||
|
|
||||||
document.getElementById('err-box').innerHTML = '';
|
document.getElementById('err-box').innerHTML = '';
|
||||||
|
|
||||||
document.getElementById('policy-json').textContent = data.policyJson || '—';
|
document.getElementById('policy-json').textContent = data.policyJson || '—';
|
||||||
|
|
||||||
document.getElementById('encoded-policy').textContent = data.encodedPutPolicy || '—';
|
document.getElementById('encoded-policy').textContent = data.encodedPutPolicy || '—';
|
||||||
|
|
||||||
document.getElementById('sign-input').textContent = data.signInput || '—';
|
document.getElementById('sign-input').textContent = data.signInput || '—';
|
||||||
|
|
||||||
document.getElementById('sign-hex').textContent = data.signHex || '—';
|
document.getElementById('sign-hex').textContent = data.signHex || '—';
|
||||||
|
|
||||||
document.getElementById('encoded-sign').textContent = data.encodedSign || '—';
|
document.getElementById('encoded-sign').textContent = data.encodedSign || '—';
|
||||||
|
|
||||||
document.getElementById('upload-token').textContent = tokenDisplay(data);
|
document.getElementById('upload-token').textContent = tokenDisplay(data);
|
||||||
|
|
||||||
document.getElementById('access-key').textContent = data.accessKey || '—';
|
document.getElementById('access-key').textContent = data.accessKey || '—';
|
||||||
|
|
||||||
document.getElementById('scope-val').textContent = data.scope || data.bucket || '—';
|
document.getElementById('scope-val').textContent = data.scope || data.bucket || '—';
|
||||||
|
|
||||||
document.getElementById('b64-mode').textContent = data.b64Mode || '—';
|
document.getElementById('b64-mode').textContent = data.b64Mode || '—';
|
||||||
|
|
||||||
document.getElementById('deadline-val').textContent = data.deadlineUnix != null ? String(data.deadlineUnix) : '—';
|
document.getElementById('deadline-val').textContent = data.deadlineUnix != null ? String(data.deadlineUnix) : '—';
|
||||||
|
|
||||||
document.getElementById('algorithm').textContent = data.algorithm || '—';
|
document.getElementById('algorithm').textContent = data.algorithm || '—';
|
||||||
|
|
||||||
document.getElementById('doc-golden').innerHTML = data.docGoldenMatch
|
|
||||||
|
|
||||||
? '<span class="badge ok">与文档黄金样例一致</span>'
|
|
||||||
|
|
||||||
: '<span class="badge info">非文档固定向量(点「文档黄金样例」可对照)</span>';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const sum = document.getElementById('format-summary');
|
const sum = document.getElementById('format-summary');
|
||||||
|
|
||||||
sum.innerHTML = data.formatOk
|
sum.innerHTML = data.formatOk
|
||||||
|
? '<span class="badge ok">格式符合 AccessKey:encodedSign:policyB64</span>'
|
||||||
? '<span class="badge ok">格式符合 AccessKey:encodedSign:encodedPutPolicy</span>'
|
|
||||||
|
|
||||||
: '<span class="badge err">格式校验未全部通过</span>';
|
: '<span class="badge err">格式校验未全部通过</span>';
|
||||||
|
|
||||||
renderChecks(data.formatChecks);
|
renderChecks(data.formatChecks);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const matchEl = document.getElementById('runner-match');
|
const matchEl = document.getElementById('runner-match');
|
||||||
|
|
||||||
if (data.runnerTokenMatch) {
|
if (data.runnerTokenMatch) {
|
||||||
|
|
||||||
matchEl.innerHTML = '<span class="badge ok">一致</span>';
|
matchEl.innerHTML = '<span class="badge ok">一致</span>';
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
matchEl.innerHTML = '<span class="badge warn">不一致</span>(自定义 deadline 或需重新拉取配置)';
|
matchEl.innerHTML = '<span class="badge warn">不一致</span>(自定义 deadline 或需重新拉取配置)';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('runner-preview').textContent = data.runnerTokenPreview || '—';
|
document.getElementById('runner-preview').textContent = data.runnerTokenPreview || '—';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const doc = data.docExample || {};
|
|
||||||
|
|
||||||
document.getElementById('doc-note').textContent = doc.note || '—';
|
|
||||||
|
|
||||||
document.getElementById('doc-token-parts').textContent =
|
|
||||||
|
|
||||||
'AccessKey: ' + (doc.accessKeyPart || '') + '\n' +
|
|
||||||
|
|
||||||
'encodedSign: ' + (doc.encodedSignPart || '') + '\n' +
|
|
||||||
|
|
||||||
'encodedPutPolicy: ' + (doc.encodedPolicyPart || '');
|
|
||||||
|
|
||||||
document.getElementById('doc-policy').textContent = doc.policyJsonDecoded || '—';
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function load(refresh) {
|
async function load(refresh) {
|
||||||
|
|
||||||
const errBox = document.getElementById('err-box');
|
const errBox = document.getElementById('err-box');
|
||||||
|
|
||||||
errBox.innerHTML = '';
|
errBox.innerHTML = '';
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
const dl = document.getElementById('deadline').value.trim();
|
const dl = document.getElementById('deadline').value.trim();
|
||||||
|
|
||||||
const bk = document.getElementById('bucket').value.trim();
|
const bk = document.getElementById('bucket').value.trim();
|
||||||
|
|
||||||
if (dl) params.set('deadline', dl);
|
if (dl) params.set('deadline', dl);
|
||||||
|
|
||||||
if (bk) params.set('bucket', bk);
|
if (bk) params.set('bucket', bk);
|
||||||
|
|
||||||
if (refresh) params.set('refresh', '1');
|
if (refresh) params.set('refresh', '1');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const data = await api('/api/fileauth/generate?' + params.toString());
|
const data = await api('/api/fileauth/generate?' + params.toString());
|
||||||
|
|
||||||
render(data);
|
render(data);
|
||||||
|
|
||||||
showToast('已生成', 'ok');
|
showToast('已生成', 'ok');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
||||||
showInlineError(errBox, e.message || String(e));
|
showInlineError(errBox, e.message || String(e));
|
||||||
|
|
||||||
showToast(firstLine(e.message) || '生成失败', 'err');
|
showToast(firstLine(e.message) || '生成失败', 'err');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function loadDocGolden() {
|
|
||||||
|
|
||||||
document.getElementById('deadline').value = '1451491200';
|
|
||||||
|
|
||||||
document.getElementById('bucket').value = 'my-bucket';
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
const data = await api('/api/fileauth/generate?doc=1');
|
|
||||||
|
|
||||||
render(data);
|
|
||||||
|
|
||||||
showToast('文档黄金样例', 'ok');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
showInlineError(document.getElementById('err-box'), e.message || String(e));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('btn-gen').onclick = () => load(false);
|
document.getElementById('btn-gen').onclick = () => load(false);
|
||||||
|
|
||||||
document.getElementById('btn-doc').onclick = () => loadDocGolden();
|
|
||||||
|
|
||||||
document.getElementById('btn-refresh').onclick = () => load(true);
|
document.getElementById('btn-refresh').onclick = () => load(true);
|
||||||
|
|
||||||
document.getElementById('show-full').onchange = () => {
|
document.getElementById('show-full').onchange = () => {
|
||||||
|
|
||||||
if (lastData) document.getElementById('upload-token').textContent = tokenDisplay(lastData);
|
if (lastData) document.getElementById('upload-token').textContent = tokenDisplay(lastData);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('btn-copy').onclick = async () => {
|
document.getElementById('btn-copy').onclick = async () => {
|
||||||
|
|
||||||
if (!lastData || !lastData.uploadToken) {
|
if (!lastData || !lastData.uploadToken) {
|
||||||
|
|
||||||
showToast('请先生成 token', 'warn');
|
showToast('请先生成 token', 'warn');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
await navigator.clipboard.writeText(lastData.uploadToken);
|
await navigator.clipboard.writeText(lastData.uploadToken);
|
||||||
|
|
||||||
showToast('已复制完整 uploadToken', 'ok');
|
showToast('已复制完整 uploadToken', 'ok');
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
|
|
||||||
showToast('复制失败', 'err');
|
showToast('复制失败', 'err');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
load(false);
|
load(false);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,11 @@
|
|||||||
<div class="cards">
|
<div class="cards">
|
||||||
<a class="card" href="/config">
|
<a class="card" href="/config">
|
||||||
<h2>互医配置</h2>
|
<h2>互医配置</h2>
|
||||||
<p>查看 xk-api 云端与本地 .env、Runner 生效值;本地 §2.3.3 生成 uploadToken。</p>
|
<p>查看 xk-api 云端与本地 .env、Runner 生效值;本地 FileAuth.java 生成 uploadToken。</p>
|
||||||
</a>
|
</a>
|
||||||
<a class="card" href="/fileauth">
|
<a class="card" href="/fileauth">
|
||||||
<h2>上传凭证</h2>
|
<h2>上传凭证</h2>
|
||||||
<p>§2.3.3 upload profile(scope=仅 bucket),文档黄金样例校验。</p>
|
<p>FileAuth.java upload profile(scope=仅 bucket),分步生成与格式校验。</p>
|
||||||
</a>
|
</a>
|
||||||
<a class="card" href="/upload">
|
<a class="card" href="/upload">
|
||||||
<h2>文件上传</h2>
|
<h2>文件上传</h2>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<nav class="nav" id="main-nav"></nav>
|
<nav class="nav" id="main-nav"></nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<p class="muted-note">§2.3.3 生产 token:<strong>scope=仅 bucket 名</strong>。上传为 PDF 二进制体(<code>Content-Type: application/pdf</code>),不用 multipart。403 且 body 为 <code>404</code> 表示 token 验签失败。</p>
|
<p class="muted-note">FileAuth.java 生产 token:<strong>scope=仅 bucket 名</strong>。上传为 PDF 二进制体(<code>Content-Type: application/pdf</code>),不用 multipart。403 且 body 为 <code>404</code> 表示 token 验签失败。</p>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<label>PDF 文件 <input type="file" id="file" accept=".pdf,application/pdf"></label>
|
<label>PDF 文件 <input type="file" id="file" accept=".pdf,application/pdf"></label>
|
||||||
@@ -33,15 +33,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="status-upload" class="panel token-panel">
|
<div id="status-upload" class="panel token-panel">
|
||||||
<h3>B. 本次上传凭证(§2.3.3)</h3>
|
<h3>B. 本次上传凭证(FileAuth.java)</h3>
|
||||||
<p id="upload-token-placeholder" class="muted-note">上传成功或失败后显示本次请求使用的 token 信息。</p>
|
<p id="upload-token-placeholder" class="muted-note">上传成功或失败后显示本次请求使用的 token 信息。</p>
|
||||||
<div id="upload-token-detail" style="display:none">
|
<div id="upload-token-detail" style="display:none">
|
||||||
<p>filename: <code id="t-filename">—</code> · scope: <code id="t-scope">—</code> · algorithm: <code id="t-algo">—</code> · deadline: <code id="t-deadline">—</code></p>
|
<p>filename: <code id="t-filename">—</code> · scope: <code id="t-scope">—</code> · algorithm: <code id="t-algo">—</code> · deadline: <code id="t-deadline">—</code></p>
|
||||||
<h4>signInput(HMAC 输入 = encodedPutPolicy)</h4>
|
<h4>signInput(HMAC 输入 = policy JSON)</h4>
|
||||||
<pre id="t-sign-input">—</pre>
|
<pre id="t-sign-input">—</pre>
|
||||||
<h4>putPolicy JSON</h4>
|
<h4>putPolicy JSON</h4>
|
||||||
<pre id="t-policy">—</pre>
|
<pre id="t-policy">—</pre>
|
||||||
<h4>encodedPutPolicy</h4>
|
<h4>policyB64(第三段)</h4>
|
||||||
<pre id="t-encoded-policy">—</pre>
|
<pre id="t-encoded-policy">—</pre>
|
||||||
<h4>encodedSign</h4>
|
<h4>encodedSign</h4>
|
||||||
<pre id="t-encoded-sign">—</pre>
|
<pre id="t-encoded-sign">—</pre>
|
||||||
@@ -68,7 +68,7 @@ async function showRunnerStatus() {
|
|||||||
const v = await api('/api/config/view');
|
const v = await api('/api/config/view');
|
||||||
const eff = v.effective || {};
|
const eff = v.effective || {};
|
||||||
const el = document.getElementById('runner-status');
|
const el = document.getElementById('runner-status');
|
||||||
el.innerHTML = `uploadToken: ${eff.uploadTokenSet ? '<span class="badge ok">有效</span> ' + esc(eff.uploadTokenPreview) + ' <span class="badge info">' + esc(eff.uploadTokenSource || 'local-ver233-upload') + '</span>' : '<span class="badge err">空</span>'}
|
el.innerHTML = `uploadToken: ${eff.uploadTokenSet ? '<span class="badge ok">有效</span> ' + esc(eff.uploadTokenPreview) + ' <span class="badge info">' + esc(eff.uploadTokenSource || 'local-fileauth-java') + '</span>' : '<span class="badge err">空</span>'}
|
||||||
· fileBucket: <code>${esc(eff.fileBucket || '-')}</code>
|
· fileBucket: <code>${esc(eff.fileBucket || '-')}</code>
|
||||||
· 上传地址: <code>${esc(eff.fileUploadURL)}</code>`;
|
· 上传地址: <code>${esc(eff.fileUploadURL)}</code>`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -97,7 +97,7 @@ function renderUploadTokenInfo(data) {
|
|||||||
detail.style.display = '';
|
detail.style.display = '';
|
||||||
document.getElementById('t-filename').textContent = data.filename || '—';
|
document.getElementById('t-filename').textContent = data.filename || '—';
|
||||||
document.getElementById('t-scope').textContent = data.scope || '—';
|
document.getElementById('t-scope').textContent = data.scope || '—';
|
||||||
document.getElementById('t-algo').textContent = info.algorithm || 'ver233';
|
document.getElementById('t-algo').textContent = info.algorithm || 'fileauth-java-upload';
|
||||||
document.getElementById('t-deadline').textContent = info.deadlineUnix != null ? String(info.deadlineUnix) : '—';
|
document.getElementById('t-deadline').textContent = info.deadlineUnix != null ? String(info.deadlineUnix) : '—';
|
||||||
document.getElementById('t-sign-input').textContent = info.signInput || '—';
|
document.getElementById('t-sign-input').textContent = info.signInput || '—';
|
||||||
document.getElementById('t-policy').textContent = info.policyJson || '—';
|
document.getElementById('t-policy').textContent = info.policyJson || '—';
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ func (r *Runner) applyCloudConfig(conf *xkapi.ConfigResponse) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// refreshUploadToken 按 §2.3.3 upload profile 重新生成 uploadToken(scope=仅 bucket)。
|
// refreshUploadToken 按 FileAuth.java upload profile 重新生成 uploadToken(scope=仅 bucket)。
|
||||||
|
|
||||||
func (r *Runner) refreshUploadToken() error {
|
func (r *Runner) refreshUploadToken() error {
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ func (r *Runner) ConfigSnapshot() ConfigSnapshot {
|
|||||||
|
|
||||||
UploadToken: r.uploadToken,
|
UploadToken: r.uploadToken,
|
||||||
|
|
||||||
UploadTokenSource: "local-ver233-upload",
|
UploadTokenSource: "local-fileauth-java",
|
||||||
|
|
||||||
CloudUploadToken: r.cloudUploadToken,
|
CloudUploadToken: r.cloudUploadToken,
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ type Runner struct {
|
|||||||
store *db.Store
|
store *db.Store
|
||||||
organ hy.OrganConfig
|
organ hy.OrganConfig
|
||||||
fileUploadURL string // 实际上传地址(经 forward 或直连政务云)
|
fileUploadURL string // 实际上传地址(经 forward 或直连政务云)
|
||||||
uploadToken string // 本地按 §2.3.3 生成(默认 prescription.pdf)
|
uploadToken string // 本地按 FileAuth.java 生成
|
||||||
cloudUploadToken string // xk-api 预生成值,仅诊断对比
|
cloudUploadToken string // xk-api 预生成值,仅诊断对比
|
||||||
fileBucket string
|
fileBucket string
|
||||||
fileUploadViaForward bool
|
fileUploadViaForward bool
|
||||||
@@ -478,7 +478,7 @@ func (r *Runner) CloudUploadToken() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UploadTokenSource 说明 uploadToken 生成方式。
|
// UploadTokenSource 说明 uploadToken 生成方式。
|
||||||
func (r *Runner) UploadTokenSource() string { return "local-ver233-upload" }
|
func (r *Runner) UploadTokenSource() string { return "local-fileauth-java" }
|
||||||
|
|
||||||
// RegenerateUploadToken 供诊断:用当前凭证重新生成 token(与 refreshUploadToken 相同)。
|
// RegenerateUploadToken 供诊断:用当前凭证重新生成 token(与 refreshUploadToken 相同)。
|
||||||
func (r *Runner) RegenerateUploadToken() error {
|
func (r *Runner) RegenerateUploadToken() error {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"xk-hy-transit-go/internal/hyfile"
|
"xk-hy-transit-go/internal/hyfile"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UploadTestResult Web 测试上传结果(含本次 §2.3.3 token 分步信息)。
|
// UploadTestResult Web 测试上传结果(含本次 FileAuth.java token 分步信息)。
|
||||||
type UploadTestResult struct {
|
type UploadTestResult struct {
|
||||||
FileID string
|
FileID string
|
||||||
Filename string
|
Filename string
|
||||||
@@ -17,7 +17,7 @@ type UploadTestResult struct {
|
|||||||
Parts fileauth.UploadTokenParts
|
Parts fileauth.UploadTokenParts
|
||||||
}
|
}
|
||||||
|
|
||||||
// uploadTokenPartsForUpload 生成 §2.3.3 upload profile token(scope=仅 bucket)。
|
// uploadTokenPartsForUpload 生成 FileAuth.java upload profile token(scope=仅 bucket)。
|
||||||
func (r *Runner) uploadTokenPartsForUpload() (fileauth.UploadTokenParts, error) {
|
func (r *Runner) uploadTokenPartsForUpload() (fileauth.UploadTokenParts, error) {
|
||||||
r.configMu.RLock()
|
r.configMu.RLock()
|
||||||
key := strings.TrimSpace(r.hyAppKey)
|
key := strings.TrimSpace(r.hyAppKey)
|
||||||
|
|||||||
Reference in New Issue
Block a user