微信小程序的加密解密以及小程序的支付

写项目的时候注意的问题

如果写的问题,大吉大利
有问题,最好是报错,因为有了报错,你就知道错在哪里?开心
最怕的是有问题,但是就是不报错。找不到问题的所在。

小程序的支付流程图

微信小程序的加密解密以及小程序的支付

题目

背景支付平台,这个个平台相当于支付宝的平台。
1 用户下单,就记入订单,如果支付成功就改变订单状态。(某个商户底下的订单)
2 支付宝商户。商户可以对他已支付的订单,做体现。
3 体现出问题了。
当用户提现的时候,就出问题,a商户1000万,但是体现980万,10000万,999.9万
体现的代码逻辑
- 查询已支付支付的订单,把所有已经支付的订单,的钱数做累加,状态是没有提现的 1 并且返回订单id
100    

- 给商户的体现钱数+钱数做累加,等于可体现钱数 2

- 将所有该用户底下的订单定做,update操作,把已支付的全部改成以体现 3 只改变,返回订单id

订单表
钱数   支付状态   是否体现  商户id
100    1          1       1
200    1          0      1
100    1          0      1

用户表
可体现钱数
100

会不会有订单进来

回顾

1 小程序的登入
    1 先调用wx.login来获取code.
    2 将code通过wx.request传递到后端。后端通过code,appid,Appsecret来请求code2session接口来获得
    openid,session_key,openid是单个应用用户的唯一标识
    3 后端得到open_id与session_key,进行存储,并自定义登入状态,key->val,val是openid与session_key,key相当于token,在把token传递给小程序。
    4 小程序收到后端返回的token,保存,下次请求后端的时候如果需要登入状态,把这个token带上
    
2 授权
    1 微信的部分需要用户同意后才能使用,哪些接口需要用户同意呢?我们scope列表中的对应关系。
    2 我们可以用wx.getSetting来判断接口有没有被用户授权,查看哪个接口,就需要判断wx.getSetting的返回值,authsetting中的socop值判断。
    3 如果我们发现这个接口的authsetting中的scope为false,就标识该用户没有对该接口授权,这个时候我们就可以用wx.authorize吊起对应授权弹框,如果要对哪个接口授权,就需要给wx.authorize,传递对应的scope值,如果
authsetting中的scope为true,表示该用户已经对该接口授权,我们就可以直接使用对应的接口
    4 wx.authorize不能将wx.getUserInfor这接口直接吊起授权弹框。我们只能通过<button>按钮的行式手动吊起弹框

后端,如何解析wx.getUserInfor中的用户信息。

1 我们用encryptedData和iv,进行解密,必须要用到session_key,所以用必须是登入状态。
2 但是session_key是有有效期。而且session_key的有效期,不是一个固定值,他是通过用户行为来决定,session_key的有效期时间。
3 但是我们可以通过wx.checkSession来判断有没有过期。
4 保证session_key没有过期的情况下。我们将iv,encryptedData,token(登入凭证)发送到后端.
5 后端使用官方提供的sdk,进行解密。
6 解密成功以后保存到数据,数据库的字符集一定要是utf8mb4,才能保存表情包

如官方的sdk没有Crypto包用下面的方法解决:

pip install pycryptodome 

小程序的app.js

//app.js
App({

  /*
    当小程序初始话完成,会触发onlaunch(全局只触发一次)
  */
  onLaunch: function () {
    // let that = this
    // 登录
    this.my_login()

  },
  my_login:function(){
    let that = this
    wx.login({
      success: res => {
        // 发送 res.code 到后台换取 openId, sessionKey, unionId
        console.log(res.code)
        wx.request({
          url: that.globalData.baseurl + "login/",
          data: { "code": res.code },
          method: "POST",
          success(e) {
            wx.setStorageSync('token', e.data.data.token)
          }
        })
      }
    })
  },
  /**
   * 当小程序启动,或者是重后台进入到前台的时候,会执行onshow,
   * 那我们可以通过这个option中的scene值来判断不同进入场景
   */
  // onShow:function(option){
  //   console.log("小程序onshow,:onShow",option)
  // },
  // /*小程序重前台进入到后台的时候,会触发:onHide*/
  // onHide:function(){
  //   console.log("小程序重前台进入到后台的时候,会触发:onHide")

  // },
  /**可以在全局使用 */
  globalData: {
    userInfo: null,
    baseurl: "http://127.0.0.1:8000/"

  }
})

