-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
488 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.