草庐IT

微信小程序开发实战10_2 小程序支付请求签名

go lang 2023-10-23 原文

为了保证支付接口使用的安全,微信支付平台在支付API中使用了一些用于接口安全调用的技术。在调用时接口需要使用商户私钥进行接口调用的签名,获取到微信支付平台的应答之后也需要对应答进行签名验证。微信的应答签名使用平台证书来进行签名验证,因此在调用支付接口前还需要实现平台证书的下载以及管理。另外微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密,因此开发者还需要了解如何使用APIv3密钥进行数据解密。在调用具体接口之前需要了解这是逻辑,并实现接口调用的一些基础代码。

11.1基本规则

商户接入微信支付,调用API必须遵循以下规则:
1)微信支付API v3使用 JSON 作为消息体的数据交换格式。请求须设置HTTP头部:

  • Content-Type: application/json
  • Accept: application/json
    2)请求的唯一标识
    微信支付给每个接收到的请求分配了一个唯一标识。请求的唯一标识包含在应答的HTTP头Request-ID中。
    3)错误信息
    微信支付API v3使用HTTP状态码来表示请求处理的结果。
  • 处理成功的请求,如果有应答的消息体将返回200,若没有应答的消息体将返回204。
  • 已经被成功接受待处理的请求,将返回202。
  • 请求处理失败时,如缺少必要的入参、支付时余额不足,将会返回4xx范围内的错误码。
  • 请求处理时发生了微信支付侧的服务系统错误,将返回500/501/503的状态码。这种情况比较少见。
    4)User Agent
    HTTP协议要求发起请求的客户端在每一次请求中都使用HTTP头 User-Agent来标识自己。微信支付API v3很可能会拒绝处理无User-Agent 的请求。

11.2请求签名

微信支付使用APIv3密钥对请求进行签名。微信支付会在收到请求后进行签名的验证。如果签名验证不通过,微信支付将会拒绝处理请求,并返回401 Unauthorized。
开发人员调用支付接口时需要按照以下的规则构造签名串。签名串一共有五行,每一行为一个参数,行尾以 \n结束,包括最后一行。
HTTP请求方法\n
URL\n
请求时间戳\n
请求随机串\n
请求报文主体\n

然后使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值。
微信支付要求请求使用HTTP Authorization头来传递签名。Authorization由认证类型和签名信息两个部分组成。具体内容为:

  • 认证类型,目前为WECHATPAY2-SHA256-RSA2048
  • 签名信息:包括发起请求的商户的商户号mchid,商户API证书的serial_no,请求随机串nonce_str,时间戳timestamp,签名值signature。
    Authorization 头的示例如下:
    Authorization:WECHATPAY2-SHA256-RSA2048 mchid=“1900009191”,nonce_str=“593BEC0C930BF1AFEB40B4A08C8FB242”,signature=“uOVRnA4qG…”,timestamp=“1554208460”,serial_no=“1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C”’
    下面我们一步步来实现向微信支付服务器发送一个POST请求,首先来看看如何生成向请求头中的Authorization信息。
    接下来首先给出商户数据结构的定义,定义商户对象是需要指定商户的类型(直连商户、服务商商户),以及商户的参数(商户号、商户关联的APPID), 以及为商户对象加载商户密钥以及商户证书。以下是商户结构的定义代码:
type MchWxapp struct {
   //商户类型 0直连商户 1服务商商户
   MchType       int
   //商户对应的appid
   Appid     string
   //商户号
   Mchid     string
   //商户的API v3密钥
   MchAPIKey  string
   //商户API私钥
   MchPrivateKey *rsa.PrivateKey
   //商户 API 证书
   MchCertificate *x509.Certificate
}

商户的API私钥用于生成调用签名,接下来给出商户密钥的加载代码:

func LoadPrivateKeyWithPath(path string) (privateKey *rsa.PrivateKey, err error) {
   privateKeyBytes, err := ioutil.ReadFile(path)
   if err != nil {
      return nil, err 
   }
   block, _ := pem.Decode([]byte(privateKeyStr))
   if block == nil {
      return nil, fmt.Errorf("decode private key err")
   }
   key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
   if err != nil {
      return nil, err 
   }
   privateKey, ok := key.(*rsa.PrivateKey)
   if !ok {
      return nil, fmt.Errorf("%s is not rsa private key", privateKeyStr)
   }
   return privateKey, nil
}

生成请求头Authorization信息时需要用到商户证书的SerialNumber,以下是商户证书的加载代码:

func LoadCertificateWithPath(path string) (certificate *x509.Certificate, err error) {
   certificateBytes, err := ioutil.ReadFile(path)
   if err != nil {
      return nil, err 
   }
   block, _ := pem.Decode([]byte(certificateStr))
   if block == nil {
      return nil, fmt.Errorf("decode certificate err")
   }
   certificate, err = x509.ParseCertificate(block.Bytes)
   if err != nil {
      return nil, err 
   }
}

接下来要进行签名串的构造以及对签名串进行签名,具体代码如下:

