# 用VFP开发BS模式验证码的两个模式,让你网站更安全

网站上的验证码一般放在登录或注册页面。它的作用是保护网站安全,一般网站都要通过验证码来防止机器大规模注册,机器暴力破解数据密码等危害。

虽然有了验证码的存在,登录变得麻烦一点,但是对于网站安全来说,这个功能还是很有必要的。

我们利用VFP祺佑框架开发BS网站,也会遇上这样的问题,所以为了网站安全,也需要为我们开发的网页上加上验证码。

库和工具说明: 前端:vue+axios​ 后端:VFP祺佑三层开发框架

在网站上增加验证码,有两种方式:前端生成前端判断;后端生成后端判断。接下来我们详细阐述一下。

# 第一种:在页面前端生成验证码,在提交数据前时行判断。

在提交表单时,为了防止自动程序提交,一般提供有验证码,在Form的submit前使用来提前检测验证码是否正确。这种方式如果输入的验证码和预先生成的验证码一致,则允许提交数据,否则不允许提交数据。在一定程序上避免了用户的恶意提交数据而增加服务器负担。

实现步骤:页面加载时,通过前端JS加载验证码图片。以下是具体实现核心代码。

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="./js/vue.min.js"></script>
<script src="./js/axios.js"></script>
<script src="./js/jsyzm.js"></script>
<style type="text/css">
body,html {width: 100%;text-align: center;}
#picyzm {width: 100px;height: 40px;display: inline;line-height: 40px;margin: 0 0 0 0px;}
#code_input {width: 120px;height: 40px;display: inline;line-height: 40px;margin: 0 0 0 0px;font-size:24px;}
#btn {margin: 0px;background-color: blue;color: #fff;border-radius: 5px;border: 0;width: 100px;height: 40px;dispaly:inline;}
img{margin:0px;padding:0px;height: 40px;display: inline;line-height: 40px;cursor:hand;}
</style>
</head>
<body>
<!—此处省略了 form 里面 用户名 及密码 部分代码 -->
	<div id="app">
		<input type="text" id="code_input"  v-model:value="cYZM" placeholder="在这个框里输入验证码">
		<div id="picyzm"   ></div>
	    <input type="button" value="验证" id="btn"  v-on:click="toCX">	</div>
<script>		
	var vm = new Vue({
	    el: "#app",
	    data: {
	     cYZM: "",
	     verifyCode:""
	    },
	    mounted:function(){
	     //初始化验证码
	     this.verifyCode = new GVerify({
	      id : "picyzm",
	      type : "blend"
	     });
	    },
	    methods: {
	     toCX: function() {
	      var res = this.verifyCode.validate(this.cYZM);
	      if (!res){  alert('验证码错误')      }
		  else {alert(' 验证通过 ')}
		},
		}
	})	
	</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

2 生成验证码的JS代码库(JSYZM.js)不依赖JQuery

注意options对象的type属性可接收的类型(图形验证码默认类型blend:数字字母混合类型、number:纯数字、letter:纯字母)

