从零玩转系列之微信支付安全

Posted 杨不易呀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零玩转系列之微信支付安全相关的知识,希望对你有一定的参考价值。

一、前言

halo各位大佬很久没更新了最近在搞微信支付,因商户号审核了我半个月和小程序认证也找了资料并且将商户号和小程序进行关联,至此微信支付Native支付完成.此篇文章过长我将分几个阶段的文章发布(项目源码都有,小程序和PC端)

在此之前已经更新了 微信支付开篇

二、微信支付安全(证书/秘钥/签名)

1. 信息安全的基础 - 机密性

明文:加密前的消息叫“明文”(plain text)

密文:加密后的文本叫“密文”(cipher text)

密钥:只有掌握特殊“钥匙”的人,才能对加密的文本进行解密,这里的“钥匙”就叫做“密钥”(key)

“密钥”就是一个字符串,度量单位是“位”(bit),比如,密钥长度是 128,就是 16 字节的二进制串

加密:实现机密性最常用的手段是“加密”(encrypt)

按照密钥的使用方式,加密可以分为两大类:对称加密和非对称加密。

解密:使用密钥还原明文的过程叫“解密”(decrypt)

加密算法:加密解密的操作过程就是“加密算法”

所有的加密算法都是公开的,而算法使用的“密钥”则必须保密

2. 对称加密和非对称加密

对称加密
  • 特点:只使用一个密钥,密钥必须保密,常用的有 AES算法优点:运算速度快
  • 缺点:秘钥需要信息交换的双方共享,一旦被窃取,消息会被破解,无法做到安全的密钥交 换
  • 优点:运算速度快
  • 缺点:秘钥需要信息交换的双方共享,一旦被窃取,消息会被破解,无法做到安全的密钥交 换
非对称加密
  • 特点:使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,常用的有 RSA、SM2
  • 优点:黑客获取公钥无法破解密文,解决了密钥交换的问题
  • 缺点:运算速度非常慢
混合加密
  • 实际场景中把对称加密和非对称加密结合起来使用

3.身份认证

  • 公钥加密, 私钥解密的作用是加密信息

  • 私钥加密,公钥解密的作用是身份认证

4.摘要算法(Digest Algorithm)

摘要算法就是我们常说的散列函数、哈希函数(Hash Function),它能够把任意长度的数据“压缩”成固定长度、而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。

作用

保证信息的完整性

特性

  • 不可逆:只有算法,没有秘钥,只能加密,不能解密
  • 难题友好性:想要破解,只能暴力枚举
  • 发散性:只要对原文进行一点点改动,摘要就会发生剧烈变化
  • 抗碰撞性:原文不同,计算后的摘要也要不同

常见摘要算法

MD5、SHA1、SHA2(SHA224、SHA256、SHA384)

5.数字签名

数字签名是使用私钥对摘要加密生成签名,需要由公钥将签名解密后进行验证,实现身份认证和不可否 认

签名和验证签名的流程

6.数字证书

数字证书解决“公钥的信任”问题,可以防止黑客伪造公钥。

不能直接分发公钥,公钥的分发必须使用数字证书,数字证书由CA颁发 HTTPS 协议中的数字证书

7.微信APIv3证书

商户证书:

商户API证书是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。

商户证书在商户后台申请:点我前往申请获取商户证书

8. ⚠️ 平台证书(微信支付平台)

微信支付平台证书是指由微信支付 负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行验签。

平台证书的获取:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml

后续Native模式通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新我们就先不需要下载这个微信平台证书,后续JSAPI需要下载(使用了国内顶级开源项目来操作它没有默认处理所以你懂的....)

证书下载参考: https://github.com/wechatpay-apiv3/CertificateDownloader

9. ⚠️ API密钥和APIv3密钥

都是对称加密需要使用的加密和解密密钥,一定要保管好,不能泄露。

API密钥对应V2版本的API
APIv3密钥对应V3版本的API

从零玩转第三方登录之WeChat公众号扫码关注登陆 -wechatgzh

title: 从零玩转第三方登录之WeChat公众号扫码关注登陆 
date: 2022-09-27 22:46:53.362
updated: 2023-03-30 13:28:41.359
url: https://www.yby6.com/archives/wechatgzh
categories: 
- 从零玩转系列
tags: 
- 第三方登录
- 从零玩转系列

