700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > 为 Django admin 登录页添加验证码

为 Django admin 登录页添加验证码

时间:2022-08-09 07:16:12

相关推荐

为 Django admin 登录页添加验证码

为什么80%的码农都做不了架构师?>>>

历史原因,使用上古版本 django 1.6.5,但新版本应该大同小异

首先添加自定义后台模块app, 如adm,并添加到 INSTALLED_APPS 下。

假设处理自定义登录的view是 apps/adm/views/custom_admin.py 中的 custom_login 函数

配置url:

from django.contrib import adminfrom adm.views.custom_admin import custom_loginadmin.autodiscover()admin.site.login = custom_loginurlpatterns += (url(r'^custom-admin/', include(admin.site.urls)),url(r'^custom-admin/', include('adm.urls')),)

登录页中有两种形式,一种是本地和测试环境, 输入用户名加密码加captch三项,然后登录,此处的captcha都是修改的django的表单,不少代码是搬运的django-simple-captcha,因为这个库使用的话还需要添加它的model进行migrate,所以没直接使用;另一种是线上环境,需要输入用户名(手机号)加密码加短信验证码三项,并且短信验证码的需要点击获取,正确输入弹出的captcha并且用户名在setting中的白名单才可以获取,此处的captcha是后台的api获取,而非表单。

view如下(需要自己实现生成captcha图片和发送短信的api):

from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_loginfrom django.contrib.sites.models import get_current_sitefrom django.http import HttpResponseRedirect, HttpResponsefrom django.template.response import TemplateResponsefrom django.views.decorators.cache import never_cachefrom django.views.decorators.csrf import csrf_protectfrom django.views.decorators.debug import sensitive_post_parametersfrom django.utils.encoding import force_textfrom adm.forms import SmscodeAdminAuthenticationForm, MultiCaptchaAdminAuthenticationFormfrom settings import ADMIN_ENABLE_SMS_LOGIN, ADMIN_PHONES, ADMIN_SHOW_DELETE_SELECTEDif ADMIN_ENABLE_SMS_LOGIN:admin.AdminSite.login_form = SmscodeAdminAuthenticationFormAuthForm = SmscodeAdminAuthenticationFormelse:admin.AdminSite.login_form = MultiCaptchaAdminAuthenticationFormAuthForm = MultiCaptchaAdminAuthenticationForm# 根据设置禁用批量删除if not ADMIN_SHOW_DELETE_SELECTED:admin.site.disable_action('delete_selected')@sensitive_post_parameters()@csrf_protect@never_cachedef custom_login(request, template_name='custom_login.html',redirect_field_name=REDIRECT_FIELD_NAME,authentication_form=AuthForm,current_app=None, extra_context=None):redirect_to = request.REQUEST.get(redirect_field_name, '')if request.method == "POST":ua = request.META['HTTP_USER_AGENT'].strip()ip = request.META['REMOTE_ADDR']username = request.POST.get('username')logtrade.info('try login. username:%s' % username)if username not in ADMIN_PHONES:password = request.POST.get('password')logging.error(u'登录失败! 用户名不在白名单:%s, input password:%s, IP:%s, UserAgent:%s' % (username, password, ip, ua))form = authentication_form(request, data=request.POST)if form.is_valid():auth_login(request, form.get_user())return HttpResponseRedirect(redirect_to)else:form = authentication_form(request)current_site = get_current_site(request)context = {'form': form,redirect_field_name: redirect_to,'site': current_site,'site_name': current_site.name,'sms_login': ADMIN_ENABLE_SMS_LOGIN,}if extra_context is not None:context.update(extra_context)return TemplateResponse(request, template_name, context,current_app=current_app)

其中两个表单的实现如下:

大部分功能使用了django-simple-captcha的代码,主要的改动是把captcha文本存在缓存里,使用session_key关联,取消了数据库的使用

