Django微信公众号开发:签名校验、消息路由与access_token缓存实践
2026/9/16 6:16:23 网站建设 项目流程

简介:面向微信公众号开发者与Django学习者,这份源码提供了一个完整的前后端集成示例,涵盖公众号接口对接、页面渲染、数据配置与样式交互等常见开发环节,适合有一定Python基础、希望从实际项目中理解公众号开发流程的中级工程师。压缩包共493个文件,约70.77MB,以Python脚本、JavaScript脚本、GIF动图、CSS样式、HTML页面为主,其中Python处理后端逻辑,JavaScript与CSS负责前端交互,GIF和HTML构成展示素材;同时配有XML/JSON配置、Markdown笔记、SQLite数据库和Android客户端安装包,方便直接研究或改造复用。资源按功能模块组织,目录层次清晰,可快速定位公众号菜单配置、消息处理、Web展示、静态资源管理等代码片段。同时附带的批处理脚本、配置模板和日志文件,为本地部署调试与排错提供了参考。目前已有326人学习下载,适合需要完整项目样例来巩固Django与微信公众号开发技能的读者。

1. 基于 Django 框架的微信公众号开发与设计源码,解决的是工程而不是接口示例

搜「基于 Django 框架的微信公众号开发与设计源码」的人,通常不缺微信官方文档,缺的是一份能直接跑的 Django 工程。公众号后台的「服务器配置」要求填一个 URL,之后微信服务器会把验证请求、用户消息、菜单事件全部 POST 到这个地址。也就是说,公众号开发的第一道坎不是调接口,而是先有一个长期可用的 Django 入口,再组织好验签、消息路由和 access_token 缓存。这个标题把 Django 与微信公众号绑在一起,讲的是用 URL 路由、ORM、缓存把微信回调与业务数据做成完整系统。适合刚接手公众号项目的 Python 工程师,也适合把公众号能力并入现有 Django 服务的团队。一个反直觉的结论:最耗时的是签名校验、超时重试、token 过期这类边界问题,把源码按「回调层、业务层、API 封装层」切分,才是标题里「设计」二字的真正落点。

2. 微信公众号回调的三个约定:签名校验、XML 消息路由与 access_token 生命周期

2.1 服务器配置里的 Token 签名校验怎么算

公众号后台「基本配置」里,URL 和 Token 决定回调能否生效。保存配置时微信服务器会向你的 URL 发一个 GET 请求,带 signature、timestamp、nonce、echostr 四个参数。校验算法是固定的:把 Token、timestamp、nonce 三个字符串按字典序排序后拼接,再做 SHA1,结果与 signature 相等就说明请求确实来自微信,此时把 echostr 原样返回,配置保存成功。

import hashlib from django.conf import settings def check_wechat_signature(signature, timestamp, nonce): token = settings.WECHAT_TOKEN tmp = "".join(sorted([token, timestamp, nonce])) return hashlib.sha1(tmp.encode("utf-8")).hexdigest() == signature

这里的sorted是字典序排序,不是按长度排;timestamp 和 nonce 都要保持字符串原样,不要转 int,否则排序结果变化、哈希对不上。公众号后台保存配置时,微信端用的就是同一套算法,任何一方拼接顺序不同都会验签失败。后续网页授权、JS-SDK 签名也共用 sha1 工具函数,建议把验签放独立的 crypto.py 统一维护。

2.2 用户消息是 POST 的 XML,路由表就是设计图

配置生效后,用户发消息、点菜单,微信都会 POST 一段 XML 到你的 URL。字段包括 ToUserName、FromUserName、CreateTime、MsgType、Content、MsgId;菜单点击则是 MsgType 为 event 的事件,带 Event 和 EventKey。回调入口只有一个,进入后要按消息类型分流,路由表应当是开发前第一张画出来的图。

MsgType触发场景常用处理
text用户发文本关键词回复、业务查询
image / voice / video发媒体素材落库、内容审核
event(subscribe)关注 / 扫码写用户表、发欢迎语
event(CLICK)菜单点击按 EventKey 路由
event(LOCATION)上报位置门店、区域运营

我一般把{消息类型: 处理函数}维护成字典注册表,代替一长串 if-else,新增类型只需注册新函数。还要记住一个硬约束:微信要求在 5 秒内响应,超时会重试三次;业务超过 5 秒(查库、调外部接口)时,回调里先返回空串,真正的回复交给异步任务走客服消息接口,这是公众号开发最常见的解耦方式。

