微信登录

微信PC扫码登录

准备工作

网站应用微信登录是基于OAuth2.0协议标准构建的微信OAuth2.0授权登录系统。 在进行微信OAuth2.0授权登录接入之前,在微信开放平台注册开发者账号,并拥有一个已审核通过的网站应用,并获得相应的AppID和AppSecret,申请微信登录且通过审核后,可开始接入流程。

因为这个开放平台注册账号需要钱,所以我们申请一个微信公众平台接口测试账号网站

接口配置信息

接口配置信息的url需要是一个能被外网访问的域名,本地链接是不可以的,如果没有域名可以用cpolar内网穿透

此url我们需要创建一个接口用来让微信校验是否可用

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping("/wxCheck")
public String wxSignatureCheck(
@RequestParam(value = "signature") String signature,
@RequestParam(value = "timestamp") String timestamp,
@RequestParam(value = "nonce") String nonce,
@RequestParam(value = "echostr") String echostr
){
log("收到微信校验请求,echostr:",echostr);
//校验是否微信的请求
return echostr;
}

根据要求返回echostr就可以了

体验接口权限表中的网页服务的网页账号填写回调网页域名

回调域名不需要http://开头 正确示例:7fa65291.r16.cpolar.top

授权流程说明

微信OAuth2.0授权登录让微信用户使用微信身份安全登录第三方应用或网站,在微信用户授权登录已接入微信OAuth2.0的第三方应用后,第三方可以获取到用户的接口调用凭证(access_token),通过access_token可以进行微信开放平台授权关系接口调用,从而可实现获取微信用户基本开放信息和帮助用户实现基础开放功能等。 微信OAuth2.0授权登录目前支持authorization_code模式,适用于拥有server端的应用授权。该模式整体流程为:

1
2
3
1. 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
2. 通过code参数加上AppID和AppSecret等,通过API换取access_token;
3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。

实现

第一步

引导用户访问https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect此链接

参数说明

参数 是否必须 说明
appid 公众号的唯一标识
redirect_uri 授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理
response_type 返回类型,请填写code
scope 应用授权作用域,snsapi_base
(不弹出授权页面,直接跳转,只能获取用户openid),
snsapi_userinfo
(弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,
即使在未关注的情况下,只要用户授权,也能获取其信息 )
state 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节
#wechat_redirect 无论直接打开还是做页面302重定向时候,必须带此参数
forcePopup 强制此次授权需要用户弹窗确认;
默认为false;需要注意的是,若用户命中了特殊场景下的静默授权逻辑,则此参数不生效

首先肯定不能让用户自己输入链接访问,所以我们用此链接生成二维码,当用户点击微信登录时,后端返回二维码,前端展示,在用户扫码后自动跳转此连接

代码

首先需要导入生成二维码的依赖

1
2
3
4
5
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.5</version>
</dependency>

写出微信登录的接口

1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping(value = "/wxLogin")
@ResponseBody
public void wxLoginPage(HttpServletResponse response) throws IOException {
//redirect_url是回调的地址 注意要转成UrlEncode格式
String redirectUrl = URLEncoder.encode("http://7fa65291.r16.cpolar.top/wxCallback","UTF-8");
//构造二维码链接地址
String url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx4582e0245ae500d9&redirect_uri="
+ redirectUrl + "&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";
//生成二维码的,扫描后跳转上面的地址
response.setContentType("image/png");
QrCodeUtil.generate(url,300,300,"jpg",response.getOutputStream());
}

回调地址下面讲解

此时引导用户访问连接已完成

第二步

通过code换取网页授权access_token

链接为:

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

参数说明

参数 是否必须 说明
appid 公众号的唯一标识
secret 公众号的appsecret
code 填写第一步获取的code参数
grant_type 填写为authorization_code

首先这个code是会在用户授权后自动请求回调地址时获取的

写出回调接口

1
2
3
4
5
6
7
8
9
@RequestMapping("/wxCallback")
@ResponseBody
public String pcCallback(String code, String state, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
HttpSession session) throws IOException {
//通过code获取用户信息
user = WeChatUtil.getUserInfo(code);
//缓存用户信息,构造session
return JSON.toJSONString(user);
}

创建一个WeChatUtil类并在里面创建getUserInfo(String code)方法

首先我们实现用code换取网页授权access_token

