为 Django admin 登录页添加验证码

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

 

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

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

配置url:


from django.contrib import admin
from adm.views.custom_admin import custom_login

admin.autodiscover()
admin.site.login = custom_login

urlpatterns += (
    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_login
from django.contrib.sites.models import get_current_site
from django.http import HttpResponseRedirect, HttpResponse
from django.template.response import TemplateResponse
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.utils.encoding import force_text

from adm.forms import SmscodeAdminAuthenticationForm, MultiCaptchaAdminAuthenticationForm
from settings import  ADMIN_ENABLE_SMS_LOGIN, ADMIN_PHONES, ADMIN_SHOW_DELETE_SELECTED

if ADMIN_ENABLE_SMS_LOGIN:
    admin.AdminSite.login_form = SmscodeAdminAuthenticationForm
    AuthForm = SmscodeAdminAuthenticationForm
else:
    admin.AdminSite.login_form = MultiCaptchaAdminAuthenticationForm
    AuthForm = MultiCaptchaAdminAuthenticationForm

# 根据设置禁用批量删除
if not ADMIN_SHOW_DELETE_SELECTED:
    admin.site.disable_action('delete_selected')


@sensitive_post_parameters()
@csrf_protect
@never_cache
def 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-8

from django.contrib.admin.forms import AdminAuthenticationForm

from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse, NoReverseMatch
from django import forms
from django.forms import ValidationError
from django.forms.fields import CharField, MultiValueField
from django.forms.widgets import TextInput, MultiWidget, HiddenInput
from django.utils.translation import ugettext, ugettext_lazy
from six import u
import base64
from django.core.cache import cache
from settings import ADMIN_PHONES

# utils为自己实现的库,包括生成captcha,发送短信等
from utils.captcha_util import get_captcha_by_text, get_code_text, compare_code_ignore_case
from utils.sms_util import verify_sms_code

CAPTCHA_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 CaptchaStore
        This has to be called inside render
        """
        #key = CaptchaStore.generate_key()
        key = self.session_key  # 用session_key关联captcha
        set_captcha(key)

        # these can be used by format_output and render
        self._value = [key, u('')]
        self._key = key
        self.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 = kwargs
        self._args['output_format'] = self._args.get('output_format') or CAPTCHA_OUTPUT_FORMAT
        self._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 ret

    def 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 ret

    def format_output(self, rendered_widgets):
        hidden_field, text_field = rendered_widgets
        text_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-blog.csdnimg.cn/2022010623403229180.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_key
        fields = (
            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 None


    def 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 value


class MultiCaptchaAdminAuthenticationForm(AdminAuthenticationForm):
    def __init__(self, request=None, *args, **kwargs):
        if not request.session.session_key:
            request.session.create()
        session_key = request.session.session_key
        super(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 = cellphone
        self.required = True
        super(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;
}

 

最后效果如下

 

转载于:https://my.oschina.net/sukai/blog/1604584

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值