小程序可以通过官方提供的登录能力来获取用户身份的标示,具体文档可以参考 这里,通过流程时序可以看到,对于需要和前端配合的服务端开发,主要实现的就是通过小程序提供的 code 换取用户的 openid 和 session_key,并用换取到的 openid 和 secret_key 作为自定义的登录态。分析后得知,作为小程序后端的开发,主要实现以下几部分内容:
* 提供一个 HTTP 接口,供小程序方使用,传递 code;
* 换取用户身份标识;
* 维护一个自定义的登录态;
* 需要存储用户的 openid,以备后续使用。
提供给小程序一个 HTTP 接口,接口使用 Tornado 框架
为了简化思路,下面代码都没有做各种异常处理!
class LoginHandler(RequestHandler): def post(self): req_data = json.loads(self.request.body) js_code = req_data.get('js_code') # 这里是换取用户的信息 user_info = get_user_info(js_code=js_code) openid = user_info['openid'] session_key = user_info['session_key'] user_uuid = str(uuid.uuid4()) # 暴露给小程序端的用户标示 # 用来维护用户的登录态 User.save_user_session( user_uuid=user_uuid, openid=openid, session_key=session_key ) # 微信小程序不能设置cookie,把用户信息存在了 headers 中 self.set_header('Authorization', user_uuid) # 存储用户信息 User.save_user_info(open_id=openid) self.set_status(204)
def get_user_info(js_code): req_params = { "appid": 'app_id', # 小程序的 ID "secret": 'secret', # 小程序的 secret "js_code": js_code, "grant_type": 'authorization_code' } req_result = requests.get('https://api.weixin.qq.com/sns/jscode2session', params=req_params, timeout=3, verify=False)
class User(object): REDIS_EXPIRES = 7 * 24 * 60 * 60 table = tables['user'] @classmethod def save_user_session(cls, user_uuid, openid, session_key): user_session_value = { 'openid': openid, 'session_key': session_key } user_session_key = 'US:' + user_uuid with user_redis.pipeline(transaction=False) as pipe: pipe.hmset(user_session_key, user_session_value) pipe.expire(user_session_key, cls.REDIS_EXPIRES) pipe.execute()
class User(object): table = tables['user'] @classmethod def save_user_info(cls, open_id): # 存储用户 sql = cls.table.insert().values(open_id=open_id)
SQL 语句
CREATE TABLE `user` ( `id` int(20) unsigned NOT NULL AUTO_INCREMENT, `open_id` varchar(32) NOT NULL COMMENT '用户 open_id', `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_oid` (`open_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
全部代码可以点击 这里 获取!
在小程序的开发过程中,会存在模板消息的发送,具体文档见 这里,模板消息的发送是和语言无关的,这里将简要写一下怎么用 Python 给用户发送模板消息。 通过文档可以知道,发送的时候,需要使用小程序的 access_token 以及用户提交的 form_id,这里实现小程序的发送也就主要分为三部分:
1. 获取小程序的 access_token;
2. 获取用户提交的 form_id;
3. 给用户发送模板消息。
def get_access_token(): payload = { 'grant_type': 'client_credential', 'appid': 'appid', 'secret': 'secret' } req = requests.get('https://api.weixin.qq.com/cgi-bin/token', params=payload, timeout=3, verify=False) access_token = req.json().get('access_token') redis.set('ACCESS_TOKEN', access_token)
class FormHandler(RequestHandler): def post(self): req_data = self.request.body req_data = json.loads(req_data) form_id = req_data.get('form_id') template_push(form_id) # 使用消息进行模板推送
def template_push(form_id): data = { "touser": 'openid', "template_id": 'template_id', "page": 'pages/index/index', "form_id": form_id, "data": { 'keyword1': { 'value': 'value1' } }, "emphasis_keyword": '' } access_token = redis.get('ACCESS_TOKEN') push_url = 'https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token={}'.format(access_token) requests.post(push_url, json=data, timeout=3, verify=False)
至此,用户就会收到消息了,详细代码点击 这里
根据微信提供的文档, 后端要和小程序本身进行交互,只需要提供一个接口获取到加密数据和 iv 就可以了,获取到后,直接可以使用示例代码进行解密,如下:
import base64 import json from Crypto.Cipher import AES def decrypt(app_id, session_key, encrypted_data, iv): # base64 decode session_key = base64.b64decode(session_key) encrypted_data = base64.b64decode(encrypted_data) iv = base64.b64decode(iv) cipher = AES.new(session_key, AES.MODE_CBC, iv) try: decrypted = json.loads(_unpad(cipher.decrypt(encrypted_data))) except Exception as e: logging.warning('decrypt failed {}'.format(e)) return None if decrypted['watermark']['appid'] != app_id: return None return decrypted def _unpad(s): return s[:-ord(s[len(s)-1:])]
官方文档
import cString import requests from tornado.web import authenticated, RequestHandler URL = 'https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode' def fetch_qrcode_image(path, width=430): params = {'access_token': 'access_token'} json_data = { 'width': width, 'path': path } res = requests.post(URL, params=params, json=json_data, timeout=3, verify=False) qrcode_image = cStringIO.StringIO(res.content) return qrcode_image.getvalue() # tornado class QRCodeHandler(RequestHandler): def get(self): path = self.get_argument('path') if not path: self.set_status(404) return qr_code = fetch_qrcode_image(path) if not qr_code: self.set_status(404) return self.set_header('Content-Type', 'image/jpeg') self.finish(qr_code)