appId和secret在测试号信息里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static WeChatUser getUserInfo(String code) throws IOException {
//构造http请求客户端
HttpClient httpClient = HttpClients.createDefault();
//用code交换token,code为扫码后微信服务器响应来的值
String tokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + appId + "&secret=" + secret + "&code=" + code + "&grant_type=authorization_code";
//发请求
HttpGet httpGet = new HttpGet(tokenUrl);
String responseResult = "";
//接受返回的数据,转成utf-8格式
HttpResponse response = httpClient.execute(httpGet);
if(response.getStatusLine().getStatusCode() == 200){
responseResult = EntityUtils.toString(response.getEntity(),"UTF-8");
}
log("获取accessToken返回结果:{}",responseResult);
//将结果封装到TokenInfo对象中
TokenInfo tokenInfo = JSON.parseObject(responseResult, TokenInfo.class);
}

此方法现在用code交换了access_tokenopenid这样我们就能进行下一步了

TokenInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Data
public class TokenInfo {
/**
* 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
*/
private String access_token;

/**
* access_token接口调用凭证超时时间,单位(秒)
*/
private String expires_in;

/**
* 用户刷新access_token
*/
private String refresh_token;

/**
* 用户唯一标识
*/
private String openid;

/**
* 用户授权的作用域,使用逗号(,)分割
*/
private String scope;
}

第三步

拉取用户信息(需scope为 snsapi_userinfo)

链接为

https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN

参数 描述
access_token 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
openid 用户的唯一标识
lang 返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语

继续在getUserInfo方法下写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//用accessToken获取扫码人的个人信息
String userInfoUrl = "https://api.weixin.qq.com/sns/userinfo?access_token=" + tokenInfo.getAccess_token()
+ "&openid=" + tokenInfo.getOpenid() + "&lang=zh_CN";
//发请求
HttpGet httpGet1 = new HttpGet(userInfoUrl);
//接收数据
HttpResponse response1 = httpClient.execute(httpGet1);
if(response1.getStatusLine().getStatusCode() == 200){
responseResult = EntityUtils.toString(response1.getEntity(),"UTF-8");
}
log("获取个人信息返回: {}",responseResult);
//将获取到的用户信息转化为WeChatUser对象
WeChatUser weChatUser = JSON.parseObject(responseResult, WeChatUser.class);
return weChatUser;

这些代码实现了用accessToken和openid获取扫码人的个人信息

并将返回的个人信息存放到weChatUser对象中

到最后返回个人信息

WeChatUser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Data
public class WeChatUser {
/**
* 用户的唯一标识
*/
private String openid;

/**
* 用户昵称
*/
private String nickname;

/**
* 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
*/
private Integer sex;

/**
* 城市
*/
private String city;

/**
* 省份
*/
private String province;

/**
* 国家
*/
private String country;

/**
* 用户头像
*/
private String headimgurl;
}

小程序登录

准备工作

首先注册一个小程序账号

注册后在开发管理-开发设置 查看AppID和获取AppSecret

小程序获取code是用 wx.login(Object object) 来获取的

所以当前端使用 wx.login(Object object)方法并将用户信息和code返回给后端,我们就可以通过code获取openid和session_key

登录流程

实现

创建小程序登录的接口

1
2
3
4
@PostMapping("/login")
public UserDTO appletLogin(@RequestBody UserDTO userDTO) throws IOException {

}

UserDTO 里为用户信息和code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Data
public class UserDTO {
/**
* 用户的唯一标识
*/
private String wxId;

/**
* 用户昵称
*/
private String wxUsername;

/**
* 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
*/
private Integer sex;

/**
* 城市
*/
private String city;

/**
* 省份
*/
private String province;

/**
* 国家
*/
private String country;

/**
* 用户头像
*/
private String wxHeadPic;

/**
* 用户手机号
*/
private String phone;

/**
* 登录凭证
*/
private String code;

/**
* token
*/
private String token;
}

该如何凭借code获取 openid 和 session_key

请求此链接:GET https://api.weixin.qq.com/sns/jscode2session

请求参数

属性 类型 必填 说明
appid string 小程序 appId
secret string 小程序 appSecret
js_code string 登录时获取的 code,可通过wx.login获取
grant_type string 授权类型,此处只需填写 authorization_code

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//构造http请求客户端
HttpClient httpClient = HttpClients.createDefault();
//用code交换token,code为扫码后微信服务器响应来的值
String tokenUrl = "https://api.weixin.qq.com/sns/jscode2session?appid=wx325282a4da418aca&secret=9222d4e72894f1210cba8de8d630d6ee&js_code=" + userDTO.getCode() + "&grant_type=authorization_code";
//发请求
HttpGet httpGet = new HttpGet(tokenUrl);
String responseResult = "";
//接受返回的数据,转成utf-8格式
HttpResponse response = httpClient.execute(httpGet);
if(response.getStatusLine().getStatusCode() == 200){
responseResult = EntityUtils.toString(response.getEntity(),"UTF-8");
}
log("获取accessToken返回结果:{}",responseResult);
//将结果封装到TokenInfo对象中
LoginDTO loginDTO = JSON.parseObject(responseResult, LoginDTO.class);

