前言

微信公众号,已经是目前开发者不容错过的一个通道了,像是很多餐厅都通过微信扫码点餐,很多网站都是舍弃了原本的账号密码登陆,而改用微信公众号关注登陆。针对于这个场景,今天来使用python和golang来实现一下。

流程

大致流程是这样:

  • 首先我们要获取二维码和场景值
  • 通过配置公众号回调地址来将场景值和获取的open_id存到redis,同时获取新用户的信息
  • 然后前端通过轮询或者websocket请求登陆接口来实现。查找场景值对应的open_id,通过数据库的数据生成自定义的登陆状态。

1. 配置公众号

没有服务号没有关系,微信推出了一个测试服务号接口的地址

我们需要配置URL和Token,URL要填写你能够收到信息的服务器地址,Token你可以用某些包来生成,也可以用脸在键盘上滚几圈。

文档地址

我这里用fastapi来做一下模拟,需要注意的是,文档上所需要的返回数据是text/plain而不是application/json,所以我们使用PlainTextResponse

import hashlib

from fastapi import FastAPI, Query
from fastapi.responses import JSONResponse, PlainTextResponse

app = FastAPI()

@app.get('/wechat/')
async def WeChat(
        signature: str = Query(...),
        timestamp: str = Query(...),
        nonce: str = Query(...),
        echostr: str = Query(...)
                 ):

    if not signature:
        return JSONResponse("dasdfafd")
    # token 微信公众平台自定义token
    token = "fjklsdjfksdjkflsd"
    # 将token、timestamp、nonce三个参数进行字典序排序
    list1 = [token, timestamp, nonce]
    list1.sort()
    # 将三个参数字符串拼接成一个字符串进行sha1加密
    info = "".join(list1)
    sha1 = hashlib.sha1()
    sha1.update(info.encode())
    hashcode = sha1.hexdigest()
    if hashcode == signature:
        return PlainTextResponse(echostr)
    else:
        return ""


if __name__ == '__main__':
    import uvicorn

    uvicorn.run(app='test_requests:app', host="0.0.0.0", port=8010, reload=True)

点击一下就可以了。

2. 生成带参数的二维码

文档地址,需要注意的是,公众号每天获取access_token的次数是2000次,而每个access_token的过期时间是两个小时,所以我们在生产中,可以将access_token存到缓存中。

class MyWeChatOAuth:

    def __init__(self):
        self.app_id = 'XXX'
        self.secret = 'XXX'

    def get_token(self):
        """
        获取access_token
        """
        url = f"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={self.app_id}&secret={self.secret}"
        ret = requests.get(url=url)
        content = (ret.content).decode("utf-8")
        res = json.loads(content)
        return res.get("access_token")

    def get_ticket(self):
        url = f"https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={self.get_token()}"
        scene_id = int(time.time() * 100000)
        data = {
            "expire_seconds": 300,
            "action_name": "QR_SCENE",
            "action_info": {
                "scene": {
                    "scene_id": scene_id
                }
            }
        }
        rep = requests.post(url, json=data)
        content = (rep.content).decode("utf-8")
        js = json.loads(content)
        ticket = js.get("ticket")
        ret_url = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={}".format(ticket)
        return ret_url

ret_url就是二维码的地址,此时需要将场景值scene_id返回给前端。

3. 关注/取消事件

文档地址,这个地址就要写你在配置的时候填写的url了。

判断Event,关注着的为SCAN,未关注为subscribe,取消关注为unsubscribe。这里的业务逻辑就要自己DIY了,我的选择是将获取到的用户信息入库,然后将场景值和open_id(也就是和Event同级的FromUserName)存入redis,将新关注的用户获取用户信息入库,这里就不放业务代码了。无非就是读库写库操作。

@app.post('/wechat/')
async def create_wechat(
        request: Request,
                        ):
    req = await request.body()
    xmlmsg = xmltodict.parse(req)
    if xmlmsg["xml"]["Event"] == "SCAN":
        # 已经关注的
        pass
    elif xmlmsg["xml"]["Event"] == "subscribe":
        # 没有关注的
        pass
    else:
        # 其他
        pass
    return JSONResponse("ok")

4. 获取用户信息

通过access_token和open_id来获取该用户的信息。

url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token={access_token}&openid={open_id}&lang=zh_CN"
resp = requests.get(url.format(access_token=access_token, open_id=xmlmsg["xml"]["FromUserName"]))
resp.encoding = 'utf-8'

Golang实现

由于最近在学习go语言,所以用python实现了一遍之后,还要再用go实现一遍,也为目前go的资料不多贡献一丢丢可以CV的代码。

// 获取access_token
func GetAccessToken() (string) {
	var (
		AppId string = "XXX"
		Secret string = "XXX"
	)
	// 拼接url
	url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
		AppId,
		Secret)
	// 初始化map
	result := make(map[string]string)
	ret, _ := http.Get(url)
	// 读取结果
	content, _ := ioutil.ReadAll(ret.Body)
	// 赋值
	_ = json.Unmarshal([]byte(content), &result)
	return result["access_token"]
}


// 获取二维码
func GetTicket() map[string]interface{} {
	result := make(map[string]interface{})
	data := make(map[string]interface{})
	url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s", GetAccessToken())
	scene := fmt.Sprintf("%010v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(100000000))
	scene_id,_ := strconv.ParseInt(scene, 10, 64)
	data["expire_seconds"] = 300
	data["action_name"] = "QR_SCENE"
	action_info := make(map[string]interface{})
	scene_ids := make(map[string]int64)
	scene_ids["scene_id"] = scene_id
	action_info["scene"] = scene_ids
	data["action_info"] = action_info
	req,_ := json.Marshal(data)
	//json_data := bytes.NewBuffer(req)
	resp, _ := http.Post(url, "application/json", bytes.NewBuffer(req))
	content, _ := ioutil.ReadAll(resp.Body)
	// 赋值
	var res = make(map[string]string)
	_ = json.Unmarshal([]byte(content), &res)
	result["ticket"] = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket="+res["ticket"]
	result["scene_id"] = scene_id
	return result
}

回调的话需要启一个http服务,我没有使用go自带的包,而是使用了web框架gin

type XmlData struct {
	FromUserName string `xml:"FromUserName"`
	Event string `xml:"Event"`
	EventKey string `xml:"EventKey"`
}


r := gin.New()
	r.POST("/wechat/", func(c *gin.Context) {
    // 获取请求体
		res := c.Request
    // 使用结构体来接收xml的数据
		var xml_data XmlData
    // 转换
		content, _ := ioutil.ReadAll(res.Body)
		fmt.Println(string(content))
    // 赋值到结构体
		_ = xml.Unmarshal([]byte(content), &xml_data)
		//fmt.Println(xml_data.Event)
		//fmt.Println(xml_data.FromUserName)
		c.String(http.StatusOK, "hello Salmon!")
	})
	_ = r.Run(":8020")

然后业务代码就DIY吧,ps:吐槽一下,go写业务是真的有些麻烦,也可能是python写习惯的原因吧。