# encoding=utf-8from django.contrib.admin.forms import AdminAuthenticationFormfrom django.core.exceptions import ImproperlyConfiguredfrom django.core.urlresolvers import reverse, NoReverseMatchfrom django import formsfrom django.forms import ValidationErrorfrom django.forms.fields import CharField, MultiValueFieldfrom django.forms.widgets import TextInput, MultiWidget, HiddenInputfrom django.utils.translation import ugettext, ugettext_lazyfrom six import uimport base64from django.core.cache import cachefrom settings import ADMIN_PHONES# utils为自己实现的库,包括生成captcha,发送短信等from utils.captcha_util import get_captcha_by_text, get_code_text, compare_code_ignore_casefrom utils.sms_util import verify_sms_codeCAPTCHA_OUTPUT_FORMAT = u'%(image)s %(hidden_field)s %(text_field)s'def get_captcha(key):return cache.get('captcha_' + key)def set_captcha(key):captcha_text = get_code_text() # 获取captcha文本cache.set('captcha_' + key, captcha_text, 30 * 60)def delete_captcha(key):cache.delete('captcha_' + key)class BaseCaptchaTextInput(MultiWidget):"""Base class for Captcha widgets"""def __init__(self, attrs=None):self.session_key = attrs.get('session_key')widgets = (HiddenInput(attrs),TextInput(attrs),)super(BaseCaptchaTextInput, self).__init__(widgets, attrs)def decompress(self, value):if value:return value.split(',')return [None, None]def fetch_captcha_store(self, name, value, attrs=None):"""Fetches a new CaptchaStoreThis has to be called inside render"""#key = CaptchaStore.generate_key()key = self.session_key # 用session_key关联captchaset_captcha(key)# these can be used by format_output and renderself._value = [key, u('')]self._key = keyself.id_ = self.build_attrs(attrs).get('id', None)def id_for_label(self, id_):if id_:return id_ + '_1'return id_def image_url(self):# 返回的不是url而是图片的base64编码text = get_captcha(self.session_key)captcha_content = get_captcha_by_text(text)return base64.b64encode(captcha_content)def refresh_url(self):return reverse('captcha-refresh')class CaptchaTextInput(BaseCaptchaTextInput):def __init__(self, attrs=None, **kwargs):self._args = kwargsself._args['output_format'] = self._args.get('output_format') or CAPTCHA_OUTPUT_FORMATself._args['id_prefix'] = self._args.get('id_prefix')for key in ('image', 'hidden_field', 'text_field'):if '%%(%s)s' % key not in self._args['output_format']:raise ImproperlyConfigured('All of %s must be present in your CAPTCHA_OUTPUT_FORMAT setting. Could not find %s' % (', '.join(['%%(%s)s' % k for k in ('image', 'hidden_field', 'text_field')]),'%%(%s)s' % key))super(CaptchaTextInput, self).__init__(attrs)def build_attrs(self, extra_attrs=None, **kwargs):ret = super(CaptchaTextInput, self).build_attrs(extra_attrs, **kwargs)if self._args.get('id_prefix') and 'id' in ret:ret['id'] = '%s_%s' % (self._args.get('id_prefix'), ret['id'])return retdef id_for_label(self, id_):ret = super(CaptchaTextInput, self).id_for_label(id_)if self._args.get('id_prefix') and 'id' in ret:ret = '%s_%s' % (self._args.get('id_prefix'), ret)return retdef format_output(self, rendered_widgets):hidden_field, text_field = rendered_widgetstext_field = text_field.replace('<input', '<input autocomplete="off"')return self._args['output_format'] % {'image': self.image,'hidden_field': hidden_field,'text_field': text_field}def render(self, name, value, attrs=None):self.fetch_captcha_store(name, value, attrs)self.image = '<img src="https://img-/202623403229180.png" alt="captcha" class="captcha" />' % self.image_url()return super(CaptchaTextInput, self).render(name, self._value, attrs=attrs)class CaptchaField(MultiValueField):def __init__(self, session_key=None, *args, **kwargs):self.session_key = session_keyfields = (CharField(show_hidden_initial=True),CharField(),)if 'error_messages' not in kwargs or 'invalid' not in kwargs.get('error_messages'):if 'error_messages' not in kwargs:kwargs['error_messages'] = {}kwargs['error_messages'].update({'invalid': ugettext_lazy('Invalid CAPTCHA')})attrs = {'session_key': session_key}kwargs['widget'] = kwargs.pop('widget', CaptchaTextInput(attrs=attrs,output_format=kwargs.pop('output_format', None),id_prefix=kwargs.pop('id_prefix', None)))super(CaptchaField, self).__init__(fields, *args, **kwargs)def compress(self, data_list):if data_list:return ','.join(data_list)return Nonedef clean(self, value):super(CaptchaField, self).clean(value)response, value[1] = (value[1] or '').strip().lower(), ''correct_captcha = cache.get('captcha_' + self.session_key)if not compare_code_ignore_case(response, correct_captcha):raise ValidationError(getattr(self, 'error_messages', {}).get('invalid', ugettext_lazy('Invalid CAPTCHA')))delete_captcha(self.session_key)return valueclass MultiCaptchaAdminAuthenticationForm(AdminAuthenticationForm):def __init__(self, request=None, *args, **kwargs):if not request.session.session_key:request.session.create()session_key = request.session.session_keysuper(MultiCaptchaAdminAuthenticationForm, self).__init__(*args, **kwargs)self.fields['captcha'] = CaptchaField(session_key=session_key)class SmscodeField(CharField):"""短信验证码字段"""def __init__(self, cellphone=None, *args, **kwargs):self.cellphone = cellphoneself.required = Truesuper(SmscodeField, self).__init__(*args, **kwargs)def clean(self, value):super(SmscodeField, self).clean(value)# 登录用户名必须是setting白名单中设置的手机号if self.cellphone not in ADMIN_PHONES:params = {'username': u"用户名"}raise forms.ValidationError(u'用户名或密码有误', code='invalid', params=params)if not verify_sms_code(cellphone=self.cellphone, sms_code=value):params = {'smscode': u'短信验证码'}raise forms.ValidationError(u'短信验证码有误', code='invalid', params=params)class SmscodeAdminAuthenticationForm(AdminAuthenticationForm):def __init__(self, request=None, *args, **kwargs):if not request.session.session_key:request.session.create()super(SmscodeAdminAuthenticationForm, self).__init__(*args, **kwargs)self.fields['smscode'] = SmscodeField(cellphone=self.data.get('username'))

