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.py
MESSAGE_URL_START
— значение задается в./project/settings.py
MESSAGE_URL_END
— значение задается в./project/settings.py
EMAIL_USER
— значение задается в./project/settings.py
EMAIL_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