!(function(window, document) {
    function GVerify(options) { // 创建一个图形验证码对象,接收options对象为参数
        this.options = { // 默认options参数值
            id: "", // 容器Id
            canvasId: "verifyCanvas", // canvas的ID
            width: "100", // 默认canvas宽度
            height: "30", // 默认canvas高度
            type: "blend", // 图形验证码默认类型blend:数字字母混合类型、number:纯数字、letter:纯字母
            code: ""
        }
        
        if(Object.prototype.toString.call(options) == "[object Object]"){// 判断传入参数类型
            for(var i in options) { // 根据传入的参数,修改默认参数值
                this.options[i] = options[i];
            }
        }else{
            this.options.id = options;
        }
        
        this.options.numArr = "0,1,2,3,4,5,6,7,8,9".split(",");
        this.options.letterArr = getAllLetter();

        this._init();
        this.refresh();
    }

    GVerify.prototype = {
        /** 版本号* */
        version: '1.0.0',
        
        /** 初始化方法* */
        _init: function() {
            var con = document.getElementById(this.options.id);
            var canvas = document.createElement("canvas");
            this.options.width = con.offsetWidth > 0 ? con.offsetWidth : "100";
            this.options.height = con.offsetHeight > 0 ? con.offsetHeight : "30";
            canvas.id = this.options.canvasId;
            canvas.width = this.options.width;
            canvas.height = this.options.height;
            canvas.style.cursor = "pointer";
            canvas.innerHTML = "您的浏览器版本不支持canvas";
            con.appendChild(canvas);
            var parent = this;
            canvas.onclick = function(){
                parent.refresh();
            }
        },
        
        /** 生成验证码* */
        refresh: function() {
            this.options.code = "";
            var canvas = document.getElementById(this.options.canvasId);
            if(canvas.getContext) {
                var ctx = canvas.getContext('2d');
            }else{
                return;
            }
            
            ctx.textBaseline = "middle";

            ctx.fillStyle = randomColor(180, 240);
            ctx.fillRect(0, 0, this.options.width, this.options.height);

            if(this.options.type == "blend") { // 判断验证码类型
                var txtArr = this.options.numArr.concat(this.options.letterArr);
            } else if(this.options.type == "number") {
                var txtArr = this.options.numArr;
            } else {
                var txtArr = this.options.letterArr;
            }

            for(var i = 1; i <= 4; i++) {
                var txt = txtArr[randomNum(0, txtArr.length)];
                this.options.code += txt;
                ctx.font = randomNum(this.options.height/2, this.options.height) + 'px SimHei'; // 随机生成字体大小
                ctx.fillStyle = randomColor(50, 160); // 随机生成字体颜色
                ctx.shadowOffsetX = randomNum(-3, 3);
                ctx.shadowOffsetY = randomNum(-3, 3);
                ctx.shadowBlur = randomNum(-3, 3);
                ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
                var x = this.options.width / 5 * i;
                var y = this.options.height / 2;
                var deg = randomNum(-30, 30);
                /** 设置旋转角度和坐标原点* */
                ctx.translate(x, y);
                ctx.rotate(deg * Math.PI / 180);
                ctx.fillText(txt, 0, 0);
                /** 恢复旋转角度和坐标原点* */
                ctx.rotate(-deg * Math.PI / 180);
                ctx.translate(-x, -y);
            }
            /** 绘制干扰线* */
            for(var i = 0; i < 4; i++) {
                ctx.strokeStyle = randomColor(40, 180);
                ctx.beginPath();
                ctx.moveTo(randomNum(0, this.options.width), randomNum(0, this.options.height));
                ctx.lineTo(randomNum(0, this.options.width), randomNum(0, this.options.height));
                ctx.stroke();
            }
            /** 绘制干扰点* */
            for(var i = 0; i < this.options.width/4; i++) {
                ctx.fillStyle = randomColor(0, 255);
                ctx.beginPath();
                ctx.arc(randomNum(0, this.options.width), randomNum(0, this.options.height), 1, 0, 2 * Math.PI);
                ctx.fill();
            }
        },
        
        /** 验证验证码* */
        validate: function(code){
            var code = code.toLowerCase();
            var v_code = this.options.code.toLowerCase();
            if(code == v_code){
                return true;
            }else{
                return false;
            }
        }
    }
    /** 生成字母数组* */
    function getAllLetter() {
        var letterStr = "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z";
        return letterStr.split(",");
    }
    /** 生成一个随机数* */
    function randomNum(min, max) {
        return Math.floor(Math.random() * (max - min) + min);
    }
    /** 生成一个随机色* */
    function randomColor(min, max) {
        var r = randomNum(min, max);
        var g = randomNum(min, max);
        var b = randomNum(min, max);
        return "rgb(" + r + "," + g + "," + b + ")";
    }
    window.GVerify = GVerify;
})(window, document);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138

此种验证码,在前端生成,前端判断,简洁明了,但是有弊端。如果绕开了登录页面,直接发起登录探测,此时验证码属于无效状态,服务器仍有被恶意攻击的可能。

# 第二种:在后端生成验证码,前端显示,提交数据时后端断验证码。

实现步骤:前端页渲染,调用后台生成的验证码图片,在前端显示,提交到到后台进行判断。注意这个验证码有个判断时效的问题。

