
HTML 页面里直接生成验证码不现实
浏览器端纯 HTML + JS 无法安全生成真正可用的验证码。所谓“前端生成”,实际只是画几条线、加点噪点、拼几个随机字符——Math.random() 产生的字符串没服务端校验,绕过只要看源码或重放请求就行。
真实场景下,验证码的核心是「服务端生成 + 服务端比对」,前端只负责展示和提交。你看到的 src="/captcha" 图片,背后一定是后端接口在动态画图、存 session 或 Redis,并返回图片二进制流。
常见错误现象:404 找不到验证码图片、500 后端报错、输入正确却提示“验证码错误”——八成是 session 未开启、Redis 连接失败,或前后端时间/域名不一致导致 cookie 未携带。
用 Python Flask 快速搭一个可工作的验证码接口
不用第三方库也能做,但推荐用 pillow(绘图)+ secrets(安全随机)+ session(存储答案),避免 random 被预测。
立即学习“前端免费学习笔记(深入)”;
-
secrets.choice()代替random.choice(),防止字符被暴力枚举 - 验证码图片响应头必须设为
Content-Type: image/png,否则浏览器不渲染 - session key 建议用
session['captcha_code']存,别用固定变量名如captcha,易冲突 - 生成后立即调用
session.modified = True,某些部署环境(如 uWSGI)会忽略未标记的 session 变更
from flask import Flask, session, make_response
from PIL import Image, ImageDraw, ImageFont
import io, secrets
<p>app = Flask(<strong>name</strong>)
app.secret_key = 'your-secret-key'</p><p>@app.route('/captcha')
def captcha():
code = ''.join(secrets.choice('ABCDEFGHJKLMNPQRSTUVWXYZ23456789') for _ in range(4))
session['captcha_code'] = code.lower() # 统一小写比对
session.modified = True</p><pre class='brush:php;toolbar:false;'>img = Image.new('RGB', (120, 40), color=(255, 255, 255))
d = ImageDraw.Draw(img)
d.text((10, 10), code, fill=(0, 0, 0))
buf = io.BytesIO()
img.save(buf, format='PNG')
buf.seek(0)
resp = make_response(buf.getvalue())
resp.headers['Content-Type'] = 'image/png'
return resp前端怎么安全提交并校验
重点不是“怎么显示验证码图片”,而是“怎么不让它被自动识别或跳过”。关键动作就两个:每次刷新图片时更新 src 的时间戳参数,表单提交时把用户输入和当前 session 中的值比对。
- 图片
src加上?t=Date.now()防缓存,不然点刷新按钮没反应 - 表单提交前检查
input[name="captcha"]是否为空,空则阻止提交(别只靠后端拦) - 后端校验完立刻清空
session['captcha_code'],防止同一验证码重复使用 - 不要在 HTML 里用
data-code或注释写答案,爬虫一扫就暴露
示例片段:
@@##@@document.write(Date.now())</script>" alt="验证码">
<input type="text" name="captcha" autocomplete="off">
<button type="button" onclick="document.getElementById('captcha-img').src='/captcha?t='+Date.now()">换一张</button>为什么不用现成的验证码服务(如 Google reCAPTCHA)
如果你只是想防脚本批量注册或发帖,reCAPTCHA v3 或 hCaptcha 确实省事;但它们不返回可读文本,无法满足“用户输入四个字母”的传统需求,且依赖外网、有隐私合规风险(GDPR、国内个保法)。
自建的坑主要在部署环节:Nginx 默认限制图片响应体大小,可能截断 PNG 流,需确认 client_max_body_size 和 proxy_buffering off;另外,若用 Gunicorn + Flask,记得关掉 --preload,否则多进程下 secrets 初始化可能出问题。
最常被忽略的一点:本地开发时 session 存内存没问题,上线必须切到 RedisSessionInterface 或数据库,否则负载均衡下用户换机器就永远对不上验证码。