前言

由于看见了面试鸭的登陆方式,我也想来整一个.注意: 只能微信认证的公众号才能有二维码扫码的权限,那么我们将使用 微信的测试账户来玩转扫码(沙箱)

1. 大致流程思路:

一、用户打开网页进行登陆/注册 扫码(微信的)
二、用户扫码成功后 微信会根据我们配置的回调地址访问我们的回调并且传递某些参数
三、用户扫码成功并且进行了关注我们的公众号 微信也会访问回调 传递参数
四、++域名使用内网穿透(我这里使用花生壳)++

思路地址: 接收事件推送
在微信用户和公众号产生交互的过程中,用户的某些操作会使得微信服务器通过事件推送的形式通知到开发者在开发者中心处设置的服务器地址,从而开发者可以获取到该信息。其中,某些事件推送在发生后,是允许开发者回复用户的,某些则不允许,详细内容如下:
1、关注/取消关注事件
2、 扫描带参数二维码事件
3、上报地理位置事件
4、自定义菜单事件
5、点击菜单拉取消息时的事件推送
6、点击菜单跳转链接时的事件推送

根据上述六点我们PC端只需要 1、2点即可只是来扫码公众号并且关注后登录

2. 进入测试号页面

微信测试号地址

测试号接口配置

接口信息配置: 将会get方法来进行验签你服务器的请求 和 post来回调推送信息到服务器
参考: 接口信息配置
JS接口安全配置:我们在日常当中经常可以看见js接口安全域名。那么,js接口安全域名是什么?js接口安全域名主要用于微信公众号,如果大家要进行微信的开发,创建公众号是需要填写js接口安全域名的。当我们运用程序的时候,网络是会自动验证安全域名的,它可以解决服务器终端的语言问题,能够让访问正常的运行,只有使用好js接口安全域名,网上的用户才能够访问到网页。
参考:JS接口安全配置

3. 介绍

获取 AccessToken

用于请求微信API 需要用到的认证信息
参考: 获取AccessToken

临时二维码

  1. 用户扫描带场景值二维码时,可能推送以下两种事件:
    如果用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者。
  2. 如果用户已经关注公众号,在用户扫描后会自动进入会话,微信也会将带场景值扫描事件推送给开发者。
  3. 获取带参数的二维码的过程包括两步,首先创建二维码ticket,然后凭借 ticket 到指定 URL 换取二维码。
    正确的 Json 返回结果:
    "ticket":"gQH47joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL2taZ2Z3TVRtNzJXV1Brb3ZhYmJJAAIEZ23sUwMEmm
    3sUw==","expire_seconds":60,"url":"http://weixin.qq.com/q/kZgfwMTm72WWPkovabbI"
  4. 参考: 临时二维码

4. 代码操作

编写接口配置以便能修改接口

    /***
     * 微信服务器触发get请求用于检测签名-
     * 如果需要绝对的安全就按照微信来进行验签
     */
    @GetMapping("/weChatScanCodeCallback")
    @ResponseBody
    public String weChatScan(HttpServletRequest request) 
        log.info("验签章:", request.getParameterMap());
        return request.getParameter("echostr");
    

解析微信返回参数
使用DOM4J将微信返回XML格式转换一下


/**
 * @Author yang shuai
 * @Date 2022/9/3
 */
public class XmlUtil 

    /**
     * 读取xml标签内容存放map当中
     */
    public static Map<String,Object> parseXML(InputStream in)
        Map<String,Object> map=new HashMap<>();
        try 
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(in);
            Element root = document.getRootElement();
            Iterator iterator = root.elementIterator();
            while (iterator.hasNext())
                Element element = (Element) iterator.next();
                map.put(element.getName(),element.getStringValue());
            
         catch (DocumentException e) 
            e.printStackTrace();
        
        return map;
    


接收微信回调

    /**
     * 接收微信推送事件
     */
    @PostMapping("/weChatScanCodeCallback")
    @ResponseBody
    public String weChatCallback(HttpServletRequest request) 
        try 
            InputStream inputStream = request.getInputStream();
            Map<String, Object> map = XmlUtil.parseXML(inputStream);
            log.info("接收参数:", map);
         catch (IOException e) 
            e.printStackTrace();
        
        return "success";