代码如下 1网页文件(login4.html)

<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
		<script src="./js/vue.min.js"></script>
		<script src="./js/axios.js"></script>
		<style type="text/css">
			body,html {width: 100%;text-align: center;}
			#txt {display: inline;			}
			#picyzm {width: 100px;height: 40px;display: inline;line-height: 40px;margin: 0 0 0 0px;}
			#code_input {width: 120px;height: 40px;display: inline;line-height: 40px;margin: 0 0 0 0px;font-size:24px;}
			#btn {margin: 0px;background-color: blue;color: #fff;border-radius: 5px;border: 0;width: 100px;height: 40px;dispaly:inline;}
			img{margin:0px;padding:0px;height: 40px;display: inline;line-height: 40px;cursor:hand;}
		</style>
	</head>
	<body>
		<div id="app"><div id="txt">请输入验证码:</div>
			<input type="text" id="code_input" v-model="cYZM" placeholder='' titlw="输入验证码">
			<img id="picyzm" v-bind:src="imgsrc" @click='changebmp' title='单击更换验证码'>
			<input type="button" value="验证" id="btn" @click="toCX">
		</div>
	</body>
</html>
<script>
	var vm = new Vue({
		el: "#app",
		data: {
			yoururl:'http://192.168.0.105:801/',
			imgsrc: '',
			cYZM: '',
			verifyCode:'',
			imgcs: {
				// yzmstr: '',
				// yzmwidth: 4,
				// yzmheight: 1.2,
				// yzmfont: '',
				// yzmpath: ''
			},
		},
		mounted: function() {
              this.changebmp()
		},
		methods: {
			toCX: function() {
				if(this.cYZM==null|| this.cYZM==''||this.cYZM==undefined){
					alert(null)
					return
					}
				var that=this	 
                axios.get(that.$data.yoururl+'makeyzm.fsp?proc=checkyzm',{params:{key:that.$data.cYZM}})
				     .then(function(res){		alert(res.data) 				 })
					 .catch(function(res){		alert(res.data) 		 })
			},
		   changebmp:function(){
			var that = this;
			axios.get(that.$data.yoururl+'makeyzm.fsp',{params:{}})
				.then(function(res) {that.$data.imgsrc = res.data	})
				.catch(function(res) {console.log('error:')
					console.log(res)
				})
		   }
		}
	})
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

2 VFP后端处理文件(makeyzm.prg) 里面使用了一个lblAutoSize函数,转换文字为字符。

DEFINE CLASS makeyzm as session

***********以下one two 操作在网站要目录下创建目录yzmimg
*********start of one *********
protected subpath
          subpath="yzmimg" 
*********end of one *********** 
       
*********start of two *********          
PROCEDURE init
   LOCAL sp
   sp=getwwwrootpath(this.subpath)
   IF NOT DIRECTORY(sp) THEN
      mkdir("&sp") 
   ENDIF 
ENDPROC 
*********end of two ***********

***********若网站根目录下已有目录yzmimg 可删除 two部分

PROCEDURE ondefault
  RETURN this.lblAutoSize()
ENDPROC 


***保存验证码
PROCEDURE saveyzm
  PARAMETERS lyzm
  SELECT 0
  USE zyzm
  APPEND BLANK 
  REPLACE cyzm WITH lyzm
  REPLACE yzmdt WITH DATETIME()
  USE IN SELECT('zyzm')  
ENDPROC 


***校验验证码
PROCEDURE checkyzm
  PRIVATE yzmtext,result
  yzmtext=httpqueryparams("key")
  ?"++++++++++++++++++++",yzmtext
  
  SELECT 0
  USE zyzm
  LOCATE FOR ALLTRIM(cyzm)==ALLTRIM(yzmtext) AND (DATETIME()-yzmdt)/60<5 
  ?"------------",cyzm 
  IF FOUND()
     result='验证码输入正确'
  ELSE
     result='验证期超期或验证码输入不正确,请点击验证码刷新'   
  ENDIF 
  USE IN SELECT('zyzm')
  RETURN result
ENDPROC 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

需要源码的,请关注“加菲猫的VFP”公众号,输入YZM即可获取下载链接。