小程序页面的js

// pages/test3/test3.js
const app = getApp()
Page({

  /**
   * 页面的初始数据
   */
  data: {

  },
//加密解密
  user1:function(e){
    // console.log("e",e.detail)

    wx.getSetting({
      success(res) {
        if (res.authSetting['scope.userInfo']) {

          wx.checkSession({
            success() {
              //session_key 未过期,并且在本生命周期一直有效
              wx.getUserInfo({
                success: (res) => {
                  console.log("res", res)//这个res里面就是用户的信息
                  //将数据发送到后端
                  wx.request({
                    //这里是发送iv和encryptedate
                    url: app.globalData.baseurl + "getinfo/",
                    data:{
                      iv:res.iv,
                      encryptedData:res.encryptedData,
                      token: wx.getStorageSync('token')
                    },
                    method:"POST",
                    success: (e) => {
                      console.log("后台返回的数据",e)
                    }
                  })
                },
              })

            },
            fail() {
              // session_key 已经失效,需要重新执行登录流程
              app.my_login()
              wx.request({
                //这里是发送iv和encryptedate
                url: app.globalData.baseurl + "getinfo/",
                data: {
                  iv: res.iv,
                  encryptedData: res.encryptedData,
                  token: wx.getStorageSync('token')
                },
                method: "POST",
                success: (e) => {
                  console.log("后台返回的数据", e)
                }
              })
            }
          })

          
        } 
      }
    })

    

  },
//支付
  pay:function(){
    wx.request({
      url: app.globalData.baseurl + "pay/",
      data:{"money":1, token:wx.getStorageSync('token')},
      method:"POST",
      success(e){
          console.log("支付数据",e)
        wx.requestPayment(
          {
            'timeStamp': e.data.data.timeStamp,
            'nonceStr': e.data.data.nonceStr,
            'package': e.data.data.package,
            'signType': e.data.data.signType,
            'paySign': e.data.data.paySign,
            'success': function (res) { 
              console.log("成功", res)
            },
            'fail': function (res) { 
              console.log("失败", res)
            },
          })
      }
    })
  }

})

主配置settings.py

# django-redis缓存
STATIC_URL = '/static/'

CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379',
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
             "PASSWORD": "Admin123",
        },
    },
}

# 数据库
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'python13',
        'USER':'root',
        'PASSWORD':'123456',
        'HOST':'127.0.0.1',
        'PORT': 3306,
        'OPTIONS': {'charset': 'utf8mb4'}, # 当用支付的时候用utf8mb4
    }
}

有关小程序的settings

AppId="xxx"  # 写你自己的

AppSecret="xxx"  # 写你自己的

code2Session="https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code"
pay_mchid ='xxx' # 这个需要三证合一才可以拿到什么营业执照许可证啥的,一般只有公司才有,
pay_apikey = 'xxx' # 这个需要三证合一才可以拿到什么营业执照许可证啥的,一般只有公司才有,

python支付的demo

from WXBizDataCrypt import WXBizDataCrypt

if __name__ == '__main__':
    main()

用于加密解密的校验

import base64
import json
from Crypto.Cipher import AES
from app01.wx import settings

