using System.Globalization; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; namespace JiDangJia.Server; // Only the server owns payment keys. Prices and entitlement always come from the server catalog. public sealed class AlipayGateway : IDisposable { private const string Gateway="https://openapi.alipay.com/gateway.do"; private readonly object sync=new(); private readonly RSA? merchant,alipay; private readonly string appId="",sellerId="",notifyUrl="",returnUrl=""; public bool Enabled {get;} public string Message=>Enabled?"支付宝付款成功后自动发放卡密":"支付宝暂未开通,请扫码联系客服购买"; public AlipayGateway(IConfiguration config) { if(!bool.TryParse(config["JDJ_ALIPAY_ENABLED"],out var enabled)||!enabled)return; try { appId=config["JDJ_ALIPAY_APP_ID"]??"";sellerId=config["JDJ_ALIPAY_SELLER_ID"]??""; if(!Regex.IsMatch(appId,@"^\d{16,32}$")||!Regex.IsMatch(sellerId,@"^\d{16,28}$"))return; if(!Uri.TryCreate(config["JDJ_ALIPAY_PUBLIC_BASE_URL"],UriKind.Absolute,out var root)||root.Scheme is not ("http" or "https")||root.IsLoopback||root.UserInfo.Length>0||root.Query.Length>0||root.Fragment.Length>0||root.AbsolutePath!="/")return; notifyUrl=new Uri(root,"api/purchase/alipay/notify").AbsoluteUri;returnUrl=new Uri(root,"purchase/return").AbsoluteUri; merchant=RSA.Create();merchant.ImportFromPem(File.ReadAllText(PrivateStorage.OutsideArtifacts(config["JDJ_ALIPAY_PRIVATE_KEY_FILE"]??""))); alipay=RSA.Create();alipay.ImportFromPem(File.ReadAllText(PrivateStorage.OutsideArtifacts(config["JDJ_ALIPAY_PUBLIC_KEY_FILE"]??""))); if(merchant.KeySize<2048||alipay.KeySize<2048)return; merchant.SignData("configuration-check"u8,HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1); Enabled=true; } catch(Exception ex)when(ex is IOException or UnauthorizedAccessException or ArgumentException or CryptographicException){Enabled=false;} } private static string Canonical(IReadOnlyDictionary values,bool notification=false)=>string.Join("&",values.Where(p=>p.Value.Length>0&&p.Key!="sign"&&(!notification||p.Key!="sign_type")).OrderBy(p=>p.Key,StringComparer.Ordinal).Select(p=>p.Key+"="+p.Value)); public string Checkout(string id,int amountCents,DateTimeOffset expires) { if(!Enabled)throw new CommunityException(503,Message); var values=new Dictionary(StringComparer.Ordinal) { ["app_id"]=appId,["method"]="alipay.trade.wap.pay",["format"]="JSON",["charset"]="utf-8",["sign_type"]="RSA2", ["timestamp"]=DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromHours(8)).ToString("yyyy-MM-dd HH:mm:ss",CultureInfo.InvariantCulture),["version"]="1.0", ["notify_url"]=notifyUrl,["return_url"]=returnUrl, ["biz_content"]=JsonSerializer.Serialize(new{out_trade_no=id,total_amount=(amountCents/100m).ToString("0.00",CultureInfo.InvariantCulture),subject=ServerProduct.Name+"卡密授权",seller_id=sellerId,product_code="QUICK_WAP_PAY",goods_type="0",time_expire=expires.ToOffset(TimeSpan.FromHours(8)).ToString("yyyy-MM-dd HH:mm",CultureInfo.InvariantCulture),quit_url=returnUrl}) }; lock(sync)values["sign"]=Convert.ToBase64String(merchant!.SignData(Encoding.UTF8.GetBytes(Canonical(values)),HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1)); return Gateway+"?"+string.Join("&",values.OrderBy(p=>p.Key,StringComparer.Ordinal).Select(p=>Uri.EscapeDataString(p.Key)+"="+Uri.EscapeDataString(p.Value))); } public bool Verify(IReadOnlyDictionary values) { if(!Enabled||values.Count>80||values.Any(p=>p.Key.Length>100||p.Value.Length>16384)||values.GetValueOrDefault("sign_type")!="RSA2"||values.GetValueOrDefault("app_id")!=appId||values.GetValueOrDefault("seller_id")!=sellerId||values.TryGetValue("charset",out var charset)&&!charset.Equals("utf-8",StringComparison.OrdinalIgnoreCase))return false; try { var signature=Convert.FromBase64String(values.GetValueOrDefault("sign")??""); lock(sync)return alipay!.VerifyData(Encoding.UTF8.GetBytes(Canonical(values,true)),signature,HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1); } catch(Exception ex)when(ex is FormatException or CryptographicException){return false;} } public void Dispose(){merchant?.Dispose();alipay?.Dispose();} }