1. 程式人生 > >滑動驗證碼的設計與理解

滑動驗證碼的設計與理解

在介紹之前,首先一個概念明確一個共識:沒有攻不破的網站,只有值不值得。

這意思是說,我們可以儘可能的提高自己網站的安全,但並沒有絕對的安全,當網站安全級別大於攻擊者能得到的回報時,你的網站就是安全的。

所以百度搜到的很多驗證碼都已經結合了人工智慧分析使用者行為,很厲害。但這裡只介紹我的小網站是怎麼設計的。

大概邏輯:當需要驗證碼時,前端傳送ajax向後臺請求相關資料傳送回前端,由前端生成(與後端生成圖片,然後傳送圖片到前端的做法相比安全性要差很多。但也是可以預防的,後端可以對此Session進行請求記錄,如果在一定時間內惡意多次請求,可以進行封禁ip等對策),驗證完成後,後臺再對傳回的資料進行校驗。

效果圖:

 


 

js類的設計:

1.定義一個驗證碼父類,因為目前只有這一個驗證型別,倘若以後再要擴充套件其他驗證型別呢。那麼它們之間肯定有很多公共之處(如:驗證成功、失敗的回撥,獲取驗證碼的型別,獲取驗證結果等),所以這些共同點可以提煉出來,下面是我目前的父類樣子:

 1 /**
 2  * 驗證碼的父類,所有驗證碼都要繼承這個類
 3  * @param id 驗證碼的唯一標識
 4  * @param type 驗證碼的型別
 5  * @param contentDiv 包含著驗證碼的DIV
 6  * @constructor
 7  */
 8 var Identifying = function (id,type,contentDiv){
 9     this.id = id;
10     this.type = type;
11     this.contentDiv=contentDiv;
12 }
13 
14 /**
15  * 銷燬函式
16  */
17 Identifying.prototype.destroy = function(){
18     this.successFunc = null;
19     this.errorFunc = null;
20     this.clearDom();
21     this.contentDiv = null;
22 }
23 
24 /**
25  * 清除節點內容
26  */
27 Identifying.prototype.clearDom = function(){
28     if(this.contentDiv instanceof jQuery){
29         this.contentDiv.empty();
30     }else if(this.contentDiv instanceof HTMLElement){
31         this.contentDiv.innerText = "";
32     }
33 }
34 
35 /**
36  * 回撥函式
37  * 驗證成功後進行呼叫
38  * this需要指具體驗證類
39  * @param result 物件,有對應驗證類的傳遞的引數,具體要看驗證類
40  */
41 Identifying.prototype.success = function (result) {
42     if(this.successFunc instanceof Function){
43         this.successFunc(result);
44     }
45 }
46 
47 /**
48  * 驗證失敗發生錯誤呼叫的函式
49  * @param result
50  */
51 Identifying.prototype.error = function (result) {
52     if(this.errorFunc instanceof Function){
53         this.errorFunc(result);
54     }else{
55         //統一處理錯誤
56     }
57 }
58 
59 /**
60  * 獲取驗證碼id
61  */
62 Identifying.prototype.getId = function () {
63     return this.id;
64 }
65 
66 /**
67  * 獲取驗證碼型別
68  * @returns {*}
69  */
70 Identifying.prototype.getType = function () {
71     return this.type;
72 }
73 
74 /**
75  *  顯示驗證框
76  */
77 Identifying.prototype.showIdentifying = function(callback){
78     this.contentDiv.show(null,callback);
79 }
80 
81 /**
82  * 隱藏驗證框
83  */
84 Identifying.prototype.hiddenIdentifying = function(callback){
85     this.contentDiv.hide(null,callback);
86 }
87 
88 /**
89  * 獲得驗證碼顯示的dom元素
90  */
91 Identifying.prototype.getContentDiv = function () {
92     return this.contentDiv;
93 }