2.3 access_token 两小时过期,缓存策略决定接口可用性

access_token 是公众号所有业务接口的全局票据,通过GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET获取,有效期 7200 秒。最差的写法是每次调用都重新获取,很快撞上频率限制;正确做法是全局维持一份,快过期时再刷新。

缓存方案适用规模注意点
进程内字典加时间戳单机调试多进程会各自刷 token
Django cache(LocMem)单机生产TIMEOUT 要小于 7200
Redis / Memcached多机负载均衡加锁防并发刷新

我一般直接用 Django cache,timeout 设 7000 秒,留 200 秒余量给网络抖动。多机部署必须换 Redis,并在刷新逻辑上加锁,否则两个进程同时发现 token 过期、各自刷新,后刷新的覆盖先刷新的,全站接口会间歇性报 40001。这个故障单机开发时完全复现不出来,却是生产环境最常见的微信接口故障来源。

3. 用 Django 写最小可运行源码:从 URL 配置到文本消息收发

3.1 项目骨架与 settings 里的关键项

建项目和 app 走标准流程,这就是 python django 搭建 web 项目里最典型的一个最小闭环:

python -m venv venv source venv/bin/activate pip install django requests django-admin startproject mp_project python manage.py startapp wechat

python manage.py startapp wechat创建 app 之后要立刻把 wechat 加进 INSTALLED_APPS,否则 urlconf 和 management command 不会被加载,新手在这一步最容易卡住。数据库用 SQLite 起步即可,量大了再切 MySQL;如果直接上 MySQL,先解决 django install mysqlclient 的编译依赖,Ubuntu 下先装 libmysqlclient-dev,Windows 下用预编译 wheel,Django ORM 层切库基本不用改代码。

# settings.py 追加 WECHAT_TOKEN = "your_token_here" # 与后台基本配置完全一致 WECHAT_APPID = "wx1234567890abcdef" WECHAT_SECRET = "secret_here" ALLOWED_HOSTS = ["*"] # 上线前改为真实域名

WECHAT_TOKEN就是公众号后台填的那个字符串,不要加盐、不要二次编码,否则验签永远失败。密钥建议从环境变量读取,提交源码到仓库或交付外包时尤其重要。

3.2 GET 分支:验签与 echostr 返回

回调 view 同时处理 GET 和 POST:GET 是后台保存配置时的验证请求,POST 是后续所有消息。验签结果直接决定配置能否保存成功:

# wechat/views.py import time import xml.etree.ElementTree as ET from django.http import HttpResponse, HttpResponseForbidden from django.views.decorators.csrf import csrf_exempt from wechat.crypto import check_wechat_signature @csrf_exempt def wechat_callback(request): if request.method == "GET": signature = request.GET.get("signature", "") timestamp = request.GET.get("timestamp", "") nonce = request.GET.get("nonce", "") if check_wechat_signature(signature, timestamp, nonce): return HttpResponse(request.GET.get("echostr", "")) return HttpResponseForbidden("signature mismatch") # POST 分支见 3.3

验签通过后必须把 echostr 原样返回,不能加换行或前后缀,否则后台提示「Token 验证失败」。失败时优先检查:Token 是否完全一致、服务器本机时间与标准时间偏差是否过大。时区配错或时间不同步会导致签名对不上,这在容器环境里很常见。

3.3 POST 分支:解析 XML 并回包

消息体是固定结构的 XML,用标准库 ElementTree 解析即可。微信服务器不会带 Django 的 CSRF cookie,所以必须加@csrf_exempt,安全性由签名校验兜底。

# wechat/views.py 续 def build_text_reply(to_user, from_user, content): xml = ( "<xml><ToUserName><![CDATA[{}]]></ToUserName>" "<FromUserName><![CDATA[{}]]></FromUserName>" "<CreateTime>{}</CreateTime>" "<MsgType><![CDATA[text]]></MsgType>" "<Content><![CDATA[{}]]></Content></xml>" ) return xml.format(to_user, from_user, int(time.time()), content) def _handle_post(request): try: root = ET.fromstring(request.body.decode("utf-8")) except ET.ParseError: return HttpResponse("") msg_type = root.findtext("MsgType") from_user = root.findtext("FromUserName") to_user = root.findtext("ToUserName") if msg_type == "text": content = root.findtext("Content", "").strip() # 在这里接业务逻辑:关键词、ORM 查询、内部服务 reply = build_text_reply(to_user, from_user, f"收到:{content}") return HttpResponse(reply, content_type="application/xml") if msg_type == "event" and root.findtext("Event") == "subscribe": return HttpResponse("") # 新关注逻辑见第 4 章 return HttpResponse("")