通过发送请求获取openid和session_key存在放LoginDTO中

LoginDTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Data
public class LoginDTO {
/**
* 用户唯一标识
*/
private String openid;

/**
* 会话秘钥
*/
private String session_key;

/**
* 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台账号下会返回
*/
private String unionid;

/**
* 错误码
*/
private Integer errcode;

/**
* 错误信息
*/
private String errmsg;
}

获取到openid和session_key即可将用户信息存放到我们的数据库中

User实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
public class User {
@TableId
private String id;

private String wxId;

private String headPic;

private String wxHeadPic;

private String phone;

private Integer sex;

private String username;

private String wxUsername;

private String password;
}

存放逻辑,用openid查找数据库是否有同样的id,如果有说明已经注册过,如果没有进行注册

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
   //如果没有失败code
if(null == loginDTO.getErrcode()){
//添加唯一标识
userDTO.setWxId(loginDTO.getOpenid());
}else {
return null;
}
log(userDTO.toString());
//查询数据库中是否有相同用户
User user = userMapper.selectOne(Wrappers.<User>lambdaQuery().eq(User::getWxId, userDTO.getWxId()));
Optional<User> optional = Optional.ofNullable(user);
//如果有,将数据库的数据复制到返回对象
if(optional.isPresent()){
BeanUtils.copyProperties(user,userDTO);
}else {
//数据库中不存在,注册用户信息
user = new User();
BeanUtils.copyProperties(userDTO,user);
user.setId(user.getWxId());
user.setUsername(user.getWxUsername());
user.setHeadPic(user.getWxHeadPic());
userMapper.insert(user);
BeanUtils.copyProperties(user,userDTO);
}
//测试token
userDTO.setToken(UUID.randomUUID().toString());
redisCache.setCacheObject("USER_" + userDTO.getToken(),loginDTO.getSession_key(),3600, TimeUnit.SECONDS);
return userDTO;

登录后的token不要直接存放session_key

微信支付

微信Native支付

产品介绍

简介

Native支付是指商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。

应用场景

Native支付适用于PC网站、实体店单品或订单、媒体广告支付等场景

用户扫描商户展示在各种场景的二维码进行支付,具体操作流程如下:

步骤一 商户根据微信支付的规则,为不同商品生成不同的二维码(如图3.1),展示在各种场景,用于用户扫描购买。

步骤二 用户使用微信“扫一扫”(如图3.2)扫描二维码后,获取商品支付信息,引导用户完成支付(如图3.3)。

图3.1支付二维码 图3.2打开微信扫一扫二维码 图3.3确认支付页面

步骤三 用户确认支付,输入支付密码(如图3.4)。

步骤四 支付完成后会提示用户支付成功(如图3.5),商户后台得到支付成功的通知,然后进行发货处理。

​ 图3.4用户确认支付,输入密码 图3.5支付成功提示

准备工作

在接入微信支付native服务前,你需要进行以下准备步骤:

  1. 选择接入模式:普通商户或普通服务商
  2. 申请参数:AppID、商户号
  3. 配置应用
  4. 下载并配置商户证书

选择接入模式

商户需要判断自己公司注册区域适用的接入模式和自身实际情况,申请成为普通商户或普通服务商:

  • 普通商户自行申请入驻微信支付,无需服务商协助
  • 普通服务商则自身无法作为一个普通商户直接发起交易,其发起交易必须传入相关特约商户商户号的参数信息。