class WXBizDataCrypt:
    def __init__(self, appId, sessionKey):
        self.appId = appId
        self.sessionKey = sessionKey

    def decrypt(self, encryptedData, iv):
        # base64 decode
        sessionKey = base64.b64decode(self.sessionKey)
        encryptedData = base64.b64decode(encryptedData)
        iv = base64.b64decode(iv)

        cipher = AES.new(sessionKey, AES.MODE_CBC, iv)

        decrypted = json.loads(self._unpad(cipher.decrypt(encryptedData)))

        if decrypted['watermark']['appid'] != self.appId:
            raise Exception('Invalid Buffer')

        return decrypted

    def _unpad(self, s):
        return s[:-ord(s[len(s)-1:])]

    @classmethod
    def get_info(cls, sessionKey, encryptedData, iv):

        return cls(settings.AppId, sessionKey).decrypt(encryptedData, iv)

pay

from rest_framework.views import  APIView
from rest_framework.response import  Response
from django.core.cache import cache
from app01.wx import settings
import hashlib,requests,time

class Pay(APIView):
    def post(self,request):
        param = request.data
        if param.get("token") and param.get("money"):
            openid_session_key = cache.get(param.get("token"))
            if openid_session_key:
                # 获取客户端ip,如果是负载均衡,就用HTTP_X_FORWARDED_FOR,如果不是就用下面的
                # nginx 转发:--》访问是nginx,->nginx -> uwsgi
                if request.META.get('HTTP_X_FORWARDED_FOR'):
                    #有负载均衡就用这个
                    self.ip = request.META['HTTP_X_FORWARDED_FOR']
                else:
                    #没有负载均衡就用这个
                    self.ip = request.META['REMOTE_ADDR']
                self.openid =openid_session_key.split("&")[1]
                self.money =param.get("money")
                data = self.get_pay_data()
                return Response({"code":0,"msg":"ok","data":data})

            else:
                return Response({"code": 2, "msg": "token无效"})
        else:
            return Response({"code":1,"msg":"缺少参数"})

    def get_nonce_str(self):
        import random
        data = "123456789abcdefghijklmn"
        nonce_str = "".join(random.sample(data,10))
        #random.sample(从哪里取,取多小个),变成列表
        return nonce_str

    def get_order_id(self):
        import time
        import random
        data = "123456789abcdefghijklmn"
        order_no = str(time.strftime("%Y%m%d%H%M%S"))+"".join(random.sample(data, 5))
        return  order_no

    def get_sign(self):
        data_dict ={
        "appid" :  self.appid,
        "mch_id":self.mch_id,
        "nonce_str" : self.nonce_str,
        "body" :  self.body,
        "out_trade_no" : self.out_trade_no,
        "total_fee" : self.total_fee,
        "spbill_create_ip" : self.ip,
        "notify_url" : self.notify_url,
        "trade_type" :  self.trade_type,
        "openid" : self.openid,
        }
        sign_str = "&".join([f"{k}={data_dict[k]}"  for k in sorted(data_dict)])
        sign_str = f"{sign_str}&key={settings.pay_apikey}"
        print("sign_str", sign_str)
        md5 = hashlib.md5()
        md5.update(sign_str.encode("utf-8"))
        sign = md5.hexdigest()
        return sign.upper()


    def xml_to_dict(self,xml_data):
        import xml.etree.ElementTree as ET

        xml_dict ={}

        root = ET.fromstring(xml_data)

        for child in root:
            xml_dict[child.tag]= child.text
        return xml_dict



    def get_two_sign(self,data):
        data_dict = {
            "appId":settings.AppId,
            "timeStamp":str(int(time.time())),
            "nonceStr":data['nonce_str'],
            "package":f"prepay_id={data['prepay_id']}",
            "signType":"MD5"

        }
        sign_str = "&".join([f"{k}={data_dict[k]}" for k in sorted(data_dict)])
        sign_str = f"{sign_str}&key={settings.pay_apikey}"
        md5 = hashlib.md5()
        md5.update(sign_str.encode("utf-8"))
        sign = md5.hexdigest()
        return sign.upper() , data_dict['timeStamp']



    def get_pay_data(self):
        self.appid = settings.AppId
        self.mch_id = settings.pay_mchid
        self.nonce_str = self.get_nonce_str()
        self.body = "老男孩学费"
        self.out_trade_no = self.get_order_id()
        self.total_fee =self.money
        self.spbill_create_ip =self.ip
        self.notify_url = "htttp://www.test.com"
        self.trade_type ="JSAPI"
        self.openid = self.openid
        self.sign = self.get_sign()

        body_data = f'''
        <xml>
            <appid>{self.appid}</appid>
            <mch_id>{self.mch_id}</mch_id>
            <nonce_str>{self.nonce_str}</nonce_str>
            <body>{self.body}</body>
            <out_trade_no>{self.out_trade_no}</out_trade_no>
            <total_fee>{self.total_fee}</total_fee>
            <spbill_create_ip>{self.spbill_create_ip}</spbill_create_ip>
            <notify_url>{self.notify_url}</notify_url>
            <trade_type>{self.trade_type }</trade_type>
            <openid>{self.openid }</openid>      
            <sign>{self.sign}</sign>      
        </xml> 
        '''

        url = "https://api.mch.weixin.qq.com/pay/unifiedorder"
        # 如果发送的xml数据要把数据转化二进制。body_data.encode("utf-8")
        # request
        response = requests.post(url,data=body_data.encode("utf-8"),headers = {"content-type":"application/xml"} )
        #接收一个二进制的响应
        data_dict = self.xml_to_dict(response.content)
        pay_sign,timeStamp = self.get_two_sign(data_dict)
        data = {

            "timeStamp": timeStamp,
            "nonceStr": data_dict['nonce_str'],
            "package": f"prepay_id={data_dict['prepay_id']}",
            "signType": "MD5",
            "paySign":pay_sign
        }
        return data

