Skip to content

Commit

Permalink
Merge branch 'pay'
Browse files Browse the repository at this point in the history
  • Loading branch information
feng19 committed Jun 29, 2021
2 parents 8f9fd1d + ab51d0e commit 0cc9df3
Show file tree
Hide file tree
Showing 8 changed files with 488 additions and 0 deletions.
53 changes: 53 additions & 0 deletions lib/wechat/builder/pay.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
defmodule WeChat.Builder.Pay do
@moduledoc false
defmacro __using__(options \\ []) do
options = Map.new(options)
requester = Map.get(options, :requester, WeChat.Requester.Pay)
storage = Map.get(options, :storage, WeChat.Storage.PayFile)
public_key = WeChat.Pay.Utils.decode_key(options.client_cert)
# private_key = WeChat.Pay.Utils.decode_key(options.client_key)

quote do
use Supervisor

def start_link(opts) do
opts = Map.new(opts)
requester_a = WeChat.Pay.get_requester_spec(:A, __MODULE__, opts.cacerts)
requester_b = WeChat.Pay.get_requester_spec(:B, __MODULE__, opts.cacerts)
WeChat.Pay.put_requester_opts(__MODULE__, :A, opts.serial_no)
refresher = Map.get(opts, :refresher, WeChat.Refresher.Pay)
children = [{refresher, {__MODULE__, opts}}, requester_a, requester_b]
opts = [strategy: :one_for_one, name: :"#{__MODULE__}.Supervisor"]
Supervisor.start_link(children, opts)
end

def get(url), do: get(url, [])

def get(url, opts) do
%{name: name, serial_no: serial_no} = WeChat.Pay.get_requester_opts(__MODULE__)

__MODULE__
|> unquote(requester).new(__MODULE__, name, serial_no)
|> Tesla.get(url, opts)
end

def post(url, body), do: post(url, body, [])

def post(url, body, opts) do
%{name: name, serial_no: serial_no} = WeChat.Pay.get_requester_opts(__MODULE__)

__MODULE__
|> unquote(requester).new(__MODULE__, name, serial_no)
|> Tesla.post(url, body, opts)
end

def mch_id, do: unquote(options.mch_id)
def api_secret_key, do: unquote(options.api_secret_key)
def storage, do: unquote(storage)
def client_cert, do: unquote(options.client_cert)
def client_key, do: unquote(options.client_key)
def public_key, do: unquote(public_key)
# def private_key, do: unquote(private_key)
end
end
end
23 changes: 23 additions & 0 deletions lib/wechat/pay/authorization.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule WeChat.Pay.Authorization do
@moduledoc """
微信支付 V3 Authorization 签名生成
[官方文档](https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml){:target="_blank"}
"""
@behaviour Tesla.Middleware
alias WeChat.Pay.Utils

@impl Tesla.Middleware
def call(env, next, options) do
mch_id = Keyword.fetch!(options, :mch_id)
serial_no = Keyword.fetch!(options, :serial_no)
private_key = Keyword.fetch!(options, :private_key)
token = Utils.get_token(mch_id, serial_no, private_key, env)

env
|> Tesla.put_headers([
{"Authorization", "WECHATPAY2-SHA256-RSA2048 #{token}"}
])
|> Tesla.run(next)
end
end
14 changes: 14 additions & 0 deletions lib/wechat/pay/certificates.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule WeChat.Pay.Certificates do
@moduledoc false
alias WeChat.Pay.Utils

# [获取平台证书列表](https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay5_1.shtml)
def certificates(client) do
with {:ok, %{body: %{data: certificates}}} when is_list(certificates) <-
client.get("/v3/certificates") do
IO.inspect(certificates, label: "certificates")
api_secret_key = client.api_secret_key()
{:ok, Enum.map(certificates, &Utils.decrypt_certificate(&1, api_secret_key))}
end
end
end
113 changes: 113 additions & 0 deletions lib/wechat/pay/pay.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
defmodule WeChat.Pay do
@moduledoc false

@typedoc "商户号"
@type mch_id :: binary
@typedoc "平台证书的序列号"
@type serial_no :: binary
@typedoc "平台证书列表"
@type cacerts :: [binary]
@typedoc "商户API证书"
@type client_cert :: binary
@typedoc "商户API私钥"
@type client_key :: binary
@type client :: module()
@type options :: Enumerable.t()

@doc false
defmacro __using__(options \\ []) do
quote do
use WeChat.Builder.Pay, unquote(options)
end
end

@doc "动态构建 client"
@spec build_client(client, options) :: {:ok, client}
def build_client(client, options) do
with {:module, module, _binary, _term} <-
Module.create(
client,
quote do
@moduledoc false
use WeChat.Builder.Pay, unquote(Macro.escape(options))
end,
Macro.Env.location(__ENV__)
) do
{:ok, module}
end
end

def put_requester_opts(client, id, serial_no) do
name = finch_name(client, id)

:persistent_term.put({:wechat, {client, :requester_opts}}, %{
id: id,
name: name,
serial_no: serial_no
})
end

def get_requester_opts(client) do
:persistent_term.get({:wechat, {client, :requester_opts}})
end

# 保存平台证书 serial_no => cert 的对应关系
def put_cert(client, serial_no, cert) do
:persistent_term.put({:wechat, {client, serial_no}}, cert)
end