Last

注入restTemplate请求

/**
 * @Author yang shuai
 * @Date 2022/9/3
 * 注入restTemplate用于http请求
 */
@Configuration
public class RestTemplateConfig 

    @Resource
    private RestTemplateBuilder templateBuilder;

    @Bean
    public RestTemplate restTemplate()

        return templateBuilder.build();
    


生成微信二维码

/**
 * @Author yang shuai
 * @Date 2022/9/3
 */
public interface WeChatService 
    /**
     * 获取token
     *
     * @return
     */
    String getAccessToken();

    /**
     * 获取生成二维码参数
     *
     * @return
     */
    Map<String, Object> getQrCode();

实现


/**
 * @Author yang shuai
 * @Date 2022/9/3
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatServiceImpl implements WeChatService 

    @Value("$weChat.gzh.appid:\'\'")
    private String appid;

    @Value("$weChat.gzh.secret:\'\'")
    private String secret;

    private final RestTemplate restTemplate;

    private final RedisCache redisCacheManager;

    /**
     * 获取token用于操作微信接口
     */
    @Override
    public String getAccessToken() 
        String key = "wx_access_token";
        if (redisCacheManager.hashKey(key)) 
            return redisCacheManager.getCacheObject(key);
        
        // 获取微信扫码 token
        String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appid, secret);
        ResponseEntity<String> result = restTemplate.getForEntity(url, String.class);
        if (result.getStatusCode() == HttpStatus.OK) 
            JSONObject jsonObject = JSON.parseObject(result.getBody());
            String access_token = jsonObject.getString("access_token");
            Long expires_in = jsonObject.getLong("expires_in");
            redisCacheManager.setCacheObject(key, access_token, expires_in, TimeUnit.SECONDS);
            return access_token;
        
        return null;
    

    /**
     * 获取微信公众号二维码
     */
    @Override
    public Map<String, Object> getQrCode() 
        // 获取临时二维码
        String url = String.format("https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s", getAccessToken());
        ResponseEntity<String> result = restTemplate.postForEntity(url, "\\"expire_seconds\\": 604800, \\"action_name\\": \\"QR_STR_SCENE\\", \\"action_info\\": \\"scene\\": \\"scene_str\\": \\"test\\"", String.class);

        log.info("二维码:", result.getBody());

        JSONObject jsonObject = JSON.parseObject(result.getBody());
        Map<String, Object> map = new HashMap<>();
        map.put("ticket", jsonObject.getString("ticket"));
        map.put("url", jsonObject.getString("url"));

        return map;
    


5. 改造Controller

新增获取二维码

    /**
     * 获取二维码参数
     *
     * @return
     */
    @GetMapping("/getQrCode")
    @ResponseBody
    public Object getQrCode() 
        return weChatService.getQrCode();
    

6.编写前段Demo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆</title>
</head>
<body>

<div >
    <div id="qrcode"></div>
    <div id="msg" >
        扫码成功!
    </div>
</div>
<script type=\'text/javascript\' src=\'http://cdn.staticfile.org/jquery/2.1.1/jquery.min.js\'></script>
<script src="https://cdn.bootcss.com/jquery.qrcode/1.0/jquery.qrcode.min.js"></script>
<script>
    $(function () 
        let count = 0;
        //获取二维码参数
            $.get(\'https://34i33045l8.oicp.vip/weChat/getQrCode\', function (res) 
            //生成二维码
            $(\'#qrcode\').qrcode(res.url);
        )
    )

</script>
</body>
</html>

7.启动后端查看效果

1、使用idea打开html挂载一个node

2、打开前面要求设置的内网穿透用于接收微信的回调

3、进行扫码-查看后台打印参数数据

4、扫码后查看控制台

推送 XML 数据包示例:

  1. 用户未关注时,进行关注后的事件推送
<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[FromUser]]></FromUserName>
  <CreateTime>123456789</CreateTime>
  <MsgType><![CDATA[event]]></MsgType>
  <Event><![CDATA[subscribe]]></Event>
  <EventKey><![CDATA[qrscene_123123]]></EventKey>
  <Ticket><![CDATA[TICKET]]></Ticket>
