NetCore服务器对接小程序直传阿里云OSS
NetCore服务器对接小程序直传阿里云OSS
问题描述:小程序只支持POST方式上传文件,阿里云生成上传链接只能为PUT上传。小程序得使用前端直传方式。本示例为服务器端签名直传并设置上传回调。
PolicyToken.cs
internal class PolicyToken
{
public string accessid { get; set; }
public string policy { get; set; }
public string signature { get; set; }
public string dir { get; set; }
public string host { get; set; }
public string expire { get; set; }
public string callback { get; set; }
}
CallbackParm.cs
internal class CallbackParam
{
public string callbackUrl { get; set; }
public string callbackBody { get; set; }
public string callbackBodyType { get; set; }
}
生成 PolicyToken
CallbackBody 系统参数就不过多说明,详见上面的官方文档,主要说说自定义参数,在构造 Form 表单的参数时,参数名,可以任意命名,但是有两点要注意:
- 自定义参数的格式,必须是
${x:<占位符>}
,x:
一定不能少- 自定义参数的占位符,就是
${x:<占位符>}
部分,花括号内部的名称,必须全部小写,比如:可以是userName=${x:username}&Age=${x:age}
也可以是username=${x:username}&age=${x:age}
,总之,注意占位符全部小写就行了,参数名,按照任意命名方式都可以,回调接口内注意读取就行了- 可以不用自定义占位符,直接对参数进行赋值,比如:`username=张三&age=20
public string GetPolicyToken(string bucketName, string uploadDir, string callbackUrl, long expireTime)
{
//expireTime
var expireDateTime = DateTime.Now.AddSeconds(expireTime);
// example of policy
//{
// "expiration": "2020-05-01T12:00:00.000Z",
// "conditions": [
// ["content-length-range", 0, 1048576000]
// ["starts-with", "$key", "user-dir-prefix/"]
// ]
//}
//policy
var policyConds = new PolicyConditions();
policyConds.AddConditionItem(PolicyConditions.CondContentLengthRange, 100, 1024 * 1024 * 400);
// 指定key
policyConds.AddConditionItem(MatchMode.Exact, PolicyConditions.CondKey, uploadDir);
//policyConds.AddConditionItem(MatchMode.StartWith, PolicyConditions.CondKey, uploadDir);
var policy = _ossClient.GeneratePostPolicy(expireDateTime, policyConds);
var policy_base64 = EncodeBase64("utf-8", policy);
var signature = ComputeSignature(_aliyunOSSConfig.AccessKeySecret, policy_base64);
//callback
var callback = new CallbackParam();
callback.callbackUrl = callbackUrl;
callback.callbackBody = "filename=${object}&size=${size}&mimeType=${mimeType}";
callback.callbackBodyType = "application/x-www-form-urlencoded";
var callback_string = JsonConvert.SerializeObject(callback);
var callback_string_base64 = EncodeBase64("utf-8", callback_string);
var policyToken = new PolicyToken();
policyToken.accessid = _aliyunOSSConfig.AccessKeyId;
policyToken.host = $"https://{bucketName}.{_aliyunOSSConfig.Host}";
policyToken.policy = policy_base64;
policyToken.signature = signature;
policyToken.expire = ToUnixTime(expireDateTime);
policyToken.callback = callback_string_base64;
policyToken.dir = uploadDir;
return JsonConvert.SerializeObject(policyToken);
}
private static string ToUnixTime(DateTime dtime)
{
const long ticksOf1970 = 621355968000000000;
var expires = ((dtime.ToUniversalTime().Ticks - ticksOf1970) / 10000000L)
.ToString(CultureInfo.InvariantCulture);
return expires;
}
private static string ComputeSignature(string key, string data)
{
using (var algorithm = new HMACSHA1())
{
algorithm.Key = Encoding.UTF8.GetBytes(key.ToCharArray());
return Convert.ToBase64String(
algorithm.ComputeHash(Encoding.UTF8.GetBytes(data.ToCharArray())));
}
}
private static string EncodeBase64(string code_type, string code)
{
string encode = "";
byte[] bytes = Encoding.GetEncoding(code_type).GetBytes(code);
try
{
encode = Convert.ToBase64String(bytes);
}
catch
{
encode = code;
}
return encode;
}
回调验签
验签步骤
- 获取
Authoriaztion
的值,进行Base64
解码,得到byte[]
。- 对
x-oss-pub-key-url
的值,进行Base64
解码,得到公钥的url
地址。- 校验公钥
url
地址,防止伪造,公钥地址为:
http://gosspublic.alicdn.com/
https://gosspublic.alicdn.com/
- 根据公钥
url
地址获取公钥内容。- 将
<请求路径>?<参数>\n<body内容>
或<请求路径>\n<body内容>
拼接,计算MD5
获取byte[]
(请求路径不包含Host
)。- 基于
RSA
,采用MD5
模型进行验签。- 将验证结果返回给OSS(OSS 仅接收 Json 格式的返回)。
- OSS 会将返回的内容直接返回给前端。
public async Task<bool> VerifySignature()
{
// Get the Authorization Base64 from Request
var request = _httpContextAccessor.HttpContext.Request;
if (!request.Headers.TryGetValue("Authorization", out var authInfo)
|| StringValues.IsNullOrEmpty(authInfo))
{
return false;
}
// Decode the Authorization from Request
var byteAuth = Convert.FromBase64String(authInfo);
// Decode the URL of PublicKey
if (!request.Headers.TryGetValue("x-oss-pub-key-url", out var tempPubKeyUrl)
|| StringValues.IsNullOrEmpty(tempPubKeyUrl))
{
return false;
}
var bytePubKeyUrl = Convert.FromBase64String(tempPubKeyUrl.ToString());
var pubKeyUrl = Encoding.ASCII.GetString(bytePubKeyUrl);
// 验证公钥域名
if (!pubKeyUrl.StartsWith("http://gosspublic.alicdn.com/", StringComparison.OrdinalIgnoreCase)
&& !pubKeyUrl.StartsWith("https://gosspublic.alicdn.com/", StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Get PublicKey from the URL
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(ValidateServerCertificate);
using var client = _httpClientFactory.CreateClient();
var pubKey = await client.GetStringAsync(pubKeyUrl);
var strPublicKeyContentBase64 = pubKey.Replace("-----BEGIN PUBLIC KEY-----\n", "").Replace("-----END PUBLIC KEY-----", "").Replace("\n", "");
var strPublicKeyContentXML = RSAPublicKeyString2XML(strPublicKeyContentBase64);
// Generate the New Authorization String according to the HttpRequest
var httpURL = request.Path.ToString() + request.QueryString.ToString();
// Read body
var dictBody = new Dictionary<string, string>();
if (request.HasFormContentType)
{
foreach (var item in request.Form)
{
dictBody.Add(item.Key, item.Value);
}
}
var httpBody = string.Join("&", dictBody.Select(per => $"{per.Key.UrlEncode(true)}={per.Value.UrlEncode(true)}"));
// StreamReader stream = new StreamReader(_httpContextAccessor.HttpContext.Request.Body);
// var httpBody = await stream.ReadToEndAsync();
var strAuthSourceForMD5 = string.Empty;
if (httpURL.Contains('?'))
{
var arrURL = httpURL.Split('?');
strAuthSourceForMD5 = string.Format("{0}?{1}\n{2}", System.Web.HttpUtility.UrlDecode(arrURL[0]), arrURL[1], httpBody);
}
else
{
strAuthSourceForMD5 = string.Format("{0}\n{1}", System.Web.HttpUtility.UrlDecode(httpURL), httpBody);
}
// MD5 hash bytes from the New Authorization String
var byteAuthMD5 = ByteMD5Encrypt32(strAuthSourceForMD5);
// Verify Signature
using var RSA = new RSACryptoServiceProvider();
try
{
RSA.FromXmlString(strPublicKeyContentXML);
}
catch (ArgumentNullException e)
{
throw new ArgumentNullException(string.Format("VerifySignature Failed : RSADeformatter.VerifySignature get null argument : {0} .", e));
}
catch (CryptographicException e)
{
throw new CryptographicException(string.Format("VerifySignature Failed : RSA.FromXmlString Exception : {0} .", e));
}
RSAPKCS1SignatureDeformatter RSADeformatter = new RSAPKCS1SignatureDeformatter(RSA);
RSADeformatter.SetHashAlgorithm("MD5");
var bVerifyResult = false;
try
{
bVerifyResult = RSADeformatter.VerifySignature(byteAuthMD5, byteAuth);
}
catch (ArgumentNullException e)
{
throw new ArgumentNullException(string.Format("VerifySignature Failed : RSADeformatter.VerifySignature get null argument : {0} .", e));
}
catch (CryptographicUnexpectedOperationException e)
{
throw new CryptographicUnexpectedOperationException(string.Format("VerifySignature Failed : RSADeformatter.VerifySignature Exception : {0} .", e));
}
return bVerifyResult;
}
public static byte[] ByteMD5Encrypt32(string password)
{
string cl = password;
using MD5 md5 = MD5.Create();
byte[] s = md5.ComputeHash(Encoding.UTF8.GetBytes(cl));
return s;
}
public static string RSAPublicKeyString2XML(string publicKey)
{
RsaKeyParameters publicKeyParam = (RsaKeyParameters)PublicKeyFactory.CreateKey(Convert.FromBase64String(publicKey));
return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent></RSAKeyValue>",
Convert.ToBase64String(publicKeyParam.Modulus.ToByteArrayUnsigned()),
Convert.ToBase64String(publicKeyParam.Exponent.ToByteArrayUnsigned()));
}
public static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
return true;
}
StringExtension.cs
public static string UrlEncode(this string content, bool needUpper = false)
{
if (string.IsNullOrEmpty(content))
{
return string.Empty;
}
if (!needUpper)
{
return HttpUtility.UrlEncode(content);
}
var result = new StringBuilder();
foreach (var per in content)
{
var temp = HttpUtility.UrlEncode(per.ToString());
if (temp.Length > 1)
{
result.Append(temp.ToUpper());
continue;
}
result.Append(per);
}
return result.ToString();
}
链接:阿里云 OSS Web 直传/回调/回调签名验证(.NET/C#/Layui) - 简书 (jianshu.com)