参数申请

  1. 申请AppID
    • 由于微信支付的产品体系全部搭载于微信的社交体系之上,所以普通商户或服务商商户接入微信支付之前,都需要有一个微信社交载体,该载体对应的ID即为AppID。
    • 对于普通商户,该社交载体可以是公众号(什么是公众号),小程序(什么是小程序)或App。
    • 如申请社交载体为公众号,请前往 公众平台申请。
    • 如申请社交载体为小程序,请前往 小程序平台申请。
    • 如商户已拥有自己的App,且希望该App接入微信支付,请前往 开放平台申请。
    • 商户可根据实际的业务需求来选择申请不同的社交载体。
    • 各类社交载体一旦申请成功后,可以登录对应平台查看账号信息以获取对应的AppID。
  2. 申请mchid
    • 申请mchid和AppID的操作互不影响,可以并行操作,申请地址如下: 商户号申请平台。
    • 申请成功后,会向服务商填写的联系邮箱下发通知邮件,内容包含申请成功的mchid及其登录账号密码,请妥善保存。
      注意:一个mchid只能对应一个结算币种,若需要使用多个币种收款,需要申请对应数量的mchid。
  3. 绑定AppID及mchid
    • AppID和mchid全部申请完毕后,需要建立两者之间的绑定关系
    • 普通商户模式下,AppID与mchid之间的关系为多对多,即一个AppID下可以绑定多个mchid,而一个mchid也可以绑定多个AppID。
  4. 完成

配置应用

API v3密钥主要用于平台证书解密、回调信息解密,具体使用方式可参见接口规则文档中证书和回调报文解密章节。

  1. 登录微信商户平台,进入【账户中心 > API安全 】目录,设置APIv3密钥。

  2. 在弹出窗口中点击“已沟通”。

  3. 输入API密钥,内容为32位字符,包括数字及大小写字母。点击获取短信验证码。

  4. 输入短信验证码,点击“确认”即设置成功。

下载并配置商户证书

商户API证书具体使用说明可参见接口规则文档中私钥和证书章节

以下为具体下载步骤:

  1. 从2018年底开始,微信支付新入住机构及商户都将使用CA签发证书,在证书申请页面上点击”申请证书”
  2. 在弹出窗口中点击”确定”。
  3. 在弹出窗口内点击“下载证书工具”按钮下载证书工具。
  4. 安装证书工具并打开,选择证书需要存储的路径后点击“申请证书”。
  5. 在证书工具中,将复制的商户信息粘贴并点击“下一步”。
  6. 获取请求串


  7. 生成证书串
    步骤1:生成证书串
    步骤2:在【证书工具】-“复制请求串”环节,点击“下一步”按钮进入“粘贴证书串”环节;
    步骤3:在【证书工具】-“粘贴证书串”环节,点击“粘贴”按钮将证书串粘贴至文本
    步骤4:点击“下一步”按钮,进入【证书工具】-“生成证书”环节


  8. 在【证书工具】-“生成证书”环节,已完成申请证书流程,点击“查看证书文件夹”,查看已生成的证书文件

开发准备

搭建和配置开发环境

为了帮助开发者调用开放接口,微信提供了JAVAPHPGO三种语言版本的开发库,封装了签名生成、签名验证、敏感信息加/解密、媒体文件上传 等基础功能(更多语言版本的开发库将在近期陆续提供)。

测试步骤:

  1. 根据自身开发语言,选择对应的开发库并构建项目,具体配置请参考下面链接的详细说明:

  2. 创建加载商户私钥、加载平台证书、初始化httpClient的通用方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Before
    public void setup() throws IOException {
    // 加载商户私钥(privateKey:私钥字符串)
    PrivateKey merchantPrivateKey = PemUtil
    .loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));

    // 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
    AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
    new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),apiV3Key.getBytes("utf-8"));

    // 初始化httpClient
    httpClient = WechatPayHttpClientBuilder.create()
    .withMerchant(mchId, mchSerialNo, merchantPrivateKey)
    .withValidator(new WechatPay2Validator(verifier)).build();
    }

    @After
    public void after() throws IOException {
    httpClient.close();
    }
  3. 基于接口的示例代码,替换请求参数后可发起测试

快速接入

业务流程

重点步骤:

步骤2:Native下单

用户确认支付后,商户调用微信支付Native下单API生成预支付交易以获取支付二维码链接code_url;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public void CreateOrder() throws Exception{
//请求URL
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/native");

// 请求body参数
String reqdata = "{"
+ "\"amount\": {"
+ "\"total\": 100,"
+ "\"currency\": \"CNY\""
+ "},"
+ "\"mchid\": \"1900006891\","
+ "\"description\": \"Image形象店-深圳腾大-QQ公仔\","
+ "\"notify_url\": \"https://www.weixin.qq.com/wxpay/pay.php\","
+ "\"out_trade_no\": \"1217752501201407033233388881\","
+ "\"goods_tag\": \"WXG\","
+ "\"appid\": \"wxdace645e0bc2c424\"" + "}";
StringEntity entity = new StringEntity(reqdata,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");

//完成签名并执行请求
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
} else if (statusCode == 204) {
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
response.close();
httpClient.close();
}
}