三个坑先说清楚。第一,不要用 request.POST,微信的 content-type 是 text/xml,Django 不会把它解析进 POST 字典,必须读 request.body 后自己解析。第二,回复 XML 里 FromUserName 和 ToUserName 要和收到时调换位置——收到的 FromUserName 是用户 openid,回复时要放到 ToUserName,写反了用户会看到「该公众号暂时无法回复」;第三,识别不了的消息类型直接返回空响应,不要回一段格式错误的 XML,那会触发微信侧的错误统计。ElementTree 不擅长输出 CDATA,所以回复文本直接用字符串拼接,这是公众号 XML 回包常见的保留写法。

提示:解析失败、消息类型未知时统一返回空字符串,微信不会重试也不会计入错误,这是回调开发里最安全的兜底行为。

3.4 urlconf 挂载与本地 curl 验证

最后把回调挂进 urls,本地用 curl 模拟微信的 GET 验证请求:

# wechat/urls.py from django.urls import path from . import views urlpatterns = [path("callback/", views.wechat_callback, name="wechat_callback")]
# mp_project/urls.py from django.urls import include, path urlpatterns = [path("wechat/", include("wechat.urls"))]

curl 验证时 signature 需要自己按同一算法算出来填入;用任意时间戳即可,因为校验只比较哈希,不校验时间窗口。返回 echostr 说明整条链路已经通了,这时再去公众号后台保存配置,一次就能过。

4. 把公众号能力接进 Django 工程:自定义菜单、模板消息与用户落库

4.1 用管理命令批量创建自定义菜单

菜单接口本质是一次 HTTP POST,写成 Django management command 而不是页面按钮触发,好处是可以幂等执行、上线可重放。菜单是嵌套 JSON,一级菜单最多 3 个,每个一级下最多 5 个二级菜单。

# wechat/management/commands/create_menu.py import requests from django.core.management.base import BaseCommand from wechat.utils import get_access_token class Command(BaseCommand): help = "创建公众号自定义菜单" def handle(self, *args, **options): menu = { "button": [ {"type": "view", "name": "官网", "url": "https://www.example.com"}, {"type": "click", "name": "今日推荐", "key": "DAILY_PICK"}, { "name": "更多", "sub_button": [ {"type": "view", "name": "历史文章", "url": "https://www.example.com/articles"}, ], }, ] } resp = requests.post( "https://api.weixin.qq.com/cgi-bin/menu/create", params={"access_token": get_access_token()}, json=menu, ).json() self.stdout.write(str(resp))

执行python manage.py create_menu即可。按钮 type 有 click、view、miniprogram、scancode_push 等;click 按钮靠 key 唯一标识,view 按钮直接跳网页。前后端分离架构下,view 的 url 指向前端独立域名,公众号内打开时微信会追加 fromsinglemessage 参数,前端要兼容。公众号网页授权回调则是另一个重定向高发场景,django 重定向传递数据最常见的做法是把目标地址编码进 state 参数,授权跳回后再按 state 解码分发,避免把业务参数直接暴露在 URL 上。

4.2 模板消息:ORM 用户表 + 活动开始提醒

模板消息用于服务通知,典型场景是活动开始提醒、订单状态变更。发送前提是拿到 openid 且用户关注了公众号,先建用户表:

# wechat/models.py from django.db import models class WxUser(models.Model): openid = models.CharField(max_length=64, unique=True) nickname = models.CharField(max_length=64, blank=True, default="") subscribe_time = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True)

把 WxUser 注册进 django admin 后,运营可以直接按 openid 查用户;admin 的 list_display 就是常见的 django admin 界面美化做法,不引第三方主题也能得到一个可用的运营后台。发送函数统一封装:

# wechat/services.py import requests from wechat.utils import get_access_token def send_template_message(openid, template_id, data, page=""): payload = { "touser": openid, "template_id": template_id, "page": page, "data": {key: {"value": value} for key, value in data.items()}, } return requests.post( "https://api.weixin.qq.com/cgi-bin/message/template/send", params={"access_token": get_access_token()}, json=payload, ).json()

