Ajax captcha на django сайт
Капча нужна для того, чтобы формой обратной связи пользовались только люди, а не спам-боты.
Алгоритм работы встраиваемой капчи такой:
- В форму отправки сообщений добавляем поля для капчи: закодированое значение и поле для ввода.
- Во вью, которое отправляет сообщение будем делать проверку этого ввода.
Предлагаемый вариант изображения captcha не сохраняет картинку на сервере, а генерирует ее на лету.
- Весь представленный код будет соответствовать моему Django окружению
- В этой статье будет использован файл с утилитами
Приложение для отправки сообщений
Я считаю, что сообщения от клиентов необходимо не просто отправлять на почту, но и сохранять в админке.
Создадим новое приложение и установим необходимые зависимости
Консоль
(env) ./project $ pip install numpy (env) ./project $ cd apps/ (env) ./project/apps $ ../manage.py startapp feedback (env) ./project/apps $ mv feedback/apps.py feedback/app.py (env) ./project $ cd ../
Поправим файлы
./apps/feedback/__init__.py
default_app_config = "apps.feedback.app.FeedbackConfig"
./apps/feedback/app.py
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.apps import AppConfig
class FeedbackConfig(AppConfig):
name = 'apps.feedback'
verbose_name = u'Сообщения от клиентов сайта'
./apps/feedback/models.py
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils import timezone
# Сообщения от клиентов
class Message(models.Model):
name = models.CharField(u'Имя', max_length=191)
email = models.CharField(u'Электронная почта', max_length=191)
message = models.TextField(u'Сообщение')
date_pub = models.DateTimeField(u'Дата публикации', default=timezone.now)
class Meta:
verbose_name = u'сообщение'
verbose_name_plural = u'Сообщения клиентов'
ordering = ['-date_pub']
def __unicode__(self):
return self.name
./apps/feedback/admin.py
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from apps.feedback.models import Message
class MessageAdmin(admin.ModelAdmin):
list_display = ('name', 'email', 'message')
admin.site.register(Message, MessageAdmin)
Создадим файл с формой
./apps/feedback/forms.py
# -*- coding:utf-8 -*-
from django import forms
from apps.feedback.models import Message
class MessageForm(forms.ModelForm):
name = forms.CharField(
label=u'Представьтесь',
widget=forms.TextInput(attrs={'placeholder': u'Ваше имя'}))
email = forms.EmailField(
label=u'Email',
widget=forms.TextInput(attrs={'placeholder': u'Почта для ответа'}))
message = forms.CharField(
label=u'Текст сообщения',
widget=forms.Textarea(attrs={'placeholder': u'Сообщение', 'rows': 5, }),
max_length=2000)
antispam_hidden = forms.CharField(widget=forms.HiddenInput())
antispam_text = forms.CharField(
label=u'Строка антиспама',
widget=forms.TextInput(attrs={'placeholder': u'Введите код с картинки'}))
class Meta:
model = Message
fields = ['name', 'email', 'message']
Для корректной работы ./apps/feedback/views.py нужно задать несколько переменных:
admin.site.site_title— значение задается в файле./project/urls.pyMESSAGE_URL_START— значение задается в./project/settings.pyMESSAGE_URL_END— значение задается в./project/settings.pyEMAIL_USER— значение задается в./project/settings.pyEMAIL_PASS— значение задается в./project/settings.py
А также обработать редиректы на урлы
/sending_ok/— успешная отправка сообщений/sending_fail/— ошибка при отправке сообщений/spam_detected/— ошибка при вводе кода с картинки
./apps/feedback/views.py
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import hashlib
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib import admin
from django.template.response import TemplateResponse
from django.shortcuts import redirect
from django.views.decorators.csrf import csrf_exempt
from apps.feedback.models import Message
from apps.feedback.forms import MessageForm
from libs.captcha import ajax_captcha
from utils import mail
# Отобразить новую капчу и hidden поле
@csrf_exempt
def get_captcha(request):
captcha_image, hidden_value = ajax_captcha()
context = {
'captcha_image': captcha_image,
'hidden_value': hidden_value,
}
return TemplateResponse(request, "feedback/captcha.html", context=context)
# Отправка сообщения
def sendmessage(request):
form = MessageForm()
if request.POST:
form = MessageForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
if hashlib.sha1(cd['antispam_text']).hexdigest() == cd['antispam_hidden']:
try:
message = Message.objects.create(
name = cd['name'],
email = cd['email'],
message = cd['message']
)
message.save()
users = User.objects.all()
for u in users:
if u.groups.filter(name='message').exists() and u.email:
to = u.email
subject = u'Новое сообщение с сайта %s' % admin.site.site_title
text = u'''
<html>
<body>
<p>Поступило новое сообщение с сайта.</p>
<p>%s</p>
<p>%s</p>
<p>%s</p>
<p> </p>
<p>%s%d%s</p>
</body>
</html>''' % (message.name, message.email, message.message,
settings.MESSAGE_URL_START, message.id, settings.MESSAGE_URL_END)
mail(to, subject, text.encode('UTF-8'))
return redirect('/sending_ok/')
except:
return redirect('/sending_fail/')
else:
return redirect('/spam_detected/')
context = {
'form': form,
}
return TemplateResponse(request, "page.html", context=context)
Создадим файл ./apps/feedback/urls.py
./apps/feedback/urls.py
# -*- coding:utf-8 -*-
from django.conf.urls import url
from apps.feedback.views import get_captcha, sendmessage
urlpatterns = [
url(r'^get-captcha/$', get_captcha),
url(r'^sendmessage/$', sendmessage),
]
./project/settings.py
...
INSTALLED_APPS = [
...
'apps.feedback',
...
]
...
EMAIL_USER = u'' # Yandex логин
EMAIL_PASS = u'' # Yandex пароль
MESSAGE_URL_START = u'https://ДОМЕН/admin/common/message/' # Начало ссылки для письма из формы обратной связи
MESSAGE_URL_END = u'/change/' # конец ссылки
...
./project/urls.py
...
from django.contrib import admin
...
# Параметры меняют название и заголовки в админке. Удобно задавать для каждого сайта
admin.site.site_header = u''
admin.site.site_title = u''
admin.site.index_title = u''
...
urlpatterns = [
...
url(r'^feedback/', include('apps.feedback.urls')),
...
]
...
Теперь нужно сделать и применить миграции
Консоль
(env) ./project $ ./manage.py makemigrations (env) ./project $ ./manage.py migrate
Генерация и отображение картинки в форме
Для начала нужно скачать нужный шрифт и положить его в какую-нибудь папку. В моем случае шрифт называется AvidOmnes-Medium.otf и положил я его в папку ./static/fonts/
При генерации самой картинки я использовал статью Фрактальная капча на Python.
Доработанный файл положил в папку libs/
./libs/captcha.py
# -*- coding: utf-8 -*-
# FractalCaptcha.py v 0.1
# (c) Alexandr A Alexeev 2011 | http://eax.me/
import base64
import hashlib
import io
from numpy import mgrid, exp
from PIL import Image, ImageDraw, ImageFont, ImageFilter
from random import random
from utils import getRandomString
def gaussian_grid(size = 5):
m = size/2
n = m+1
x, y = mgrid[-m:n,-m:n]
fac = exp(m**2)
g = fac*exp(-0.5*(x**2 + y**2))
return g.round().astype(int)
class GAUSSIAN(ImageFilter.BuiltinFilter):
name = "Gaussian"
gg = gaussian_grid().flatten().tolist()
filterargs = (5,5), sum(gg), 0, tuple(gg)
def captcha(secret, width=240, height=80,
font_ttf='/path_to_project_folder/static/fonts/AvidOmnes-Medium.otf', fontSize=60,
blur = 1):
mask = Image.new('RGBA', (width, height))
font = ImageFont.truetype(font_ttf, fontSize)
x_offset = -10
draw = ImageDraw.Draw(mask)
for i in range(len(secret)):
x_offset += 20 + int(random()*20)
y_offset = -10 + int(random()*30)
draw.text((x_offset, y_offset), secret[i], font=font)
angle = -10 + int(random()*15)
mask = mask.rotate(angle)
bg = plazma(width, height)
fg = plazma(width, height)
result = Image.composite(bg, fg, mask)
if blur > 0:
for i in range(blur):
result = result.filter(GAUSSIAN)
return result
def plazma(width, height):
img = Image.new('RGB', (width, height))
pix = img.load();
for xy in [(0,0), (width-1, 0), (0, height-1), (width-1, height-1)]:
rgb = []
for i in range(3):
rgb.append(int(random()*100))
pix[xy[0],xy[1]] = (rgb[0], rgb[1], rgb[2])
plazmaRec(pix, 0, 0, width-1, height-1)
return img
def plazmaRec(pix, x1, y1, x2, y2):
if (abs(x1 - x2) <= 1) and (abs(y1 - y2) <= 1):
return
rgb = []
for i in range(3):
rgb.append((pix[x1, y1][i] + pix[x1, y2][i])/2)
rgb.append((pix[x2, y1][i] + pix[x2, y2][i])/2)
rgb.append((pix[x1, y1][i] + pix[x2, y1][i])/2)
rgb.append((pix[x1, y2][i] + pix[x2, y2][i])/2)
tmp = (pix[x1, y1][i] + pix[x1, y2][i] +
pix[x2, y1][i] + pix[x2, y2][i])/4
diagonal = ((x1-x2)**2 + (y1-y2)**2)**0.5
while True:
delta = int ( ((random() - 0.5)/100 * min(100, diagonal))*255 )
if (tmp + delta >= 0) and (tmp + delta <= 255):
tmp += delta
break
rgb.append(tmp)
pix[x1, (y1 + y2)/2] = (rgb[0], rgb[5], rgb[10])
pix[x2, (y1 + y2)/2]= (rgb[1], rgb[6], rgb[11])
pix[(x1 + x2)/2, y1] = (rgb[2], rgb[7], rgb[12])
pix[(x1 + x2)/2, y2] = (rgb[3], rgb[8], rgb[13])
pix[(x1 + x2)/2, (y1 + y2)/2] = (rgb[4], rgb[9], rgb[14])
plazmaRec(pix, x1, y1, (x1+x2)/2, (y1+y2)/2)
plazmaRec(pix, (x1+x2)/2, y1, x2, (y1+y2)/2)
plazmaRec(pix, x1, (y1+y2)/2, (x1+x2)/2, y2)
plazmaRec(pix, (x1+x2)/2, (y1+y2)/2, x2, y2)
# Функция генерирует картинку и хэш
def ajax_captcha():
rand_string = getRandomString(6)
result_captcha = captcha(rand_string)
bytes_io = io.BytesIO()
result_captcha.save(bytes_io, format='PNG')
img_str = base64.b64encode(bytes_io.getvalue())
hidden_value = hashlib.sha1(rand_string).hexdigest()
return 'data:image/png;base64,%s' % img_str, hidden_value
Также понадобится функция getRandomString из utils.py
Модификация шаблонов
Рассмотрим случай, когда форма с отправкой сообщения находится в подвале, то есть на всех страницах сайта. В этом случае нам надо генерировать картинку в контекст процессоре.
./context_processors.py
# -*- coding:utf-8 -*-
...
from apps.feedback.forms import MessageForm
from libs.captcha import ajax_captcha
...
def settings_vars(request):
...
cp_captcha_image, cp_hidden_value = ajax_captcha()
...
return {
...
'cp_message_form': MessageForm,
'cp_captcha_image': cp_captcha_image,
'cp_hidden_value': cp_hidden_value,
}
Создадим файл ./templates/feedback/captcha.html
./templates/feedback/captcha.html
<img src="{{ captcha_image }}">
<input type="hidden" value="{{ hidden_value }}" name="antispam_hidden">
Модификация подвала шаблона
./templates/_footer.hmtl
...
<form method="POST" action="/feedback/sendmessage/">{% csrf_token %}
<p>{{ cp_message_form.name }}{% if cp_message_form.name.errors %}<br /> {{ cp_message_form.name.errors }}{% endif %}</p>
<p>{{ cp_message_form.email }}{% if cp_message_form.email.errors %}<br /> {{ cp_message_form.email.errors }}{% endif %}</p>
<p>{{ cp_message_form.message }}{% if cp_message_form.message.errors %}<br /> {{ cp_message_form.message.errors }}{% endif %}</p>
<p>{{ cp_message_form.antispam_text }}{% if cp_message_form.antispam_text.errors %}<br /> {{ cp_message_form.antispam_text.errors }}{% endif %}</p>
<a class="btnajax" style="cursor: pointer;">?</a>
<div class="captcha">
<img src="{{ cp_captcha_image }}">
<input type="hidden" value="{{ cp_hidden_value }}" name="antispam_hidden">
</div>
<p><input type="submit" class="button alt" value="Отправить" /></p>
</form>
...
<script src="{{ STATIC_URL }}assets/js/jquery.min.js"></script>
...
<script type="text/javascript">
$(function() {
$('.btnajax').each(function() {
$(this).on('click', function() {
$('.captcha').html("Секунду...");
var value = $(this).attr('value');
$.ajax({
url: '/feedback/get-captcha/',
cache: false,
data: { value: value },
type: 'POST',
success: function(html) {
$('.captcha').html(html);
},
});
});
});
});
</script>
На этом этапе должна отобразиться форма с сгенерированной картинкой, и при нажатии на «?» капча должна меняться.
Осталось обработать отправку сообщений и создание экземпляра класса Message
Для того, чтобы отправлять сообщения конкретным пользователям я делаю так: в админке создаю группу с названием message, и добавляю нужных пользователей в эту группу. Тогда отправка сообщений пользователям из этой группы будет выглядеть так:
for u in users:
if u.groups.filter(name='message').exists() and u.email:
На этом все.
Пример реализованной капчи можно посмотреть на сайте СибСтрой-К
Протестировано на:
pip freeze
Django==1.11.16 numpy==1.16.5 Pillow==5.3.0
При возникновении вопросов — пишите на почту it@omoroot.ru