func GenerateWxPayReqHeader(ctx *MchParam, method string, rawUrl string, signBody string) (authorization string, err error){
   timestamp := time.Now().Unix()
   url, err := url.Parse(rawUrl)
   if err != nil {
      return "", err
   }
   nonce, err := GenerateNonce()
   if err != nil {
      return "", err
   }

   SignatureMessageFormat := "%s\n%s\n%d\n%s\n%s\n"
   message := fmt.Sprintf(SignatureMessageFormat, method, url.RequestURI(), timestamp, nonce, signBody)
   signatureResult, err := SignSHA256WithRSA(ctx.MchPrivateKey, message)
   if err != nil {
      return "", err
   }

   certSerialNo := fmt.Sprintf("%X", ctx.MchCertificate.SerialNumber)
   HeaderAuthorizationFormat := "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\""
   authorization = fmt.Sprintf(HeaderAuthorizationFormat, ctx.Mchid, nonce, timestamp, certSerialNo, signatureResult)
   return authorization, nil
}

代码中使用GenerateNonce()生成一个32个字节的请求随机串,并调用SignSHA256WithRSA对待签名串进行SHA256 with RSA签名。下面是函数SignSHA256WithRSA的实现:

func SignSHA256WithRSA(privateKey *rsa.PrivateKey, source string) 
	(signature string, err error) {
   h := crypto.Hash.New(crypto.SHA256)
   _, err = h.Write([]byte(source))
   if err != nil {
      return "", nil
   }
   hashed := h.Sum(nil)
   signatureByte, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed)
   if err != nil {
      return "", err
   }
   return base64.StdEncoding.EncodeToString(signatureByte), nil
}

最后来我们通过代码来看看如何通过HTTP的POST方法来调用支付接口。以下代码中去掉了响应数据签名验证的逻辑,响应数据签名验证稍后再来分析:

func WxPayPostV3(ctx *MchParam, url string, data []byte) (string, error) {
   token, err := GenerateWxPayReqHeader(ctx, http.MethodPost, url, string(data))
   if err != nil {
      log.Println(err)
      return "", err
   }

   request, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
   if err != nil {
      return "", err
   }
   request.Header.Add("Authorization", token)
   request.Header.Add("User-Agent", "go pay sdk")
   request.Header.Add("Content-type", "application/json;charset='utf-8'")
   request.Header.Add("Accept", "application/json")

   client := &http.Client{Timeout: 5 * time.Second}
   resp, err := client.Do(request)
   if err != nil {
      log.Println(err)
      return "", err
   }
   defer resp.Body.Close()

   result, _ := ioutil.ReadAll(resp.Body)
   if resp.StatusCode != 200 && resp.StatusCode != 204 {
      err := fmt.Errorf("status:%d;msg=%s", resp.StatusCode, string(result))
      log.Println(err)
      return string(result), err
   }

   return string(result), nil
}

有关微信小程序开发实战10_2 小程序支付请求签名的更多相关文章

  1. ruby - 在 Ruby 程序执行时阻止 Windows 7 PC 进入休眠状态 - 2

    我需要在客户计算机上运行Ruby应用程序。通常需要几天才能完成(复制大备份文件)。问题是如果启用sleep,它会中断应用程序。否则,计算机将持续运行数周,直到我下次访问为止。有什么方法可以防止执行期间休眠并让Windows在执行后休眠吗?欢迎任何疯狂的想法;-) 最佳答案 Here建议使用SetThreadExecutionStateWinAPI函数,使应用程序能够通知系统它正在使用中,从而防止系统在应用程序运行时进入休眠状态或关闭显示。像这样的东西:require'Win32API'ES_AWAYMODE_REQUIRED=0x0

  2. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  3. ruby - 在 Ruby 中编写命令行实用程序 - 2

    我想用ruby​​编写一个小的命令行实用程序并将其作为gem分发。我知道安装后,Guard、Sass和Thor等某些gem可以从命令行自行运行。为了让gem像二进制文件一样可用,我需要在我的gemspec中指定什么。 最佳答案 Gem::Specification.newdo|s|...s.executable='name_of_executable'...endhttp://docs.rubygems.org/read/chapter/20 关于ruby-在Ruby中编写命令行实用程序

  4. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

    我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

  5. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  6. ruby - 无法运行 Rails 2.x 应用程序 - 2

    我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

  7. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  8. ruby-on-rails - Rails HTML 请求渲染 JSON - 2

    在我的Controller中,我通过以下方式在我的index方法中支持HTML和JSON:respond_todo|format|format.htmlformat.json{renderjson:@user}end在浏览器中拉起它时,它会自然地以HTML呈现。但是,当我对/user资源进行内容类型为application/json的curl调用时(因为它是索引方法),我仍然将HTML作为响应。如何获取JSON作为响应?我还需要说明什么? 最佳答案 您应该将.json附加到请求的url,提供的格式在routes.rb的路径中定义。这

  9. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  10. Ruby Sinatra 配置用于生产和开发 - 2

    我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm

随机推荐