调用时把{"thing1": "技术分享会", "time2": "19:00"}传给 data,template_id 从后台「模板消息」申请。data 的 key 必须与模板占位符一一对应,thing 类型限 20 字,超长直接报错。高频错误码 43004 表示用户未关注,业务层过滤;45009 表示接口超限,发送端要做节流。更稳妥的是维护一张发送记录表,记录 openid、template_id、状态,定时任务扫失败记录重试,这样每条通知都可审计。

4.3 access_token 统一走 Django cache

token 获取逻辑收敛到一个函数,所有调用方都走它:

# wechat/utils.py import requests from django.conf import settings from django.core.cache import cache def get_access_token(): token = cache.get("wechat_access_token") if token: return token resp = requests.get( "https://api.weixin.qq.com/cgi-bin/token", params={ "grant_type": "client_credential", "appid": settings.WECHAT_APPID, "secret": settings.WECHAT_SECRET, }, timeout=10, ).json() if "access_token" not in resp: raise RuntimeError(f"token 获取失败: {resp}") cache.set("wechat_access_token", resp["access_token"], timeout=7000) return resp["access_token"]

timeout 设 7000 而不是 7200,留出刷新余量。多机部署时把 CACHES 切到 Redis,并加刷新锁:进程 A 拿不到 token 去刷新、进程 B 也同时刷新,后写覆盖先写,就会出现间歇性 40001。还要确认 settings 里的全局CACHES["default"]["TIMEOUT"]没有把 7000 覆盖掉,这是缓存配置里很隐蔽的一个坑。

4.4 批量发送的异步边界

批量发模板消息不要在 view 里同步循环,那会拖垮响应。小规模用线程池,量再大上 celery 或 django-q:

# wechat/tasks.py from concurrent.futures import ThreadPoolExecutor from django.db import close_old_connections from wechat.models import WxUser from wechat.services import send_template_message def notify_all(template_id, data): openids = list(WxUser.objects.values_list("openid", flat=True)) def send(openid): try: send_template_message(openid, template_id, data) finally: close_old_connections() with ThreadPoolExecutor(max_workers=8) as pool: pool.map(send, openids)

Django ORM 的连接绑定线程,线程池里跑完必须close_old_connections(),否则连接池被占满,这是后台任务最常见的坑。微信对模板消息有分钟级限额,发送前按 openid 维度做节流,避免一个活动把额度打光。

5. 公众号上线前的验证清单:调试工具回放、错误码速查与验签装饰器

5.1 用接口调试工具回放请求

功能开发完别急着用手机反复发消息。公众号后台「开发」菜单下的在线接口调试工具可以直接回放请求、看原始报文,比手机更可控:选择接口、填 access_token 和参数、点「检查问题」,工具返回微信服务器原始响应。这对「文档一模一样却不生效」的场景定位非常有效。开发期建议申请一个测试号,AppID 和 Secret 独立,随便折腾不污染线上公众号。

5.2 高频错误码速查

errcode含义处理
40001access_token 无效或过期检查多进程覆盖,重新获取
40164调用 IP 不在白名单后台白名单加服务器出口 IP
45009接口频率超限本地限流,查循环调用

40001 不一定是真过期,更多是多个进程各持一份 token、后获取的覆盖先获取的。排查时看日志里 token 刷新时间是否过于密集,是就去加固 4.3 的缓存锁。40164 是换服务器、加负载均衡节点后最容易漏的:新节点出口 IP 没加白名单,接口会批量失败,现象是本地调试全通、线上全挂。

5.3 一个可复用的验签装饰器

把验签从 view 里抽成装饰器,任何需要微信侧调用的 view 都能一行注解搞定安全校验:

# wechat/decorators.py from functools import wraps from django.http import HttpResponseForbidden from wechat.crypto import check_wechat_signature def wechat_signature_required(view_func): @wraps(view_func) def _wrapped(request, *args, **kwargs): signature = request.GET.get("signature", "") timestamp = request.GET.get("timestamp", "") nonce = request.GET.get("nonce", "") if not check_wechat_signature(signature, timestamp, nonce): return HttpResponseForbidden("invalid signature") return view_func(request, *args, **kwargs) return _wrapped

回调 view 声明成@csrf_exempt@wechat_signature_required两层注解,先验签再进业务分发;任何没通过验签的请求在进入业务代码前就被拦下,这是回调地址暴露公网后最基础的一层防线。把这个装饰器、get_access_token 和 build_text_reply 三个组件抽出来,就是一套能复制到任意 Django 项目的公众号最小工具箱;最后把 WxUser 注册进 admin,运营人员在 Django 后台就能直接检索 openid、看关注时间,整个源码闭环到这里就完整了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询