在用户登录、注册或敏感操作环节,使用 CAPTCHA 能有效阻挡自动化脚本。但很多开发者把精力花在生成扭曲字符的图片上,却忽略了服务端验证这一环。真正的安全边界必须由服务器掌控,任何脱离服务端状态的校验都形同虚设。

为什么不能只做前端验证
前端验证指的是把验证码答案通过隐藏输入框、JavaScript 变量或 Cookie 直接交给浏览器,提交时由页面自己比对。这种方式在浏览器开发者工具里一目了然,攻击者可以读取答案后构造请求,完全绕过了防护。即便你对答案做了简单加密,密钥也迟早会在前端代码中被找到。
从原理上看,CAPTCHA 的目的是证明请求来自人类,而不是机器。这个证明必须由服务端独立确认,因为客户端环境是不可信的。服务端应当维护一份自己生成的答案记录,前端只拿到一个无意义的令牌,拿答案本身永远不该离开服务器。
基础服务端验证流程
一个最朴素的服务端验证思路是:用户访问页面时,Python 后端用随机库生成字符串,渲染成图片,并把字符串存到会话里。会话可以基于服务器内存、数据库或缓存。用户提交表单时,后端拿出会话中的答案与输入比对,一致则通过并清除记录。
下面用 Flask 演示一个基于会话的内存验证示例。这里用 session 存储答案,实际生产可用更健壮的存储。
from flask import Flask, session, request, render_template_string
import random, string
from captcha.image import ImageCaptcha
app = Flask(__name__)
app.secret_key = 'replace_with_strong_key'
def gen_code(length=4):
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length))
@app.route('/captcha')
def captcha():
code = gen_code()
session['captcha'] = code
image = ImageCaptcha().generate_image(code)
from io import BytesIO
buf = BytesIO()
image.save(buf, 'png')
buf.seek(0)
return buf.read(), 200, {'Content-Type': 'image/png'}
@app.route('/login', methods=['POST'])
def login():
user_input = request.form.get('code', '')
real_code = session.get('captcha', '')
if not real_code:
return '验证码已过期', 400
if user_input.upper() != real_code.upper():
return '验证码错误', 400
session.pop('captcha', None)
return '验证通过'
上面的代码在会话中保存答案,校验时取出比对并立即删除,保证验证码一次性。但使用默认 session 存储在单机内存时,多实例部署会导致会话不同步,这时需要集中式存储。
用 Redis 管理验证码状态
当系统横向扩展,把验证码存到 Redis 是常见做法。服务端生成令牌返回前端,答案以令牌为键存进 Redis 并设置短过期时间。这样无论请求落到哪台应用服务器,都能查到同一份答案。
以下示例展示用 Redis 存储,令牌通过接口返回给前端,前端提交时携带令牌和用户输入。
import redis
import uuid
import random, string
from flask import Flask, request, jsonify
app = Flask(__name__)
r = redis.Redis(host='127.0.0.1', port=6379, db=0)
def gen_code(length=4):
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length))
@app.route('/get_captcha')
def get_captcha():
token = str(uuid.uuid4())
code = gen_code()
r.setex('captcha:' + token, 120, code)
return jsonify({'token': token})
@app.route('/verify', methods=['POST'])
def verify():
data = request.get_json()
token = data.get('token', '')
user_input = data.get('code', '')
key = 'captcha:' + token
real_code = r.get(key)
if not real_code:
return jsonify({'ok': False, 'msg': '验证码过期或不存在'})
if user_input.upper() != real_code.decode().upper():
return jsonify({'ok': False, 'msg': '验证码错误'})
r.delete(key)
return jsonify({'ok': True})
Redis 方案的优势是天然支持分布式与自动过期,不必担心应用重启丢状态。要注意的是令牌必须具备足够随机性,使用 uuid 或 secrets 模块生成,避免被遍历猜测。
防止暴力破解的补充策略
即便有验证码,若服务端不限制尝试频率,攻击者可用脚本高速试错。应在验证失败时对该令牌或用户 IP 增加尝试计数,超过阈值直接拒绝或延长冷却时间。
另外,验证码答案比对要使用恒定时间比较函数,避免时序攻击。Python 的 hmac.compare_digest 可用于此场景。同时不要在响应里透露答案是否正确之外的多余信息,防止辅助破解。
import hmac
def safe_equal(a, b):
return hmac.compare_digest(a.upper().encode(), b.upper().encode())
# 使用示例
if safe_equal(user_input, real_code):
pass
综合来看,Python CAPTCHA 的服务端验证重点在于答案不出服务器、状态集中可控、一次性销毁以及频率限制。把这些环节串起来,才能构建真正起作用的机器流量防线。