在实际中我们可以创建Amount 和 NativePayParams 来接收下单的参数

示例:

Amount

1
2
3
4
5
6
@Builder
@Data
public class Amount {
private Integer total;
private String currency;
}

NativePayParams

1
2
3
4
5
6
7
8
9
10
@Builder
@Data
public class NativePayParams {
private String appid; //应用id
private String mchid; //商户id
private String description; //商品描述
private String out_trade_no; //订单号
private String notify_url; //支付成功回调通知地址
private Amount amount; //订单金额信息
}

请求参数替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Amount amount = Amount.builder()
.currency("CNY")
.total(1)
.build();
NativePayParams payParams = NativePayParams.builder()
.appid("appId")
.mchid("mchId")
.description("java从入门到精通")
.out_trade_no("1217752501201407033233388881")
.notify_url("https://21045581.r10.cpolar.top/native/notify")
.amount(amount)
.build();

// 请求body参数
String reqdata = JSON.toJSONString(payParams);

这样对应参数的添加就简洁明了

步骤4:商户根据返回的code_url生成二维码供用户扫描

可以使用此依赖将链接转为二维码

1
2
3
4
5
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.5</version>
</dependency>

可以查看此文档来学习如何生成二维码

步骤9-11:

支付结果

  • 方法一:支付结果通知。用户支付成功后,微信支付会将支付成功的结果以回调通知的形式同步给商户,商户的回调地址需要在调用Native下单API时传入notify_url参数。
  • 方法二:当因网络抖动或本身notify_url存在问题等原因,导致无法接收到回调通知时,商户也可主动调用查询订单API来获取订单状态。
支付结果通知

我们可以定义一个回调接口

微信支付通知的api在以下链接查看

支付通知

对应下单时的notify_url

1
2
3
4
@PostMapping("/notify")
public Map<String,String> payNotify(@RequestBody NotifyDto dto) throws GeneralSecurityException {

}

NotifyDto是支付成功后返回的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Data
public class NotifyDto {
/**
* 通知id
*/
private String id;
/**
* 通知的创建时间
*/
private String create_time;
/**
* 通知的类型
*/
private String event_type;
/**
* 通知的资源数据类型
*/
private String resource_type;
/**
* 通知资源数据
*/
private ResourceDto resource;
/**
* 回调摘要
*/
private String summary;
}

实现逻辑在Service中实现

NativePayService

1
2
3
public interface NativePayService {
public Map<String,String> payNotify(NotifyDto dto) throws GeneralSecurityException;
}

NativePayServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class NativePayServiceImpl implements NativePayService {
private String apiV3Key = "apiV3Key";
@Override
public Map<String, String> payNotify(NotifyDto dto) {
Map<String,String> res = new HashMap<>();
//解密微信传递过来的参数
try {
String json = new AesUtil(apiV3Key.getBytes()).decryptToString(dto.getResource().getAssociated_data().getBytes(),
dto.getResource().getNonce().getBytes(),
dto.getResource().getCiphertext());
String outTradeNo = JSON.parseObject(json, Map.class).get("out_trade_no").toString();

System.out.println("----------支付成功的订单号:" + outTradeNo);
} catch (GeneralSecurityException e) {
e.printStackTrace();
res.put("code","FAIL");
res.put("message","失败");
}
return res;
}
}

解析上面的代码

  1. 首先Map作为返回参数

  2. 解密微信传递过来的参数

    解密api

    步骤一:验证签名
    微信支付会对发送给商户的通知进行签名,并将签名值放在通知的HTTP头Wechatpay-Signature。商户应当验证签名,以确认请求来自微信,而不是其他的第三方。签名验证的算法请参考 《微信支付API v3签名验证》
    步骤二:参数解密
    为了保证安全性,微信支付在回调通知,对关键信息进行了AES-256-GCM加密。商户应当按照以下的流程进行解密关键信息,解密的流程:

    1. 用商户平台上设置的APIv3密钥【微信商户平台—>账户设置—>API安全—>设置APIv3密钥】,记为key;
    2. 获取resource.algorithm中描述的算法(目前为AEAD_AES_256_GCM),以及resource.nonce和resource.associated_data;
    3. 使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象。
  3. 如此得到支付结果的信息,之后可以自己进行相关业务操作

  4. 如果失败,返回

    1
    2
    3
    4
    {
    "code": "FAIL",
    "message": "失败"
    }
  5. 成功无需返回应答报文

主动调用查询订单API来获取订单状态。