登录页的模板 custom_login.html, 找前端同学写的js和样式:

{% extends "admin/base_site.html" %}{% load i18n admin_static %}{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/login.css" %}" /><link rel="stylesheet" type="text/css" href="{% static "css/custom_form.css" %}" />{{ block.super }}{% endblock %}{% block bodyclass %}login{% endblock %}{% block nav-global %}{% endblock %}{% block content_title %}{% endblock %}{% block breadcrumbs %}{% endblock %}{% block content %}{% if form.errors and not form.non_field_errors and not form.this_is_the_login_form.errors %}<p class="errornote">{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}</p>{% endif %}{% if form.non_field_errors or form.this_is_the_login_form.errors %}{% for error in form.non_field_errors|add:form.this_is_the_login_form.errors %}<p class="errornote">{{ error }}</p>{% endfor %}{% endif %}<div id="content-main"><form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}<div class="form-row">{% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %}<label for="id_username" class="required">{{ form.username.label }}:</label> {{ form.username }}</div><div class="form-row">{% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %}<label for="id_password" class="required">{% trans 'Password:' %}</label> {{ form.password }}<input type="hidden" name="this_is_the_login_form" value="1" /><input type="hidden" name="next" value="{{ next }}" /></div>{% if sms_login %}<div class="form-row">{% if not form.this_is_the_login_form.errors %}{{ form.smscode.errors }}{% endif %}<label for="smscode" class="required">短信验证码: </label><a href="javascript:void(0);" class="getSmscode" id="getSmscode" data-click="0" onclick="getMsgCode()">点击获取短信验证码</a><div><th width="920">{{ form.smscode }}</th></div></div>{% else %}<div class="form-row">{% if not form.this_is_the_login_form.errors %}{{ form.captcha.errors }}{% endif %}{{ form.captcha }}</div>{% endif %}<div class="submit-row"><label>&nbsp;</label><input type="submit" value="{% trans 'Log in' %}" /></div></form><div class="captcha-cover"><div class="form-row"><div class="captchaImg"><img id="captchaImg" src="" alt="" onclick="getCaptcha()"><span onclick="getCaptcha()">点击更换图片</span></div><div class="text-box"><input placeholder="请输入上图中的验证码" id="captchaText" value=""></div><div class="control-btn"><a href="javascript:void 0;" onclick="hideModal()">取消</a><div class="line"></div><a href="javascript:void 0;" class="current" onclick="getSmscode()">确定</a></div></div></div><script type="text/javascript" src="{% static "admin/js/jquery.min.js" %}"></script><script type="text/javascript">{% if sms_login %}// 获取图片验证码function getCaptcha() {var captchaUrl = "/custom-admin/captcha";$.ajax({type: "GET",url: captchaUrl,dataType: "json",success: function (data) {if (data.ret) {document.getElementById("captchaImg").src = "data:image/png;base64," + data.captcha;} else {console.log(data.msg);}}})}//弹窗的显示和隐藏function getMsgCode(){$(".captcha-cover").css("display","block");getCaptcha();}function hideModal(){$(".captcha-cover").css("display","none");}$("#captchaText").on("keypress",function(event){if(event.keyCode===13){getSmscode();}})// 获取短信function getSmscode(){var smscodeUrl = "/custom-admin/smscode";var captchaText = document.getElementById("captchaText").value;var cellphone = document.getElementById("id_username").value;var data = {'captcha': captchaText,'cellphone': cellphone,};$.ajax({type: "POST",url: smscodeUrl,dataType: "json",data: data,success: function (data) {if (data.ret) {hideModal();} else {alert(data.msg);getCaptcha();}}})}{% endif %}</script></div>{% endblock %}

样式:

.login .form-row #id_smscode {clear: both;padding: 6px;width: 100%;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;}.captcha-cover{display: none;position:fixed;left:0;top:0;width:100%;height:1000px;background-color:rgba(0,0,0,0.5);text-align:center;}.captcha-cover .form-row{padding-top:40px;display: inline-block;float: inherit!important;width:320px;margin:150px auto;background-color:#fff;padding-bottom:0;}.captchaImg{padding:0 30px;display: inline-block;margin-bottom:10px;}#captchaImg{float:left;display:inline-block;width: 100%;padding:6px 2px;border:1px solid #000;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;vertical-align:middle;}#captchaText{display:inline-block;vertical-align:middle;width:100%;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;}.captchaImg span{cursor:pointer;margin-left:20px;display: inline-block;line-height:40px;}#captchaImg{padding:2px 10px;width:100px;height:38px;}.text-box{padding:0 50px;}#captchaText{height:40px;box-sizing: border-box;}.control-btn{position:relative;margin-top:20px;border-top:1px solid #ededed;}.control-btn a{display: inline-block;width:49%;text-align:center;box-sizing: border-box;font-size:14px;line-height:44px;color:#666;}.control-btn a.current{color:dodgerblue;}.line{position:absolute;top:-1px;left:50%;width:1px;height:45px;background-color:#ededed;}

最后效果如下

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。