Otra implementación de captcha para Flask y WTForms
Quememos sus GPUs de aprendizaje profundo.
En el pasado escribí un captcha en PHP para limitar el registro de boletines de noticias por correo electrónico, funcionó bien, de hecho todavía está en uso hoy en día. No se pueden bloquear los registros de spam. Hay robots de registro, pero también hay personas a las que se les paga unos cuantos dólares para que inunden su sitio web con cuentas falsas o troll. Esa es la realidad y tenemos que afrontarla. Y ahora también hay un aprendizaje profundo que puede ser usado para romper nuestro código de captcha en sólo 15 minutos.
Nosotros, las personas que desarrollamos sitios web, debemos tener ideas para desafiar los registros falsos. No es posible, pero se pueden hacer pocas cosas y añadir un captcha es una de ellas.
Para muchas cosas probablemente aburridas, puedes encontrar suficientes bibliotecas haciendo esto por ti. Yo, no quiero usar una biblioteca, quiero escribir mi propio captcha en Python, aprender bebé. Y debido a que valoro la privacidad de los visitantes de mis sitios web, tampoco quiero usar ReCaptcha u otra solución servida remotamente.
La solución de captcha que se presenta a continuación funciona pero no está terminada, sólo hay que añadir la distorsión básica. No es realmente específico de Flask, es sólo Python. Utilicé BytesIO por primera vez para guardar una imagen no en un archivo sino en la memoria. Lo que es específico de Flask es cómo mostrarlo en un formulario de registro y en una página web. Tenga en cuenta que esta no es una solución completa, sólo detalla las partes más importantes.
Así es como se genera el captcha. captcha_fonts_path() es una función que necesita hacer para importar la(s) fuente(s) para el captcha.
import math
import random
import secrets
from PIL import Image, ImageFont, ImageDraw, ImageOps
from io import BytesIO
def captcha_create():
use_chars = ['A', 'b', 'C', 'd', 'e', 'f', 'h', 'K', 'M', 'N', 'p', 'R', 'S', 'T', 'W', 'X', 'Y', 'Z']
use_nums = ['2', '3', '4', '6', '7', '8', '9']
code_char_count = 5
code_chars = []
for i in range(code_char_count):
s = random.randrange(2)
if s & 1:
code_chars.append( random.choice(use_chars) )
else:
code_chars.append( random.choice(use_nums) )
# captcha dimensions
w = 300
h = 100
background_color = (255, 255, 255)
im_captcha = Image.new('RGB', (w, h), background_color)
# equal width pieces for each code char
w_code_char = int(math.floor(w/code_char_count))
h_code_char = h
ttf_file = os.path.join(captcha_fonts_path(), 'Arial.ttf')
font_size = int(w_code_char * 3/4)
draw_x = int(w_code_char/4)
draw_y = int(h/4)
# draw code chars one by one at different angle (with different font)
w_code_char_offset = 0
for code_char in code_chars:
im_font = ImageFont.truetype(ttf_file, font_size)
im_code_char = Image.new('RGB', (w_code_char, h_code_char), background_color)
draw = ImageDraw.Draw(im_code_char)
# see font size
draw.text( (draw_x, draw_y), code_char, fill='black', font=im_font)
# random angle is between -20 - +20
angle = random.randrange(40) - 20
im_code_char_rotated = im_code_char.rotate(angle, fillcolor=background_color)
# paste char image into chars image
im_captcha.paste(im_code_char_rotated, (w_code_char_offset, 0))
w_code_char_offset += w_code_char
# do some line / distortion stuff (to be implemented)
# save (in memory) as jpg
im_captcha_io = BytesIO()
im_captcha.save(im_captcha_io, format='JPEG')
captcha_code = ''.join(code_chars)
captcha_image_data = im_captcha_io.getvalue()
captcha_token = secrets.token_urlsafe()
captcha_token_hashed = hash_string(captcha_token)
return captcha_token, captcha_token_hashed, captcha_code, captcha_image_data
Para mostrar la imagen captcha con Flask en su navegador:
@auth.route('/captcha', methods=['GET'])
def captcha():
captcha_token, captcha_token_hashed, captcha_code, captcha_image_data = captcha_create()
resp = make_response(captcha_image_data)
resp.content_type = "image/jpeg"
return resp
Ahora también deberíamos mostrar la imagen captcha en un formulario. Utilizo Flask con Flask-WTForms y tengo una macro Jinja que pone todos los campos del formulario en la página. Esto ahorra mucho tiempo! Para poner la imagen captcha en la forma necesitamos un widget. ¿Cómo hacer esto? Ciertamente no soy un profesional de WTForms y creo que debería haber muchos más ejemplos de widgets. Esta es mi solución. En form.py he creado un widget para CaptchaCodeField, que no hace más que poner la imagen en la pantalla:
class CaptchaCodeOutput(Input):
def __call__(self, field, **kwargs):
return kwargs.get('value', '<img src="' + field._value() + '" style="border: 1px solid #dddddd; ">')
class CaptchaCodeField(StringField):
widget = CaptchaCodeOutput()
class AuthRegisterForm(FlaskForm):
....
captcha_token = HiddenField('Captcha token')
captcha_image_url = CaptchaCodeField(_l('Captcha image url'))
captcha_code = StringField(_l('Code'), filters=[strip_whitespace], validators=[
DataRequired()])
accept_conditions = BooleanField(_l('I have read and accept the privacy policy *'),validators=[
DataRequired()])
submit = SubmitField(_l('Register'))
def validate_password(self, field):
password = field.data
if not valid_password(password):
raise ValidationError(valid_password_message())
Luego en views.py relleno la url de la imagen captcha en este campo antes de llamar a render_template():
....
captcha_image_url = url_for('auth.captcha', captcha_token=captcha_token)
form.captcha_token.data = captcha_token
form.captcha_image_url.data = captcha_image_url
if captcha_error:
flash(_l('The code you entered did not match the code from the image. Please try again.'))
return render_template(
'auth/register_step2.html',
form=form)
Creo que todavía vale la pena poner un captcha en su sitio web si los visitantes de su sitio web no tienen que pagar por algo. Por supuesto que ahora tenemos un aprendizaje profundo, pero la caja de herramientas de desarrolladores de captcha también está más llena hoy en día. Piensa en usar diferentes tipos de letra, diferentes tipos de letra, diferentes anchos y alturas, añade distorsión, invierte los colores, sé creativo, sé impredecible, ¡quememos sus GPUs de aprendizaje profundo! También puede considerar otras soluciones de captcha como Q&A.
ImportarError: El módulo _imagingft C no está instalado
Por supuesto, otro mensaje de error agradable apareció al usar Pillow, ver arriba. Esto se puede resolver añadiendo freetype, freetype-dev. Puede comprobar esto entrando en su contenedor e interactuando ejecutando:
phython3
y luego escribir a máquina:
from PIL import features
features.check('freetype2')
Si freetype está instalado esto devolverá True, si no está instalado devolverá False. Como estoy usando Docker y la imagen Alpine, tuve que hacer cambios en mi Dockerfile:
FROM python:3.6-alpine as base
MAINTAINER Peter Mooring peterpm@xs4all.nl peter@petermooring.com
RUN mkdir /svc
WORKDIR /svc
COPY requirements.txt .
# install package dependencies
# COPY requirements.txt /requirements.txt, requirements.txt already copied
# Solve 'No working compiler found' error,
# see: https://github.com/gliderlabs/docker-alpine/issues/458
# Solve 'The headers or library files could not be found for jpeg, a required dependency when compiling Pillow from source.',
# see https://blog.sneawo.com/blog/2017/09/07/how-to-install-pillow-psycopg-pylibmc-packages-in-pythonalpine-image/
# Solve: ImportError: The _imagingft C module is not installed (when using '.paste' of Pillow)
# Solved after adding freetype, freetype-dev
RUN rm -rf /var/cache/apk/* && \
rm -rf /tmp/*
RUN apk update
# Instead, I run python setup.py bdist_wheel first, then run pip wheel -r requirements.txt for pypi packages.
RUN apk add --update \
curl \
python3 \
pkgconfig \
python3-dev \
openssl-dev \
libffi-dev \
musl-dev \
make \
gcc \
freetype \
freetype-dev \
jpeg-dev zlib-dev \
libmagic \
&& rm -rf /var/cache/apk/* \
&& pip wheel -r requirements.txt --wheel-dir=/svc/wheels
# the wheels are now here: /svc/wheels
FROM python:3.6-alpine
RUN apk add --no-cache \
freetype \
freetype-dev \
jpeg-dev zlib-dev \
libmagic
COPY --from=base /svc /svc
WORKDIR /svc
RUN pip install --no-index --find-links=/svc/wheels -r requirements.txt
# after installation, remove wheels, does not free up space, probably because we are in new layer, too bad is some 20MB
#RUN rm -R *
# create and set working directory
RUN mkdir -p /home/flask/app/web
WORKDIR /home/flask/app/web
# copy app code into container
COPY . ./
# create group and user used in this container
RUN addgroup flaskgroup && adduser -D flaskuser -G flaskgroup && chown -R flaskuser:flaskgroup /home/flask
USER flaskuser
Enlaces / créditos
How I developed a captcha cracker for my University's website
https://dev.to/presto412/how-i-cracked-the-captcha-on-my-universitys-website-237j
How to break a CAPTCHA system in 15 minutes with Machine Learning
https://medium.com/@ageitgey/how-to-break-a-captcha-system-in-15-minutes-with-machine-learning-dbebb035a710
Recientes
- Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web
- Don't Repeat Yourself (DRY) con Jinja2
- SQLAlchemy, PostgreSQL, número máximo de filas por user
- Mostrar los valores en filtros dinámicos SQLAlchemy
- Transferencia de datos segura con cifrado de Public Key y pyNaCl
- rqlite: una alternativa de alta disponibilidad y dist distribuida SQLite
Más vistos
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando PyInstaller y Cython para crear un ejecutable de Python
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados
- Flask RESTful API validación de parámetros de solicitud con esquemas Marshmallow