然後,滑動驗證碼類繼承此父類(js繼承會單獨寫篇文章),滑動驗證碼類如下:

  1 /**
  2  * 滑動驗證類
  3  * complete傳遞的引數為identifyingId,identifyingType,moveEnd_X
  4  * @param config 各種配置
  5  */
  6 var ImgIdentifying = function(config) {
  7     Identifying.call(this, config.identifyingId, config.identifyingType,config.el);
  8     this.config = config;
  9     this.init();
 10     this.showIdentifying();
 11 }
 12 
 13 //繼承父類
 14 extendClass(Identifying, ImgIdentifying);
 15 
 16 /**
 17  * 銷燬函式
 18  */
 19 ImgIdentifying.prototype.destroy = function () {
 20     Identifying.prototype.destroy.call(this);
 21 }
 22 
 23 var width = '260';
 24 var height = '116';
 25 var pl_size = 48;
 26 var padding_ = 20;
 27 ImgIdentifying.prototype.init = function () {
 28 
 29     this.clearDom();
 30     var el = this.getContentDiv();
 31     var w = width;
 32     var h = height;
 33     var PL_Size = pl_size;
 34     var padding = padding_;
 35     var self = this;
 36 
 37     //這個要轉移到後臺
 38     function RandomNum(Min, Max) {
 39         var Range = Max - Min;
 40         var Rand = Math.random();
 41 
 42         if (Math.round(Rand * Range) == 0) {
 43             return Min + 1;
 44         } else if (Math.round(Rand * Max) == Max) {
 45             return Max - 1;
 46         } else {
 47             var num = Min + Math.round(Rand * Range) - 1;
 48             return num;
 49         }
 50     }
 51 
 52     //確定圖片
 53     var imgSrc = this.config.img;
 54     var X = this.config.X;
 55     var Y = this.config.Y;
 56     var left_Num = -X + 10;
 57     var html = '<div style="position:relative;padding:16px 16px 28px;border:1px solid #ddd;background:#f2ece1;border-radius:16px;">';
 58     html += '<div style="position:relative;overflow:hidden;width:' + w + 'px;">';
 59     html += '<div style="position:relative;width:' + w + 'px;height:' + h + 'px;">';
 60     html += '<img id="scream" src="' + imgSrc + '" style="width:' + w + 'px;height:' + h + 'px;">';
 61     html += '<canvas id="puzzleBox" width="' + w + '" height="' + h + '" style="position:absolute;left:0;top:0;z-index:222;"></canvas>';
 62     html += '</div>';
 63     html += '<div class="puzzle-lost-box" style="position:absolute;width:' + w + 'px;height:' + h + 'px;top:0;left:' + left_Num + 'px;z-index:11111;">';
 64     html += '<canvas id="puzzleShadow" width="' + w + '" height="' + h + '" style="position:absolute;left:0;top:0;z-index:222;"></canvas>';
 65     html += '<canvas id="puzzleLost" width="' + w + '" height="' + h + '" style="position:absolute;left:0;top:0;z-index:333;"></canvas>';
 66     html += '</div>';
 67     html += '<p class="ver-tips"></p>';
 68     html += '</div>';
 69     html += '<div class="re-btn"><a></a></div>';
 70     html += '</div>';
 71     html += '<br>';
 72     html += '<div style="position:relative;width:' + w + 'px;margin:auto;">';
 73     html += '<div style="border:1px solid #c3c3c3;border-radius:24px;background:#ece4dd;box-shadow:0 1px 1px rgba(12,10,10,0.2) inset;">';//inset 為內陰影
 74     html += '<p style="font-size:12px;color: #486c80;line-height:28px;margin:0;text-align:right;padding-right:22px;">按住左邊滑塊,拖動完成上方拼圖</p>';
 75     html += '</div>';
 76     html += '<div class="slider-btn"></div>';
 77     html += '</div>';
 78 
 79     el.html(html);
 80 
 81     var d = PL_Size / 3;
 82     var c = document.getElementById("puzzleBox");
 83     //getContext獲取該dom節點的canvas畫布元素
 84     //---------------------------------這一塊是圖片中央缺失的那一塊--------------------------------------
 85     var ctx = c.getContext("2d");
 86 
 87     ctx.globalCompositeOperation = "xor";
 88     //設定陰影模糊級別
 89     ctx.shadowBlur = 10;
 90     //設定陰影的顏色
 91     ctx.shadowColor = "#fff";
 92     //設定陰影距離的水平距離
 93     ctx.shadowOffsetX = 3;
 94     //設定陰影距離的垂直距離
 95     ctx.shadowOffsetY = 3;
 96     //rgba第四個引數是透明度,前三個是三原色,跟rgb比就是多了第四個引數
 97     ctx.fillStyle = "rgba(0,0,0,0.8)";
 98     //beginPath() 方法開始一條路徑,或重置當前的路徑。
 99     //提示:請使用這些方法來建立路徑:moveTo()、lineTo()、quadricCurveTo()、bezierCurveTo()、arcTo() 以及 arc()。
100     ctx.beginPath();
101     //指線條的寬度
102     ctx.lineWidth = "1";
103     //strokeStyle 屬性設定或返回用於筆觸的顏色、漸變或模式
104     ctx.strokeStyle = "rgba(0,0,0,0)";
105     //表示畫筆移到(X,Y)位置,沒畫東西
106     ctx.moveTo(X, Y);
107     //畫筆才開始移動到指定座標,之間畫一條直線
108     ctx.lineTo(X + d, Y);
109     //繪製一條貝塞爾曲線,一共四個點確定,開始點(沒在引數裡),和兩個控制點(1和2引數結合,3和4引數結合),結束點(5和6引數結合)
110     ctx.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y);
111     ctx.lineTo(X + 3 * d, Y);
112     ctx.lineTo(X + 3 * d, Y + d);
113     ctx.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d);
114     ctx.lineTo(X + 3 * d, Y + 3 * d);
115     ctx.lineTo(X, Y + 3 * d);
116     //必須和beginPath()成對出現
117     ctx.closePath();
118     //進行繪製
119     ctx.stroke();
120     //根據fillStyle進行填充
121     ctx.fill();
122 
123     //---------------------------------這個為要移動的塊------------------------------------------------
124     var c_l = document.getElementById("puzzleLost");
125     //---------------------------------這個為要移動的塊增加陰影------------------------------------------------
126     var c_s = document.getElementById("puzzleShadow");
127     var ctx_l = c_l.getContext("2d");
128     var ctx_s = c_s.getContext("2d");
129     var img = new Image();
130     img.src = imgSrc;
131 
132     img.onload = function () {
133         //從原圖片,進行設定處理再顯示出來(其實就是設定你想顯示圖片的位置2和3引數,和框w高h)
134         ctx_l.drawImage(img, 0, 0, w, h);
135     }
136     ctx_l.beginPath();
137     ctx_l.strokeStyle = "rgba(0,0,0,0)";
138     ctx_l.moveTo(X, Y);
139     ctx_l.lineTo(X + d, Y);
140     ctx_l.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y);
141     ctx_l.lineTo(X + 3 * d, Y);
142     ctx_l.lineTo(X + 3 * d, Y + d);
143     ctx_l.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d);
144     ctx_l.lineTo(X + 3 * d, Y + 3 * d);
145     ctx_l.lineTo(X, Y + 3 * d);
146     ctx_l.closePath();
147     ctx_l.stroke();
148     //帶陰影,數字越高陰影越嚴重
149     ctx_l.shadowBlur = 10;
150     //陰影的顏色
151     ctx_l.shadowColor = "black";
152 
153     // ctx_l.fill(); 其實加這句就能有陰影效果了,不知道為什麼加多個圖層
154 
155     //分割畫布的塊
156     ctx_l.clip();
157 
158     ctx_s.beginPath();
159     ctx_s.lineWidth = "1";
160     ctx_s.strokeStyle = "rgba(0,0,0,0)";
161     ctx_s.moveTo(X, Y);
162     ctx_s.lineTo(X + d, Y);
163     ctx_s.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y);
164     ctx_s.lineTo(X + 3 * d, Y);
165     ctx_s.lineTo(X + 3 * d, Y + d);
166     ctx_s.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d);
167     ctx_s.lineTo(X + 3 * d, Y + 3 * d);
168     ctx_s.lineTo(X, Y + 3 * d);
169     ctx_s.closePath();
170     ctx_s.stroke();
171     ctx_s.shadowBlur = 20;
172     ctx_s.shadowColor = "black";
173     ctx_s.fill();
174 
175     //開始時間
176     var beginTime;
177     //結束時間
178     var endTime;
179     var moveStart = '';
180     $(".slider-btn").mousedown(function (e) {
181         $(this).css({"background-position": "0 -216px"});
182         moveStart = e.pageX;
183         beginTime = new Date().valueOf();
184     });
185 
186     onmousemove = function (e) {
187         var e = e || window.event;
188         var moveX = e.pageX;
189         var d = moveX - moveStart;
190         if (moveStart == '') {
191 
192         } else {
193             if (d < 0 || d > (w - padding - PL_Size)) {
194 
195             } else {
196                 $(".slider-btn").css({"left": d + 'px', "transition": "inherit"});
197                 $("#puzzleLost").css({"left": d + 'px', "transition": "inherit"});
198                 $("#puzzleShadow").css({"left": d + 'px', "transition": "inherit"});
199             }
200         }
201     };
202 
203     onmouseup = function (e) {
204         var e = e || window.event;
205         var moveEnd_X = e.pageX - moveStart;
206         var ver_Num = X - 10;
207         var deviation = self.config.deviation;
208         var Min_left = ver_Num - deviation;
209         var Max_left = ver_Num + deviation;
210 
211         if (moveStart == '') {
212 
213         } else {
214             endTime = new Date().valueOf();
215             if (Max_left > moveEnd_X && moveEnd_X > Min_left) {
216                 $(".ver-tips").html('<i style="background-position:-4px -1207px;"></i><span style="color:#42ca6b;">驗證通過</span><span></span>');
217                 $(".ver-tips").addClass("slider-tips");
218                 $(".puzzle-lost-box").addClass("hidden");
219                 $("#puzzleBox").addClass("hidden");
220                 setTimeout(function () {
221                     $(".ver-tips").removeClass("slider-tips");
222                 }, 2000);
223                 self.success({
224                     'identifyingId': self.config.identifyingId, 'identifyingType': self.config.identifyingType,
225                     'moveEnd_X': moveEnd_X
226                 })
227             } else {
228                 $(".ver-tips").html('<i style="background-position:-4px -1229px;"></i><span style="color:red;">驗證失敗:</span><span style="margin-left:4px;">拖動滑塊將懸浮影象正確拼合</span>');
229                 $(".ver-tips").addClass("slider-tips");
230                 setTimeout(function () {
231                     $(".ver-tips").removeClass("slider-tips");
232                 }, 2000);
233                 self.error();
234             }
235         }
236         //0.5指動畫執行到結束一共經歷的時間
237         setTimeout(function () {
238             $(".slider-btn").css({"left": '0', "transition": "left 0.5s"});
239             $("#puzzleLost").css({"left": '0', "transition": "left 0.5s"});
240             $("#puzzleShadow").css({"left": '0', "transition": "left 0.5s"});
241         }, 1000);
242         $(".slider-btn").css({"background-position": "0 -84px"});
243         moveStart = '';
244         $(".re-btn a").on("click", function () {
245             Access.getAccess().initIdentifying($('#acessIdentifyingContent'));
246         })
247     }
248 }
249 
250 /**
251  * 獲取該型別驗證碼的一些引數
252  */
253 ImgIdentifying.getParamMap = function () {
254 
255     var min_X = padding_ + pl_size;
256     var max_X = width - padding_ - pl_size - pl_size / 6;
257     var max_Y = padding_;
258     var min_Y = height - padding_ - pl_size - pl_size / 6;
259 
260     var paramMap = new Map();
261     paramMap.set("min_X", min_X);
262     paramMap.set("max_X", max_X);
263     paramMap.set("min_Y", min_Y);
264     paramMap.set("max_Y", max_Y);
265 
266     return paramMap;
267 }
268 
269 /**
270  * 設定驗證成功的回撥函式
271  * @param success
272  */
273 ImgIdentifying.prototype.setSuccess = function (successFunc) {
274     this.successFunc = successFunc;
275 }
276 
277 /**
278  * 設定驗證失敗的回撥函式
279  * @param success
280  */
281 ImgIdentifying.prototype.setError = function (errorFunc) {
282     this.errorFunc = errorFunc;
283 }

