.NET 前後分离 Web API 蓝新金流串接

本文将介绍在前後分离的状况下,後端如何与前端配合制作 Web API ,串接蓝新金流服务。

.NET Framework 4.7.2

开发环境

  • Visual Studio 2019
  • AspNet.Mvc version=”5.2.7"
  • AspNet.WebApi version=”5.2.7"
  • EntityFramework version="6.1.3"

Web API 蓝新金流串接

蓝新资料

参考资料

蓝新注册及设定

  1. 至蓝新测试帐号页面注册个人会员并登入,於会员中心⇒商店管理⇒开立商店,开立"网路商店"

  2. 开立商店完成後,回到商店资料设定,进入商店的详细资料页,於金流特约商店设定中,启用的项目的支付方式於本范例只启用 信用卡一次付清WebATM,其它项目先设为不启用

    https://ithelp.ithome.com.tw/upload/images/20220314/20139487PJAtjEV8b1.jpg

  3. 点选生成 API串接金钥

    https://ithelp.ithome.com.tw/upload/images/20220314/201394878gKSLzi486.jpg

  4. 上传商店 Logo

程序码实作

  1. 後端建立 CryptoUtil.cs 类别档,用於加解密

    using System;
    using System.Security.Cryptography;
    using System.Text;
    
    namespace MyProject.Security
    {
        /// <summary>
        /// 蓝新加解密 Util
        /// </summary>
        public class CryptoUtil
        {
            /// <summary>
            /// 字串加密 AES
            /// </summary>
            /// <param name="source">加密前字串</param>
            /// <param name="cryptoKey">加密金钥</param>
            /// <param name="cryptoIV">cryptoIV</param>
            /// <returns>加密後字串</returns>
            public static byte[] EncryptAES(byte[] source, string cryptoKey, string cryptoIV)
            {
                byte[] dataKey = Encoding.UTF8.GetBytes(cryptoKey);
                byte[] dataIV = Encoding.UTF8.GetBytes(cryptoIV);
    
                using (var aes = Aes.Create()) {
                    aes.Mode = CipherMode.CBC;
                    aes.Padding = PaddingMode.PKCS7;
                    aes.Key = dataKey;
                    aes.IV = dataIV;
    
                    using (var encryptor = aes.CreateEncryptor()) {
                        return encryptor.TransformFinalBlock(source, 0, source.Length);
                    }
                }
            }
    
            /// <summary>
            /// 字串解密 AES
            /// </summary>
            /// <param name="source">解密前字串</param>
            /// <param name="cryptoKey">解密金钥</param>
            /// <param name="cryptoIV">cryptoIV</param>
            /// <returns>解密後字串</returns>
            public static byte[] DecryptAES(byte[] source, string cryptoKey, string cryptoIV)
            {
                byte[] dataKey = Encoding.UTF8.GetBytes(cryptoKey);
                byte[] dataIV = Encoding.UTF8.GetBytes(cryptoIV);
    
                using (var aes = Aes.Create()) {
                    aes.Mode = CipherMode.CBC;
                    // 无法直接用 PaddingMode.PKCS7,会跳"填补无效,而且无法移除。"
                    // 所以改为 PaddingMode.None 并搭配 RemovePKCS7Padding
                    aes.Padding = PaddingMode.None;
                    aes.Key = dataKey;
                    aes.IV = dataIV;
    
                    using (var decryptor = aes.CreateDecryptor()) {
                        return RemovePKCS7Padding(decryptor.TransformFinalBlock(source, 0, source.Length));
                    }
                }
            }
    
            /// <summary>
            /// 加密後再转 16 进制字串
            /// </summary>
            /// <param name="source">加密前字串</param>
            /// <param name="cryptoKey">加密金钥</param>
            /// <param name="cryptoIV">cryptoIV</param>
            /// <returns>加密後的字串</returns>
            public static string EncryptAESHex(string source, string cryptoKey, string cryptoIV)
            {
                string result = string.Empty;
    
                if (!string.IsNullOrEmpty(source)) {
                    var encryptValue = EncryptAES(Encoding.UTF8.GetBytes(source), cryptoKey, cryptoIV);
    
                    if (encryptValue != null) {
                        result = BitConverter.ToString(encryptValue)?.Replace("-", string.Empty)?.ToLower();
                    }
                }
    
                return result;
            }
    
            /// <summary>
            /// 16 进制字串解密
            /// </summary>
            /// <param name="source">加密前字串</param>
            /// <param name="cryptoKey">加密金钥</param>
            /// <param name="cryptoIV">cryptoIV</param>
            /// <returns>解密後的字串</returns>
            public static string DecryptAESHex(string source, string cryptoKey, string cryptoIV)
            {
                string result = string.Empty;
    
                if (!string.IsNullOrEmpty(source)) {
                    // 将 16 进制字串 转为 byte[] 後
                    byte[] sourceBytes = GetByteArray(source);
    
                    if (sourceBytes != null) {
                        // 使用金钥解密後,转回 加密前 value
                        result = Encoding.UTF8.GetString(DecryptAES(sourceBytes, cryptoKey, cryptoIV)).Trim();
                    }
                }
    
                return result;
            }
    
            /// <summary>
            /// 字串加密 SHA256
            /// </summary>
            /// <param name="source">加密前字串</param>
            /// <returns>加密後字串</returns>
            public static string EncryptSHA256(string source)
            {
                string result = string.Empty;
    
                using (SHA256 algorithm = SHA256.Create()) {
                    var hash = algorithm.ComputeHash(Encoding.UTF8.GetBytes(source));
    
                    if (hash != null) {
                        result = BitConverter.ToString(hash)?.Replace("-", string.Empty)?.ToUpper();
                    }
    
                }
    
                return result;
            }
    
            /// <summary>
            /// 将 16 进位字串转换为 byteArray
            /// </summary>
            /// <param name="source">欲转换之字串</param>
            /// <returns></returns>
            public static byte[] GetByteArray(string source)
            {
                byte[] result = null;
    
                if (!string.IsNullOrWhiteSpace(source)) {
                    var outputLength = source.Length / 2;
                    var output = new byte[outputLength];
    
                    for (var i = 0; i < outputLength; i++) {
                        output[i] = Convert.ToByte(source.Substring(i * 2, 2), 16);
                    }
                    result = output;
                }
    
                return result;
            }
    
            private static byte[] RemovePKCS7Padding(byte[] data)
            {
                int iLength = data[data.Length - 1];
                var output = new byte[data.Length - iLength];
                Buffer.BlockCopy(data, 0, output, 0, output.Length);
                return output;
            }
        }
    
    }
    
  2. 前端 建立 "确认商品" 页面,点选按钮後将商品资料送到後端接收 API,夹带登入的 token 用来取得购买者身份 (付款前)

  3. 後端建立接收使用者购买内容 API (付款前)

    [HttpPost]
    public IHttpActionResult SetChargeData(ChargeRequest chargeData)
    {
        // Do Something ~ (相关资料检查处理,成立订单加入资料库,并将订单付款状态设为未付款)
    
        // 整理金流串接资料
        // 加密用金钥
        string hashKey = "填入生成的 HashKey";
        string hashIV = "填入生成的 HashIV";
    
        // 金流接收必填资料
        string merchantID = "填入商店代号";
        string tradeInfo = "";
        string tradeSha = "";
        string version = "2.0"; // 参考文件串接程序版本
    
        // tradeInfo 内容,导回的网址都需为 https 
        string respondType = "JSON"; // 回传格式
        string timeStamp = ((int)(dateTimeNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds).ToString();
        string merchantOrderNo = timeStamp +"_"+ "订单ID"; // 底线後方为订单ID,解密比对用,不可重覆(规则参考文件)
        string amt = "订单金额";
        string itemDesc = "商品资讯";
        string tradeLimit = "600"; // 交易限制秒数
        string notifyURL = @"https://" + Request.RequestUri.Host + "NotifyURL"; // NotifyURL 填後端接收蓝新付款结果的 API 位置,如 : /api/users/getpaymentdata
        string returnURL = "付款完成导回页面网址" + "/" + "订单ID";  // 前端可用 Status: SUCCESS 来判断付款成功,网址夹带可拿来取得活动内容
        string email = "消费者信箱"; // 通知付款完成用
        string loginType = "0"; // 0不须登入蓝新金流会员
    
        // 将 model 转换为List<KeyValuePair<string, string>>
        List<KeyValuePair<string, string>> tradeData = new List<KeyValuePair<string, string>>() {
            new KeyValuePair<string, string>("MerchantID", merchantID),
            new KeyValuePair<string, string>("RespondType", respondType),
            new KeyValuePair<string, string>("TimeStamp", timeStamp),
            new KeyValuePair<string, string>("Version", version),
            new KeyValuePair<string, string>("MerchantOrderNo", merchantOrderNo),
            new KeyValuePair<string, string>("Amt", amt),
            new KeyValuePair<string, string>("ItemDesc", itemDesc),
            new KeyValuePair<string, string>("TradeLimit", tradeLimit),
            new KeyValuePair<string, string>("NotifyURL", notifyURL),
            new KeyValuePair<string, string>("ReturnURL", returnURL),
            new KeyValuePair<string, string>("Email", email),
            new KeyValuePair<string, string>("LoginType", loginType)
        };
    
        // 将 List<KeyValuePair<string, string>> 转换为 key1=Value1&key2=Value2&key3=Value3...
        var tradeQueryPara = string.Join("&", tradeData.Select(x => $"{x.Key}={x.Value}"));
        // AES 加密
        tradeInfo = CryptoUtil.EncryptAESHex(tradeQueryPara, hashKey, hashIV);
        // SHA256 加密
        tradeSha = CryptoUtil.EncryptSHA256($"HashKey={hashKey}&{tradeInfo}&HashIV={hashIV}");
    
        // 送出金流串接用资料,给前端送蓝新用
        return Ok(new
        {
            Status = true,
            PaymentData = new
            {
                MerchantID = merchantID,
                TradeInfo = tradeInfo,
                TradeSha = tradeSha,
                Version = version
            }
        });
    }
    
  4. 前端 将後端加密回传的资料填入後,用表单送出到蓝新处理页面 (栏位内容设为隐藏)

    <!-- 用表单送给蓝新 -->
    <form name='Newebpay' method='post' action='https://ccore.newebpay.com/MPG/mpg_gateway'>
        <!-- 设定 hidden 可以隐藏不用给使用者看的资讯 -->
        <!-- 蓝新金流商店代号 -->
        <input type='hidden' id='MerchantID' name='MerchantID' value='填入後端回传的 MerchantID'>
        <!-- 交易资料透过 Key 及 IV 进行 AES 加密 -->
        <input type='hidden' id='TradeInfo' name='TradeInfo' value='填入後端回传的 TradeInfo'>
        <!-- 经过上述 AES 加密过的字串,透过商店 Key 及 IV 进行 SHA256 加密 -->
        <input type='hidden' id='TradeSha' name='TradeSha' value='填入後端回传的 TradeSha'>
        <!-- 串接程序版本 -->
        <input type='hidden' id='Version' name='Version' value='填入後端回传的 Version'>
        <!-- 直接执行送出 -->
        <input type='submit' value='前往付款'>
    </form>
    
  5. 测试用付款页面,付款完成後会导回 ReturnURL,并同时将付款结果资料传到 NotifyURL

    • 测试状态下,付款选 WebATM 直接点选银行後送出即可进行付款
    • 测试状态下,付款选 信用卡一次付清 文件提供卡号 : 4000-2211-1111-1111,效期填大於今天,验证码 3 码乱填
  6. 後端建立蓝新回传资料栏位类别档 NewebPayReturn.cs 及 解密资料栏位类别档 PaymentResult.cs

    namespace MyProject.Security
    {
        public class NewebPayReturn
        {
            public string Status { get; set; }
            public string MerchantID { get; set; }
            public string Version { get; set; }
            public string TradeInfo { get; set; }
            public string TradeSha { get; set; }
        }
    }
    
    namespace MyProject.Security
    {
        public class PaymentResult
        {
            public string Status { get; set; }
            public string Message { get; set; }
            public Result Result { get; set; }
        }
    
        public class Result
        {
            public string MerchantID { get; set; }
            public string Amt { get; set; }
            public string TradeNo { get; set; }
            public string MerchantOrderNo { get; set; }
            public string RespondType { get; set; }
            public string IP { get; set; }
            public string EscrowBank { get; set; }
            public string PaymentType { get; set; }
            public string RespondCode { get; set; }
            public string Auth { get; set; }
            public string Card6No { get; set; }
            public string Card4No { get; set; }
            public string Exp { get; set; }
            public string TokenUseStatus { get; set; }
            public string InstFirst { get; set; }
            public string InstEach { get; set; }
            public string Inst { get; set; }
            public string ECI { get; set; }
            public string PayTime { get; set; }
            public string PaymentMethod { get; set; }
        }
    }
    
  7. 後端建立接收付款资讯内容 API (串接蓝新-付款完)

    [HttpPost]
    public HttpResponseMessage GetPaymentData(NewebPayReturn data)
    {
        // 付款失败跳离执行
    	var response = Request.CreateResponse(HttpStatusCode.OK);
        if (!data.Status.Equals("SUCCESS")) return response;
    
        // 加密用金钥
        string hashKey = "填入生成的 HashKey";
        string hashIV = "填入生成的 HashIV";
        // AES 解密
        string decryptTradeInfo = CryptoUtil.DecryptAESHex(data.TradeInfo, hashKey, hashIV);
        PaymentResult result = JsonConvert.DeserializeObject<PaymentResult>(decryptTradeInfo);
        // 取出交易记录资料库的订单ID
        string[] orderNo = result.Result.MerchantOrderNo.Split('_');
        int logId = Convert.ToInt32(orderNo[1]);
    
        // 用取得的"订单ID"修改资料库此笔订单的付款状态为 true
    
        // 用取得的"订单ID"寄出付款完成订单成立,商品准备出货通知信
    
        return response;
    }
    
  8. 前端 使用 ReturnURL 网址夹带的"订单ID"向後端取得订单资料,并显示於画面告知用户

    • 前端跟後端都会收到 Status + MerchantID + Version + TradeInfo + TradeSha
    • 所以前端可用 Status=SUCCESS 来判断付款有没有成功

实际流程

https://ithelp.ithome.com.tw/upload/images/20220314/201394871haWaGe4iD.png


<<:  JS [笔记] debounce、throttle

>>:  8. STM32-PWM(上)

Unity与Photon的新手相遇旅途 | Day29-Unity 发布到Android手机上

今天的教学为教大家如何将Unity build完之後安装到Android手机上面测试。 ...

Day 15. slate × Interfaces × Iteration

JS 的 Iteration 在 Slate 里头占了不小的份量,即便有 Ref concepts...

Flutter体验 Day 5-Widget 乐高积木

Widget乐高积木 在开始实作App之前,我们需要认识 Flutter 程序架构的基本概念。 回顾...

Day 23: 元件原则 — 耦合性 (待改进中... )

「本章描述的依赖性管理度量,可以用来量测一个设计有多符合『好的依赖及抽象』模式。经验告诉我们,依赖...

110/12 - 把照片储存在Pictures/应用程序名称资料夹 - 2

Android 11开始把getExternalStoragePublicDirectory标记弃用...