</xml>

示例:

在这里大家应该大致的知道下面的该如何实现了!

  1. 微信回调会一直存在 Ticket 字段 用于表示每次二维码的唯一标识
    我们将它进行存储redis当中并且可以看到 Event 我们利用它来区分当前是否为扫码还是关注的推送
  2. 则前段进行段轮训来请求校验当前为什么状态?

参数说明:

参数 描述
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,event
Event 事件类型,subscribe(扫码关注) or SCAN (扫码)
EventKey 事件 KEY 值,qrscene_为前缀,后面为二维码的参数值
Ticket 二维码的ticket,可用来换取二维码图片

8. 改造Controller

新增短轮询检查扫码状态


    /**
     * 用于检测扫码和关注状态
     *
     * @return
     */
    @PostMapping("/checkLogin")
    @ResponseBody
    public Object checkLogin(String ticket) 
        // 存在该信息并且为关注了公众号
        if (redisCache.hashKey(ticket)) 
            if (!redisCache.getCacheObject(ticket).equals("subscribe")) 
                return AjaxResult.error(201, "扫码成功");
            
            //扫码通过则删除
            redisCache.deleteObject(ticket);
            return AjaxResult.success();

        
        return AjaxResult.error("无动作");
    

修改微信回调完善业务


    /**
     * 接收微信推送事件
     *
     * @param request
     * @return
     */
    @PostMapping("/weChatScanCodeCallback")
    @ResponseBody
    public String weChatCallback(HttpServletRequest request) 
        try 
            InputStream inputStream = request.getInputStream();
            Map<String, Object> map = XmlUtil.parseXML(inputStream);
            log.info("接收参数:", map);
            String userOpenId = (String) map.get("FromUserName");
            String event = (String) map.get("Event");
           //  自己生成的二维码不管是关注还是扫码都能取到ticket凭证,这里我使用Ticket作为每次二维码的唯一标识
            String ticket = (String) map.get("Ticket");
            if ("subscribe".equals(event)) 
                //  根据openid判断用户是否存在,不存在则获取新增用户
                // 或者根据前段传递手机号或者用户名称来进行openId绑定 看你自己的业务.
                
                
                redisCache.setCacheObject(ticket, "subscribe", (long) (10 * 60), TimeUnit.SECONDS);
                log.info("用户关注:", userOpenId);
             else if ("SCAN".equals(event)) 
                redisCache.setCacheObject(ticket, "scan", (long) (10 * 60), TimeUnit.SECONDS);
                log.info("用户扫码:", userOpenId);
            
         catch (IOException e) 
              log.error("回调异常:",e);
        
        return "success";
    

新增前段短轮询

替换你自己的内网穿透

$(function () 
        let count = 0;
        //获取二维码参数
            $.get(\'https://34i33045l8.oicp.vip/weChat/getQrCode\', function (res) 
            //生成二维码
            $(\'#qrcode\').qrcode(res.url);
            // 轮训获取用户扫码登陆状态
            let task = setInterval(function () 
                $.post(\'https://34i33045l8.oicp.vip/weChat/checkLogin\', ticket: res.ticket, function (code,msg) 
                    console.log(code);
                    if (code === 200)  // 扫码并且关注成功
                        clearInterval(task)
                        location.href = \'http://yby6.com\'
                     else if (code === 201)  // 扫码成功
                        $("#msg").text(msg);
                        document.querySelector("#msg").style.display = "block"
                     else 

                    
                    count ++;
                )
            , 2000)
        )
    )

最后操作流程

注: 前端记得整扫码超时!

以上是关于从零玩转系列之微信支付安全的主要内容,如果未能解决你的问题,请参考以下文章

从零玩转JavaWeb系列7web服务器-----get与post的区别

从零玩转JavaWeb系列7web服务器-----表单的提交

从零玩转Docker之docker-compose-azdocker-compose

从零玩转JavaWeb系列7web服务器-----用户登录界面二维码的制作

带你从零玩转云服务器

从零玩转SpringSecurity+JWT整合前后端分离-从零玩转springsecurityjwt整合前后端分离