@@ -419,8 +419,11 @@ impl OssClient {
let policy = serde_json ::to_string ( & policy_json )
. map_err ( | error | OssError ::SerializePolicy ( format! ( " 序列化 policy 失败: {error} " ) ) ) ? ;
let encoded_policy = BASE64_STANDARD . encode ( policy . as_bytes ( ) ) ;
let signature =
sign_v4_content ( & self . config . access_key_secret , & signature_scope , & encoded_policy ) ? ;
let signature = sign_v4_content (
& self . config . access_key_secret ,
& signature_scope ,
& encoded_policy ,
) ? ;
Ok ( OssPostObjectResponse {
signature_version : " v4 " ,
@@ -492,11 +495,8 @@ impl OssClient {
let canonical_uri = build_v4_canonical_uri ( & self . config . bucket , Some ( & object_key ) ) ;
let object_url_path = format! ( " / {} " , encode_url_path ( & object_key ) ) ;
let additional_headers = " host " ;
let canonical_headers = format! (
" host: {} . {} \n " ,
self . config . bucket ( ) ,
self . config . endpoint ( )
) ;
let canonical_headers =
format! ( " host: {} . {} \n " , self . config . bucket ( ) , self . config . endpoint ( ) ) ;
let canonical_query = build_canonical_query_string ( & query ) ;
let canonical_request = build_v4_canonical_request (
Method ::GET . as_str ( ) ,
@@ -506,10 +506,16 @@ impl OssClient {
additional_headers ,
OSS_UNSIGNED_PAYLOAD ,
) ;
let string_to_sign =
build_v4_string_to_sign ( query[ " x-oss-date " ] . as_str ( ) , & signature_scope , & canonical_request ) ;
let signature =
sign_v4_content ( & self . config . access_key_secret , & signature_scope , & string_to_sign ) ? ;
let string_to_sign = build_v4_string_to_sign (
query [ " x-oss-date " ] . as_str ( ) ,
& signature_scope ,
& canonical_request ,
) ;
let signature = sign_v4_content (
& self . config . access_key_secret ,
& signature_scope ,
& string_to_sign ,
) ? ;
query . insert ( " x-oss-signature " . to_string ( ) , signature ) ;
let signed_url = format! (
" {} {} ? {} " ,
@@ -1036,8 +1042,13 @@ fn signed_request_builder(
additional_headers ,
& body_sha256 ,
) ;
let string_to_sign = build_v4_string_to_sign ( & signed_at_text , & signature_scope , & canonical_request ) ;
let signature = sign_v4_content ( config . access_key_secret ( ) , & signature_scope , & string_to_sign ) ? ;
let string_to_sign =
build_v4_string_to_sign ( & signed_at_text , & signature_scope , & canonical_request ) ;
let signature = sign_v4_content (
config . access_key_secret ( ) ,
& signature_scope ,
& string_to_sign ,
) ? ;
let mut builder = client
. request ( method , target_url )
. header ( " x-oss-content-sha256 " , body_sha256 )
@@ -1065,33 +1076,23 @@ fn signed_request_builder(
}
fn build_v4_signature_scope ( endpoint : & str , signed_at : OffsetDateTime ) -> Result < String , OssError > {
let date = signed_at
. date ( )
. to_string ( )
. replace ( '-' , " " ) ;
let date = signed_at . date ( ) . to_string ( ) . replace ( '-' , " " ) ;
let region = extract_oss_region ( endpoint ) ? ;
Ok ( format! ( " {date} / {region} / {OSS_V4_SERVICE} / {OSS_V4_REQUEST} " ) )
}
fn build_v4_signature_date ( signed_at : OffsetDateTime ) -> Result < String , OssError > {
let date = signed_at
. date ( )
. to_string ( )
. replace ( '-' , " " ) ;
let time = signed_at
. time ( )
. to_string ( )
. split ( '.' )
. next ( )
. unwrap_or ( " 00:00:00 " )
. replace ( ':' , " " ) ;
if time . len ( ) ! = 6 {
return Err ( OssError ::Sign ( " OSS V4 签名时间格式化失败 " . to_string ( ) ) ) ;
}
Ok ( format! ( " {date} T {time} Z " ) )
// 中文注释: time::Time 的 Display 在小时小于 10 时不会稳定补零, OSS V4 必须使用固定宽度 UTC 时间。
Ok ( format! (
" {:04} {:02} {:02} T {:02} {:02} {:02} Z " ,
signed_at . year ( ) ,
u8 ::from ( signed_at . month ( ) ) ,
signed_at . day ( ) ,
signed_at . hour ( ) ,
signed_at . minute ( ) ,
signed_at . second ( )
) )
}
fn build_v4_canonical_uri ( bucket : & str , object_key : Option < & str > ) -> String {
@@ -1116,9 +1117,7 @@ fn extract_oss_region(endpoint: &str) -> Result<String, OssError> {
. map ( str ::to_string )
. filter ( | region | ! region . is_empty ( ) )
. ok_or_else ( | | {
OssError ::InvalidConfig ( format! (
" OSS endpoint 无法解析 region, 当前值: {endpoint} "
) )
OssError ::InvalidConfig ( format! ( " OSS endpoint 无法解析 region, 当前值: {endpoint} " ) )
} )
}
@@ -1131,7 +1130,10 @@ fn sign_v4_content(
Ok ( hex_sha256_hmac ( & signing_key , content . as_bytes ( ) ) )
}
fn build_v4_signing_key ( access_key_secret : & str , signature_scope : & str ) -> Result < Vec < u8 > , OssError > {
fn build_v4_signing_key (
access_key_secret : & str ,
signature_scope : & str ,
) -> Result < Vec < u8 > , OssError > {
let mut parts = signature_scope . split ( '/' ) ;
let date = parts
. next ( )
@@ -1160,8 +1162,7 @@ fn hmac_sha256_raw(key: &[u8], content: &str) -> Result<Vec<u8>, OssError> {
}
fn hex_sha256_hmac ( key : & [ u8 ] , content : & [ u8 ] ) -> String {
let mut signer = HmacSha256 ::new_from_slice ( key )
. expect ( " HMAC-SHA256 accepts keys of any size " ) ;
let mut signer = HmacSha256 ::new_from_slice ( key ) . expect ( " HMAC-SHA256 accepts keys of any size " ) ;
signer . update ( content ) ;
hex_lower ( & signer . finalize ( ) . into_bytes ( ) )
}
@@ -1213,7 +1214,13 @@ fn build_v4_canonical_headers(headers: &BTreeMap<String, String>) -> String {
fn build_canonical_query_string ( params : & BTreeMap < String , String > ) -> String {
params
. iter ( )
. map ( | ( key , value ) | format! ( " {} = {} " , encode_url_query_value ( key ) , encode_url_query_value ( value ) ) )
. map ( | ( key , value ) | {
format! (
" {} = {} " ,
encode_url_query_value ( key ) ,
encode_url_query_value ( value )
)
} )
. collect ::< Vec < _ > > ( )
. join ( " & " )
}
@@ -1327,18 +1334,19 @@ mod tests {
response . form_fields . signature_version ,
OSS_V4_ALGORITHM . to_string ( )
) ;
assert! ( response
assert! (
response
. form_fields
. credential
. starts_with ( " test-access-key-id/ " ) ) ;
assert! ( response
. form_fields
. credential
. ends_with ( " /cn-shanghai/oss/aliyun_v4_request " ) ) ;
assert_eq! (
response . form_fields . date . len ( ) ,
" 20260507T120000Z " . len ( )
. starts_with ( " test-access-key-id/ " )
) ;
assert! (
response
. form_fields
. credential
. ends_with ( " /cn-shanghai/oss/aliyun_v4_request " )
) ;
assert_eq! ( response . form_fields . date . len ( ) , " 20260507T120000Z " . len ( ) ) ;
assert_eq! (
response . form_fields . metadata . get ( " x-oss-meta-asset-kind " ) ,
Some ( & " character-visual " . to_string ( ) )
@@ -1441,13 +1449,48 @@ mod tests {
. signed_url
. contains ( " x-oss-signature-version=OSS4-HMAC-SHA256 " )
) ;
assert! ( response
assert! (
response
. signed_url
. contains ( " x-oss-credential=test-access-key-id%2F " ) ) ;
. contains ( " x-oss-credential=test-access-key-id%2F " )
) ;
assert! ( response . signed_url . contains ( " &x-oss-expires=300 " ) ) ;
assert! ( response . signed_url . contains ( " &x-oss-signature= " ) ) ;
}
#[ test ]
fn sign_get_object_url_uses_square_hole_object_key_without_bucket_prefix ( ) {
let client = OssClient ::new (
OssConfig ::new (
" xushi-dev " . to_string ( ) ,
" oss-cn-shanghai.aliyuncs.com " . to_string ( ) ,
" test-access-key-id " . to_string ( ) ,
" test-access-key-secret " . to_string ( ) ,
DEFAULT_READ_EXPIRE_SECONDS ,
DEFAULT_POST_EXPIRE_SECONDS ,
DEFAULT_POST_MAX_SIZE_BYTES ,
DEFAULT_SUCCESS_ACTION_STATUS ,
)
. expect ( " OSS config should be valid " ) ,
) ;
let response = client
. sign_get_object_url ( OssSignedGetObjectUrlRequest {
object_key : " generated-square-hole-assets/square-hole-session-546d881972684be2980a2a882cd0cc71/square-hole-profile-134411276ce1469cbe398f946a25d7f8/square-hole-shape-image/rabbit-option/asset-1777979289912039/image.png " . to_string ( ) ,
expire_seconds : Some ( 300 ) ,
} )
. expect ( " square hole object key should build signed url " ) ;
assert_eq! ( response . bucket , " xushi-dev " . to_string ( ) ) ;
assert_eq! (
response . object_key ,
" generated-square-hole-assets/square-hole-session-546d881972684be2980a2a882cd0cc71/square-hole-profile-134411276ce1469cbe398f946a25d7f8/square-hole-shape-image/rabbit-option/asset-1777979289912039/image.png " . to_string ( )
) ;
assert! ( response
. signed_url
. starts_with ( " https://xushi-dev.oss-cn-shanghai.aliyuncs.com/generated-square-hole-assets/square-hole-session-546d881972684be2980a2a882cd0cc71/square-hole-profile-134411276ce1469cbe398f946a25d7f8/square-hole-shape-image/rabbit-option/asset-1777979289912039/image.png? " ) ) ;
}
#[ test ]
fn sign_get_object_url_rejects_unsupported_prefix ( ) {
let client = build_client ( ) ;