# 获取平台证书 serial_no 对应的 cert
def get_cert(client, serial_no) do
:persistent_term.get({:wechat, {client, serial_no}})
end

def remove_cert(client, serial_no) do
:persistent_term.erase({:wechat, {client, serial_no}})
end

defp finch_name(client, id), do: :"#{client}.Finch.#{id}"

def get_requester_spec(id, client, cacerts) when is_atom(id) do
name = finch_name(client, id)

finch_pool =
Application.get_env(:wechat, :finch_pool, size: 32, count: 8) ++
[
conn_opts: [
transport_opts: [
cacerts: cacerts,
cert: client.client_cert(),
key: client.client_key()
]
]
]

options = [name: name, pools: %{:default => finch_pool}]
spec = Finch.child_spec(options)
%{spec | id: id}
end

# [平台证书更新指引](https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay5_0.shtml)
# * 证书切换
# * 通过 Supervisor 开启新的 Finch 进程
# * 然后 将新的 Finch 进程名写入到 :persistent_term 保存
# * 请求的时候,从 :persistent_term 获取 Finch 进程名,然后再请求
def start_next_requester(client, opts) do
%{id: now_id} = get_requester_opts(client)
id = List.delete([:A, :B], now_id) |> hd()
finch_spec = get_requester_spec(id, client, opts.cacerts)
sup = :"#{client}.Supervisor"

with :ok <- Supervisor.terminate_child(sup, id),
:ok <- Supervisor.delete_child(sup, id),
{:ok, _} = return <- Supervisor.start_child(sup, finch_spec) do
put_requester_opts(client, id, opts.serial_no)
return
end
end

def init_cacerts2storage(client, cacerts) do
storage = client.storage()
storage.store(client.mch_id(), :cacerts, cacerts)
end
end
103 changes: 103 additions & 0 deletions lib/wechat/pay/utils.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
defmodule WeChat.Pay.Utils do
@moduledoc false

# [证书和回调报文解密](https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml)
# {
# "serial_no": "5157F09EFDC096DE15EBE81A47057A7232F1B8E1",
# "effective_time ": "2018-06-08T10:34:56+08:00",
# "expire_time ": "2018-12-08T10:34:56+08:00",
# "encrypt_certificate": {
# "algorithm": "AEAD_AES_256_GCM",
# "nonce": "61f9c719728a",
# "associated_data": "certificate",
# "ciphertext": "sRvt… "
# }
# }
def decrypt_certificate(
%{
"serial_no" => serial_no,
"effective_time" => effective_time,
"expire_time" => expire_time,
"nonce" => iv,
"ciphertext" => ciphertext,
"associated_data" => associated_data
},
api_secret_key
) do
data = Base.decode64!(ciphertext)
len = byte_size(data) - 16
<<data::binary-size(len), tag::binary-size(16)>> = data

certificate =
:crypto.crypto_one_time_aead(
:aes_256_gcm,
api_secret_key,
iv,
data,
associated_data,
tag,
false
)

{:ok, effective_datetime, _utc_offset} = DateTime.from_iso8601(effective_time)
{:ok, expire_datetime, _utc_offset} = DateTime.from_iso8601(expire_time)

%{
"serial_no" => serial_no,
"effective_time" => effective_time,
"effective_timestamp" => DateTime.to_unix(effective_datetime),
"expire_time" => expire_time,
"expire_timestamp" => DateTime.to_unix(expire_datetime),
"certificate" => certificate
}
end

def load_pem!(path) do
path |> File.read!() |> decode_key()
end

def decode_key(binary) do
binary |> :public_key.pem_decode() |> hd() |> :public_key.pem_entry_decode()
end

# 使用[平台证书中的公钥]进行加密
def encrypt_secret_data(data, public_key) do
:public_key.encrypt_public(data, public_key, rsa_pad: :rsa_pkcs1_oaep_padding)
end

# 使用[商户API私钥]进行解密
def decrypt_secret_data(cipher_text, private_key) do
:public_key.decrypt_private(cipher_text, private_key, rsa_pad: :rsa_pkcs1_oaep_padding)
end

# 使用[平台证书中的公钥]进行验签
def verify(signature, timestamp, nonce, body, public_key) do
# signature = Base.decode64!(signature)
:public_key.verify("#{timestamp}\n#{nonce}\n#{body}\n", :sha256, signature, public_key)
end

# 使用[商户API私钥]进行签名
def sign(env, timestamp, nonce_str, private_key) do
method = to_string(env.method) |> String.upcase()

path =
case env.query do
[] -> env.url
query -> env.url <> "?" <> URI.encode_query(query)
end

"#{method}\n#{path}\n#{timestamp}\n#{nonce_str}\n#{env.body}\n"
|> :public_key.sign(:sha256, private_key)
|> Base.encode64()
end

def get_token(mch_id, serial_no, private_key, env) do
timestamp = WeChat.Utils.now_unix()
nonce_str = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
signature = sign(env, timestamp, nonce_str, private_key)

"mchid=\"#{mch_id}\",nonce_str=\"#{nonce_str}\",timestamp=\"#{timestamp}\",serial_no=\"#{
serial_no
}\",signature=\"#{signature}\""
end
end
Loading

0 comments on commit 0cc9df3

Please sign in to comment.