微信官方不保证支付通知的调用是正确的,所以我们必须加上双保险,可以主动去查询支付的结果

主动查询api

参考下单的api,编写测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void queryOrder() throws Exception{
//请求URL
HttpGet httpGet = new HttpGet("/v3/pay/transactions/out-trade-no/1217752501201407033233388881?mchid=" + mchid);
httpPost.setHeader("Accept", "application/json");

//完成签名并执行请求
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
String url = EntityUtils.toString(response.getEntity())
} else if (statusCode == 204) {
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
response.close();
httpClient.close();
}
}

在实际中,轮询去查询订单状态

关闭订单

当商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口

注意

  • 关单没有时间限制,建议在订单生成后间隔几分钟(最短5分钟)再调用关单接口,避免出现订单状态同步不及时导致关单失败。
  • 已支付成功的订单不能关闭。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Test
public void CloseOrder() throws Exception {

//请求URL
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/sdkphp12345678920201028112429/close");
//请求body参数
String reqdata ="{\"mchid\": \""+mchId+"\"}";

StringEntity entity = new StringEntity(reqdata,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");

//完成签名并执行请求
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
} else if (statusCode == 204) {
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
response.close();
}
}

微信JSAPI支付

产品介绍

简介

JSAPI支付是指商户通过调用微信支付提供的JSAPI接口,在支付场景中调起微信支付模块完成收款。

应用场景

JSAPI支付适用于线下场所、公众号场景和PC网站场景。

商户已有H5商城网站,用户通过消息或扫描二维码在微信内打开网页时,可以调用微信支付完成下单购买的流程。具体操作流程如下:

步骤一 如图2.1,商户下发图文消息或者通过自定义菜单吸引用户点击进入商户网页。

步骤二 如图2.2,进入商户网页,用户选择购买,完成选购流程。

步骤三 如图2.3,调起微信支付控件,用户开始输入支付密码。

​ 图2.1 商户网页下单 图2.2请求微信支付 图2.3 用户确认支付,输入密码

步骤四 如图2.4,密码验证通过,支付成功。商户后台得到支付成功的通知。

步骤五 如图2.5,返回商户页面,显示购买成功。该页面由商户自定义。

步骤六 如图2.6,微信支付公众号下发支付凭证。

图2.4用户支付成功提示 图2.5 返回商户页面 图2.6 用户收到微信通知

准备工作

在正式接入微信支付JSAPI服务前,你需要进行以下准备步骤:

  1. 选择接入模式:普通商户或普通服务商
  2. 申请参数:AppID、商户号
  3. 配置应用

选择接入模式

商户需要判断自己公司注册区域适用的接入模式和自身实际情况,申请成为普通商户或普通服务商:

  • 普通商户自行申请入驻微信支付,无需服务商协助。
  • 普通服务商则自身无法作为一个普通商户直接发起交易,其发起交易必须传入相关特约商户商户号的参数信息。

具体接入模式介绍请参考接入模式文档,并按照参考文档完成相应模式的接入。

申请参数

请根据自身接入模式分别参考微信支付接入准备-普通商户微信支付接入准备-普通服务商中的参数申请 - 配置API key - 下载并配置商户证书三个步骤申请接入参数。

配置应用

设置支付授权目录
支付授权目录说明
  • 普通商户最后请求拉起微信支付收银台的页面地址我们称之为“支付授权目录”,例如:https://www.weixin.com/pay.php的支付授权目录为:https://www.weixin.com/
  • 普通商户实际的支付授权目录必须和在微信支付商户平台设置的一致,否则会报错“当前页面的URL未注册:”。
支付授权目录设置说明

登录【微信支付商户平台 —>产品中心—>开发配置】,设置后一般5分钟内生效