其中init的方法,大家就可以抄啦,驗證碼是這裡生成的(感謝網上一些熱心網友提供的Mod,在此基礎上改的)。


 

後端的設計:

首先要有一個驗證碼的介面,將一些常量和共同的方法抽象到介面中(介面最重要的作用就是行為的統一,意思是我如果知道這個是驗證碼,那麼必定就會有驗證的方法,不管它是滑動驗證,圖形驗證等,然後就可以放心的呼叫驗證方法去獲取驗證結果,下面過濾器設計就可以立馬看到這作用。具體java介面的說明會單獨寫篇文章),介面如下:

 1 /**
 2  * 驗證碼類的介面,所有驗證碼必須繼承此介面
 3  */
 4 public interface I_Identifying<T> {
 5 
 6     String EXCEPTION_CODE = SystemStaticValue.IDENTIFYING_EXCEPTION_CODE;
 7     String IDENTIFYING = "Identifying";
 8     //--------------以下為驗證碼大體錯誤型別,丟擲錯誤時候用,會傳至前端---------------
 9     //驗證成功
10     String SUCCESS = "Success";
11     //驗證失敗
12     String FAILURE = "Failure";
13     //驗證碼過期
14     String OVERDUE = "Overdue";
15 
16     //-------以下為驗證碼具體錯誤型別,存放在checkResult-------------
17     String PARAM_ERROR = "驗證碼引數錯誤";
18     String OVERDUE_ERROR = "驗證碼過期";
19     String TYPE_ERROR = "驗證碼業務型別錯誤";
20     String ID_ERROR = "驗證碼id異常";
21     String CHECK_ERROR = "驗證碼驗證異常";
22 
23 
24     /**
25      * 獲取生成好的驗證碼
26      * @param request
27      * @return
28      */
29     public T getInstance(HttpServletRequest request) throws Exception;
30 
31     /**
32      * 進行驗證,沒拋異常說明驗證無誤
33      * @return
34      */
35     public void checkIdentifying(HttpServletRequest request) throws Exception;
36 
37     /**
38      * 獲取驗證結果,如果成功則為success,失敗則為失敗資訊
39      * @return
40      */
41     public String getCheckResult();
42 
43     /**
44      * 獲取驗證碼的業務型別
45      * @return
46      */
47     public String getIdentifyingType();
48 }

 

