django的登录界面默认只有用户名和密码输入框,没有额外的安全防护,如果在生产环境中不加登录验证码直接使用,是非常危险的,因为攻击者可以用特定程序不断的进行登录尝试,直至得出正确的登录密码,所以必须加上登录验证码,提高网站攻击成本。
使用Pycharm创建Django项目,项目结构如下:
创建完成后记得在manage.py所在目录运行:
python manage.py migrate
首先下载verify.js前端登录验证插件:
jquery验证码插件verify.js_jQuery之家-自由分享jQuery、html5、css3的插件库
解压得到以下目录结构:
在templates(manage.py同级目录下)创建admin目录:
使用以下命令查看django包位置:
python -c "import django; print(django.__path__)
然后在这个目录下找到contrib\admin\templates\admin\login.html,将其复制到templates/admin下。
首先在manage.py同级目录下创建static/admin文件夹,用来保存与管理有关的全局静态文件,然后这个文件夹下创建三个目录:js、css和image。
在解压后的插件文件夹中找到verify.css,将其复制到刚刚创建的static/admin/css下。然后将插件文件夹js文件夹下的所有js文件复制到static/admin/js下。将两张示例图片复制到static/admin/image下:
修改setttings中的静态文件设置:
STATICFILES_DIRS = [
BASE_DIR / "static",
"static/admin"
]
这样插件可以通过下面的方式引入:
为了能灵活使用不同的验证码,这里使用Django的模板继承(参见模板继承),定义一个登录骨架模板,把验证码抽象为一个块,从而实现灵活替换。
打开之前复制的login.html,找到如图所示位置,添加一个verify_code块:
然后覆盖基础admin模板的extrahead块,引入Verify.js插件:
然后将这个模板的名字改为login_base.html,作为登录骨架模板:
在templates/admin目录下新建一个login.html模板,作为真正的页面登录模板:
在这个模板中覆盖我们之前定义的verify_code块:
login.html
{#继承基础登录模板#}
{% extends "admin/login_base.html" %}
{% load i18n static %}
{#覆盖基础登录模板中的验证码块verify_code#}
{% block verify_code %}
{% endblock %}
效果
发现界面样式有错乱的地方,修改打开verify.js,找到:
修改默认参数中的width为99%。 再找到
修改变量panelHtml为
var panelHtml = '';
打开verify.css,找到
将其修改为
.cerify-code-panel {
height: 100%;
overflow: hidden;
}
.verify-code-area {
width: 100%;
justify-items: stretch;
}
.varify-input-code {
width: 80%;
height: 25px;
}
.verify-change-code {
width: 20%;
color: #337AB7;
cursor: pointer;
margin-left: 10px;
text-align: center;
}
修改后效果
注意
调试网页时要先关闭网络缓存,否则刷新页面总是显示之前缓存的css文件,导致调试困难。chrome关闭网络缓存在调试窗口网络Tab下勾选禁用缓存, Edge也一样。调试完记得关闭,不然很费流量。
verify.js的普通验证码很清晰,容易被OCR识别,并且验证码的内容可以通过html标签读取,因此还是很不安全,所以最好将验证码改成使用canvas绘制:
const panelHtml = '' +
'' +
'' +
'换一张' +
'' +
'' +
'';
......
在Code类中添加randNum、randColor、drawbg、drawLine、drawCircle、drawExpression这几个方法:
//定义Code的方法
Code.prototype = {
init: function () {
......
},
//加载页面
loadDom: function () {
......
},
/**
* 产生一个随机数
* @param min 最小值
* @param max 最大值
* @returns {*}
*/
ranNum: function (min, max) {
return Math.random() * (max - min) + min;
},
/**
* 返回一个随机颜色 可设置颜色区间
* @param {number} min [颜色下限]
* @param {number} max [颜色上限]
* @return {string} [随机颜色]
*/
ranColor: function (min, max) {
const r = this.ranNum(min, max);
const g = this.ranNum(min, max);
const b = this.ranNum(min, max);
return `rgb(${r},${g},${b})`;
},
//绘制背景
drawBg: function (min, max) {
// 绘制canvas背景
this.ctx.fillStyle = this.ranColor(min, max);
// 填充颜色
this.ctx.fillRect(0, 0, this.ctxW, this.ctxH);
},
/**
* 绘制干扰 圆点
* @param {number} num [绘制的数量]
* @param {number} r [圆点半径]
* @param {number} min [下限]
* @param {number} max [上线]
*/
drawCircle: function (num, r, min, max) {
for (let i = 0; i < num; i++) {
// 开始绘制 (拿起笔)
this.ctx.beginPath();
// context.arc(x,y,r,sAngle,eAngle,counterclockwise); (绘制)
// x 圆的中心的 x 坐标。
// y 圆的中心的 y 坐标。
// r 圆的半径。
// sAngle 起始角,以弧度计。(弧的圆形的三点钟位置是 0 度)。
// eAngle 结束角,以弧度计。
// counterclockwise 可选。规定应该逆时针还是顺时针绘图。False = 顺时针,true = 逆时针。
this.ctx.arc(this.ranNum(0, this.ctxW), this.ranNum(0, this.ctxH), r, 0, 2 * Math.PI);
// 填充颜色
this.ctx.fillStyle = this.ranColor(min, max);
// 填充
this.ctx.fill();
// 闭合绘制 (放开笔)
this.ctx.closePath();
}
},
/**
* 绘制干扰 线段
* @param {number} num [绘制的数量]
* @param {number} min [下限]
* @param {number} max [上线]
*/
drawLine: function (num, min, max) {
for (let i = 0; i < num; i++) {
// 开始绘制 (拿起笔)
this.ctx.beginPath();
// 绘制开始点
this.ctx.moveTo(this.ranNum(0, this.ctxW), this.ranNum(0, this.ctxH));
// 绘制结束点
this.ctx.lineTo(this.ranNum(0, this.ctxW), this.ranNum(0, this.ctxH));
this.ctx.strokeStyle = this.ranColor(min, max);
this.ctx.stroke();
this.ctx.closePath();
}
},
//绘制算数表达式
drawExpression: function (expression) {
const fs = this.randNum(20, 50);
this.ctx.font = fs + "px Verdana";
this.ctx.fillStyle = this.randColor(0, 100);
// x 添加到水平坐标(x)上的值
// y 添加到垂直坐标(y)上的值
// 偏移
for (let i = 0; i < expression.length; i++) {
const fs = this.randNum(20, 50);
this.ctx.font = fs + "px Verdana";
this.ctx.fillStyle = this.randColor(0, 100);
// 保存绘制的状态
this.ctx.save();
// x 添加到水平坐标(x)上的值
// y 添加到垂直坐标(y)上的值
// 偏移
this.ctx.translate(this.ctxW / expression.length * i + this.ctxW / 20, 0);
// 变换角度
this.ctx.rotate(this.randNum(-30, 30,) * Math.PI / 180);
// text 规定在画布上输出的文本。
// x 开始绘制文本的 x 坐标位置(相对于画布)。
// y 开始绘制文本的 y 坐标位置(相对于画布)。
// maxWidth 可选。允许的最大文本宽度,以像素计。
this.ctx.fillText(expression[i], 0, (this.ctxH + fs) / 2.5, this.ctxW / expression.length);
// 返回之前保存过的路径状态和属性
this.ctx.restore();
}
//设置验证码
setCode: function () {
.......
},
......
};
//设置验证码
setCode: function () {
// 清空canvas
this.ctx.clearRect(0, 0, this.ctxW, this.ctxH);
//绘制背景
this.drawBg(200, 255);
//绘制干扰线条
this.drawLine(20, 0, 255);
//绘制干扰圆点
this.drawCircle(20, 5, 200, 255);
const color1Num = Math.floor(Math.random() * 3);
const color2Num = Math.floor(Math.random() * 5);
this.htmlDoms.code.css({'background-color': _code_color1[color1Num], 'color': _code_color2[color2Num]});
this.htmlDoms.code_input.val('');
this.code_chose = '';
if (this.options.type === 1) {
//添加普通验证码字符
for (let i = 0; i < this.options.codeLength; i++) {
//随机选中一个字符
const charNum = Math.floor(Math.random() * 52);
let char = _code_chars[charNum]
const fs = this.randNum(20, 50);
this.ctx.font = fs + "px Verdana";
this.ctx.fillStyle = this.randColor(0, 100);
// 保存绘制的状态
this.ctx.save();
// x 添加到水平坐标(x)上的值
// y 添加到垂直坐标(y)上的值
// 偏移
this.ctx.translate(this.ctxW / this.options.codeLength * i + this.ctxW / 20, 0);
// 变换角度
this.ctx.rotate(this.randNum(-30, 30,) * Math.PI / 180);
// text 规定在画布上输出的文本。
// x 开始绘制文本的 x 坐标位置(相对于画布)。
// y 开始绘制文本的 y 坐标位置(相对于画布)。
// maxWidth 可选。允许的最大文本宽度,以像素计。
this.ctx.fillText(char, 0, (this.ctxH + fs) / 2.5, this.ctxW / this.options.codeLength);
// 返回之前保存过的路径状态和属性
this.ctx.restore();
//添加到选定的验证码中
this.code_chose += _code_chars[charNum];
}
} else { //算法验证码
let num1 = Math.floor(Math.random() * this.options.figure);
let num2 = Math.floor(Math.random() * this.options.figure);
//随机选择一种算数
if (this.options.arith === 0) {
var tmparith = Math.floor(Math.random() * 3);
}
//要绘制的序列
let code = []
switch (tmparith) {
case 1 :
//加法
this.code_chose = parseInt(String(num1)) + parseInt(String(num2));
code.push(String(num1))
code.push("+")
code.push(String(num2))
code.push("=")
code.push('?')
this.drawExpression(code)
break;
case 2 :
//减法, 确保减法不出现负数
if (parseInt(String(num1)) < parseInt(String(num2))) {
var tmpnum = num1;
num1 = num2;
num2 = tmpnum;
}
this.code_chose = parseInt(String(num1)) - parseInt(String(num2));
code.push(String(num1))
code.push("-")
code.push(String(num2))
code.push("=")
code.push('?')
this.drawExpression(code)
break;
default :
//乘法
this.code_chose = parseInt(String(num1)) * parseInt(String(num2));
code.push(String(num1))
code.push("×")
code.push(String(num2))
code.push("=")
code.push('?')
this.drawExpression(code)
break;
}
}
},
/*常规验证码*/
.verify-code {
text-align: center;
cursor: pointer;
border: 1px solid #ddd;
}
.verify-code-panel {
height: 100%;
overflow: hidden;
}
.verify-code-area {
width: 100%;
display: flex;
justify-items: stretch;
justify-content: flex-start;
align-items: center;
flex-direction: row;
margin-bottom: 5px;
}
.varify-input-code {
width: 100%;
height: 25px;
}
.verify-change-code {
width: 20%;
color: #337AB7;
cursor: pointer;
margin-left: 10px;
text-align: center;
}
......