支付授权目录校验规则说明
  • 如果支付授权目录设置为顶级域名(例如:https://www.weixin.com/ ),那么只校验顶级域名,不校验后缀;
  • 如果支付授权目录设置为多级目录,就会进行全匹配,例如设置支付授权目录为 https://www.weixin.com/abc/123/,则实际请求页面目录不能为https://www.weixin.com/abc/,也不能为https://www.weixin.com/abc/123/pay/,必须为https://www.weixin.com/abc/123/
设置授权域名

开发JSAPI支付时,在JSAPI下单接口中要求必传用户OpenID,而获取OpenID则需要您在微信公众平台设置获取OpenID的域名,只有被设置过的域名才是一个有效的获取OpenID的域名,否则将获取失败。具体界面如图所示:

开通流程: 在入驻时选择线下场所,公众号场景,PC网站场景的商户系统默认开通此功能,其他商户如有需要,可以在入驻后前往商户平台-产品中心-JSAPI支付-申请开通。

开发准备

搭建和配置开发环境

为了帮助开发者调用开放接口,微信提供了JAVAPHPGO三种语言版本的开发库,封装了签名生成、签名验证、敏感信息加/解密、媒体文件上传 等基础功能(更多语言版本的开发库将在近期陆续提供)。

测试步骤:

  1. 根据自身开发语言,选择对应的开发库并构建项目,具体配置请参考下面链接的详细说明:

  2. 创建加载商户私钥、加载平台证书、初始化httpClient的通用方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Before
    public void setup() throws IOException {
    // 加载商户私钥(privateKey:私钥字符串)
    PrivateKey merchantPrivateKey = PemUtil
    .loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));

    // 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
    AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
    new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),apiV3Key.getBytes("utf-8"));

    // 初始化httpClient
    httpClient = WechatPayHttpClientBuilder.create()
    .withMerchant(mchId, mchSerialNo, merchantPrivateKey)
    .withValidator(new WechatPay2Validator(verifier)).build();
    }

    @After
    public void after() throws IOException {
    httpClient.close();
    }
  3. 基于接口的示例代码,替换请求参数后可发起测试

快速接入

业务流程

重点步骤:

步骤3:JSAPI下单

JSAPI下单api

用户通过商户下发的模板消息或扫描二维码在微信内进入商户网页,当用户选择相关商户购买时,商户系统先调用该接口在微信支付服务后台生成预支付交易单。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public void CreateOrder() throws Exception{
//请求URL
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");

// 请求body参数
String reqdata = "{"
+ "\"amount\": {"
+ "\"total\": 100,"
+ "\"currency\": \"CNY\""
+ "},"
+ "\"mchid\": \"1900006891\","
+ "\"description\": \"Image形象店-深圳腾大-QQ公仔\","
+ "\"notify_url\": \"https://www.weixin.qq.com/wxpay/pay.php\","
+ "\"payer\": {"
+ "\"openid\": \"o4GgauE1lgaPsLabrYvqhVg7O8yA\"" + "},"
+ "\"out_trade_no\": \"1217752501201407033233388881\","
+ "\"goods_tag\": \"WXG\","
+ "\"appid\": \"wxdace645e0bc2c424\"" + "}";
StringEntity entity = new StringEntity(reqdata,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");

//完成签名并执行请求
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
} else if (statusCode == 204) {
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
response.close();
httpClient.close();
}
}

在实际中我们可以创建Amount 和 Payer 和 JSAPIPayParams 来接收下单的参数

示例:

Amount

1
2
3
4
5
6
@Builder
@Data
public class Amount {
private Integer total;
private String currency;
}

Payer

1
2
3
4
5
@Builder
@Data
public class Payer{
private String openid;
}

NativePayParams

1
2
3
4
5
6
7
8
9
10
11
@Builder
@Data
public class NativePayParams {
private String appid; //应用id
private String mchid; //商户id
private String description; //商品描述
private String out_trade_no; //订单号
private String notify_url; //支付成功回调通知地址
private Amount amount; //订单金额信息
private Payer payer;//支付者信息
}

请求参数替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   Amount amount = Amount.builder()
.currency("CNY")
.total(1)
.build();
Payer payer = Payer.builder()
.openid("openid")
.build();
NativePayParams payParams = NativePayParams.builder()
.appid("appId")
.mchid("mchId")
.description("java从入门到精通")
.out_trade_no("1217752501201407033233388881")
.notify_url("https://21045581.r10.cpolar.top/jsapi/notify")
.amount(amount)
.payer(payer)
.build();

// 请求body参数
String reqdata = JSON.toJSONString(payParams);

这样对应参数的添加就简洁明了

步骤8:JSAPI调起支付

JSAPI调起支付api

通过JSAPI下单API成功获取预支付交易会话标识(prepay_id)后,需要通过JSAPI调起支付API来调起微信支付收银台。

由后端返回prepay_id给前端,前端请求JSAPI调起支付api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function onBridgeReady() {
WeixinJSBridge.invoke('getBrandWCPayRequest', {
"appId": "wx2421b1c4370ecxxx", //公众号ID,由商户传入
"timeStamp": "1395712654", //时间戳,自1970年以来的秒数
"nonceStr": "e61463f8efa94090b1f366cccfbbb444", //随机串
"package": "prepay_id=wx21201855730335ac86f8c43d1889123400",
"signType": "RSA", //微信签名方式:
"paySign": "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==" //微信签名
},
function(res) {
if (res.err_msg == "get_brand_wcpay_request:ok") {
// 使用以上方式判断前端返回,微信团队郑重提示:
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
}
});
}
if (typeof WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
} else {
onBridgeReady();
}