my_ser

from rest_framework import serializers
from app01 import models

class wx_user_ser(serializers.ModelSerializer):
    class Meta:
        model = models.Wxuser
        fields = "__all__"

小程序支付流程

1 用户发起请求下单支付
2 我们要保证用是登入状态。
3 组织数据,请求统一下单接口,微信官方会同步返回一个prepay_id
4 重新组织数据,进行签名,将重新组织的数据返回给小程序,小程序在吊起支付。
5 用户就可以进行支付,支付结果会同步返回给小程序
6 后台修改订单支付状态是通过微信官方服务器的异步通知

xml解析模块

<xml>
  <appid name="属性值" >{.child.text}</appid>
   child.tag表示appid   
</xml> 

import xml.etree.ElementTree as ET

如果我们要解析一个xml文件
tree = ET.parse('country_data.xml')
root = tree.getroot()

如果解析字符串
root = ET.fromstring(country_data_as_string)

这个root是 Element 
for child in root:
     print(child.tag, child.attrib)
     #child.tag表是标签名,child.attrib表示获取属性
     #child.text就表示获取内容

小程序支付在总结

1 接收到支付请求。我们先组织数据,然后进行统一下单前的签名
- 请求的数据与响应的数据都是xml.请求的时候,xml数据要变成二进制,heards中的content-type:"application/xml"
-响应的数据也是xml,我要用xml.etree.ElementTree将他转化为字典
2 拿到统一下单数据,最重要的prepay_id,进行再次签名。把一下数据发送给小程序。
            "timeStamp": 时间戳
            "nonceStr":随机字符串
            "package": f"prepay_id={data_dict['prepay_id']}",统一下单中的到的prepay_id
            "signType": "MD5",
            "paySign":通过上面数据进行加密的结果
 3 小程序掉用wx.resquestPayment()吊起支付

以后再支付文档

1 统一下单
签名:80%。
签名方式没有搞懂。用了哪些数据,传过去的数据,和签名的数据不一致。
appid = 123 传过去,appid =456
appid 123  --->apid

每一个数据的类型,长短,意义是什么?一定要搞清楚,
2异步回调

微信小程序的加密解密以及小程序的支付

上一篇:微信小程序踩坑(一)——如何微信小程序中引用第三方UI库


下一篇:小程序支付