然後,設計一個具體的滑動驗證類去實現這個介面,這裡只貼引數:

 1 /**
 2  * @author NiceBin
 3  * @description: 驗證碼類,前端需要生成驗證碼的資訊
 4  * @date 2019/7/12 16:04
 5  */
 6 public class ImgIdentifying implements I_Identifying<ImgIdentifying>,Serializable {
 7     //此次驗證碼的id
 8     private String identifyingId;
 9     //此次驗證碼的業務型別
10     private String identifyingType;
11     //需要使用的圖片
12     private String imgSrc;
13     //生成塊的x座標
14     private int X;
15     //生成塊的y座標
16     private int Y;
17     //允許的誤差
18     private int deviation = 2;
19     //驗證碼生成的時間
20     private Calendar calendar;
21     //驗證碼結果,如果有結果說明已經被校驗,防止因為網路延時的二次校驗
22     private String checkResult;
23 
24     //下面是邏輯程式碼...
25 }

 

上面每個變數都是一種校驗手段,如calendar可以檢驗驗證碼是否過期,identifyingType檢驗此驗證碼是否是對應的業務等。每多想一點,別人破解就多費勁一點。

後端驗證碼的驗證是不需要具體的類去呼叫的,而是被一個過濾器統一過濾,才過濾器註冊的時候,將需要進行驗證的路徑寫進去即可,過濾器程式碼如下:

 1 /**
 2  * @author NiceBin
 3  * @description: 驗證碼過濾器,幫忙驗證有需要驗證碼的請求,不幫忙生成驗證碼
 4  * @date 2019/7/23 15:06
 5  */
 6 @Component
 7 public class IdentifyingInterceptor implements HandlerInterceptor {
 8     @Override
 9     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
10 
11         HttpSession session = request.getSession();
12         I_Identifying identifying= (I_Identifying)session.getAttribute(I_Identifying.IDENTIFYING);
13         if(identifying!=null){
14             identifying.checkIdentifying(request);
15         }else {
16             //應該攜帶驗證碼資訊的,結果沒有攜帶,那就是個非法請求
17             return false;
18         }
19         return true;
20 
21     }
22 
23     @Override
24     public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
25 
26     }
27 
28     @Override
29     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
30 
31     }
32 }
View Code

可以看到介面的用處了,之前在使用者申請驗證碼時,驗證碼類是放到使用者session中的,所以這裡直接取出呼叫checkIdentifying即可,不需要關係它到底是滑動驗證碼,還是圖片驗證碼什麼的。

 

以上就是對滑動驗證的設計分享,都是自己拍腦袋想出來的和生產有差距,所以哪裡不對或有更好的想法,希望大家也分享給我,一起做個好朋友