重要入参说明:

  • package: JSAPI下单接口返回的prepay_id参数值,提交格式如:prepay_id=***。
  • signType: 该接口V3版本仅支持RSA。
  • paySign: 签名。

paySign生成规则、响应详情及错误码请参见 JSAPI调起支付接口文档。

步骤15:接收微信支付结果

我们可以定义一个回调接口

微信支付通知的api在以下链接查看

支付通知

对应下单时的notify_url

1
2
3
4
@PostMapping("/notify")
public Map<String,String> payNotify(@RequestBody NotifyDto dto) throws GeneralSecurityException {

}

NotifyDto是支付成功后返回的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Data
public class NotifyDto {
/**
* 通知id
*/
private String id;
/**
* 通知的创建时间
*/
private String create_time;
/**
* 通知的类型
*/
private String event_type;
/**
* 通知的资源数据类型
*/
private String resource_type;
/**
* 通知资源数据
*/
private ResourceDto resource;
/**
* 回调摘要
*/
private String summary;
}

实现逻辑在Service中实现

JSAPIPayService

1
2
3
public interface JSAPIPayService {
public Map<String,String> payNotify(NotifyDto dto) throws GeneralSecurityException;
}

JSAPIPayServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class JSAPIPayServiceImpl implements NativePayService {
private String apiV3Key = "apiV3Key";
@Override
public Map<String, String> payNotify(NotifyDto dto) {
Map<String,String> res = new HashMap<>();
//解密微信传递过来的参数
try {
String json = new AesUtil(apiV3Key.getBytes()).decryptToString(dto.getResource().getAssociated_data().getBytes(),
dto.getResource().getNonce().getBytes(),
dto.getResource().getCiphertext());
String outTradeNo = JSON.parseObject(json, Map.class).get("out_trade_no").toString();

System.out.println("----------支付成功的订单号:" + outTradeNo);
} catch (GeneralSecurityException e) {
e.printStackTrace();
res.put("code","FAIL");
res.put("message","失败");
}
return res;
}
}

解析上面的代码

  1. 首先Map作为返回参数

  2. 解密微信传递过来的参数

    解密api

    步骤一:验证签名
    微信支付会对发送给商户的通知进行签名,并将签名值放在通知的HTTP头Wechatpay-Signature。商户应当验证签名,以确认请求来自微信,而不是其他的第三方。签名验证的算法请参考 《微信支付API v3签名验证》
    步骤二:参数解密
    为了保证安全性,微信支付在回调通知,对关键信息进行了AES-256-GCM加密。商户应当按照以下的流程进行解密关键信息,解密的流程:

    1. 用商户平台上设置的APIv3密钥【微信商户平台—>账户设置—>API安全—>设置APIv3密钥】,记为key;
    2. 获取resource.algorithm中描述的算法(目前为AEAD_AES_256_GCM),以及resource.nonce和resource.associated_data;
    3. 使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象。
  3. 如此得到支付结果的信息,之后可以自己进行相关业务操作

  4. 如果失败,返回

    1
    2
    3
    4
    {
    "code": "FAIL",
    "message": "失败"
    }
  5. 成功无需返回应答报文

步骤20:主动查询订单

查询订单api

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void QueryOrder() throws Exception {

//请求URL
URIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/pay/transactions/id/4200000745202011093730578574");
uriBuilder.setParameter("mchid", mchId);

//完成签名并执行请求
HttpGet httpGet = new HttpGet(uriBuilder.build());
httpGet.addHeader("Accept", "application/json");
CloseableHttpResponse response = httpClient.execute(httpGet);

try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
} else if (statusCode == 204) {
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
response.close();
}
}
关闭订单

当商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。

注意

  • 关单没有时间限制,建议在订单生成后间隔几分钟(最短5分钟)再调用关单接口,避免出现订单状态同步不及时导致关单失败。
  • 已支付成功的订单不能关闭。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void CloseOrder() throws Exception {

//请求URL
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/sdkphp12345678920201028112429/close");
//请求body参数
String reqdata ="{\"mchid\": \""+mchId+"\"}";

StringEntity entity = new StringEntity(reqdata,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");

//完成签名并执行请求
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
} else if (statusCode == 204) {
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
response.close();
}
}