前言
最近自從3倍消費券以來,出了許多額外的券,像是動資券,農遊券等。
大眾們也蜂擁而入而導致網站掛掉時有所聞,那最近的客庄券更是慘烈,導致流程設計不得不改成最一般的方式,不用LINE前端JavaScript SDK的進行身份驗證與登入了。
本文章,並不是要探討流量導致啟動防護機制的問題,而是討論客庄券登錄流程設計的問題。
客庄券1.0
一開始還沒把LINE JS SDK拿掉之前,我姑且叫它「客庄券1.0」,所以當開始可以登錄時,我利用通勤的時間,利用手機打開頁面登入,但過了不久就出現以下的畫面:
從這個頁面來看,不難發現這是利用Heroku所架設的服務,而如果這時候使用cURL指令則會得到以下的頁面:
從上圖得知,這個頁面就一直處在503 service unavailable的HTTP回應狀態碼,意思就是遠端server掛了。
客庄券2.0
目前是掛掉的狀態,那這個情況持續了好一段時間,後來再把LINE前端JS SDK驗證登入拿掉之後,就變成下面的頁面了,我姑且叫它做「客庄券2.0」:
如果從頁面來看的話,看起來是很正常很多,後來我試著登錄券之後,就會給我這個頁面:
可是當我再次登入時候,又再次出現上述的截圖畫面,這就讓我越想越不對勁了…..直到我打開Chrome Web Console才明白到底是怎麼一回事:
1 2 | Failed to load resource: the server responded with a status of 405 () index.js:257 ajax fail |
從上面跳出的訊息來看,看來是AJAX發送的請求有405 Method Not Allowed問題,而自定義設定console log message 為ajax fail。
那從這樣的角度來看,我只好打開index.js看看內容是什麼了,底下為節錄此頁面的index.js程式碼:
$( document ).ready(function() { | |
console.log( "ready!" ); | |
let line_userid = ""; | |
$('input[name="idtype"]')[0].checked = true; // 預設為身份證字號 | |
liff.init({ | |
liffId: "1552207375-1xDKJLe5" | |
}) | |
.then(() => { | |
// Start to use liff's api | |
liff.getProfile() | |
.then(profile => { | |
line_userid = profile.userId; | |
}) | |
.catch((err) => { | |
//alert('getProfile error', err); | |
}); | |
}) | |
.catch((LiffError) => { | |
// Error happens during initialization | |
//alert(err.code, err.message); | |
}); | |
$("#twzipcode").twzipcode( { | |
zipcodeIntoDistrict: false | |
}); | |
$("#btn_save").click(function(){ | |
$("#btn_save").prop('disabled', true); | |
var uname = $("#uname").val(); | |
var uidentity_card = $("#uidentity_card").val(); | |
var uphone = $("#uphone").val(); | |
var uagree = "n"; | |
var idtype = $("input[name='idtype']:checked").val(); | |
var recaptcha = ""; | |
if ($("#uagree").is(":checked")) { | |
uagree = "y"; | |
} | |
// if (line_userid == "") { | |
// swal({ | |
// text: "非由 Line@入口進入,無法執行此功能。", | |
// type: "warning" | |
// }).then(function() { | |
// $("#btn_save").prop('disabled', false); | |
// $("#waitBox").html(""); | |
// }); | |
// return; | |
// } | |
if ($("#uname").val() == "") { | |
swal({ | |
text: "請輸入姓名。", | |
type: "warning" | |
}).then(function() { | |
$("#uname").focus(); | |
$("#btn_save").prop('disabled', false); | |
$("#waitBox").html(""); | |
}); | |
return; | |
} | |
/* | |
if (isPureChinese($("#uname").val()) == false) { | |
swal({ | |
text: "姓名請輸入中文。", | |
type: "warning" | |
}).then(function() { | |
$("#uname").focus(); | |
$("#btn_save").prop('disabled', false); | |
$("#waitBox").html(""); | |
}); | |
return; | |
} | |
*/ | |
if ($("#uidentity_card").val() == "") { | |
if (idtype == "1") { | |
swal({ | |
text: "請輸入身份證號碼。", | |
type: "warning" | |
}).then(function() { | |
$("#uidentity_card").focus(); | |
$("#btn_save").prop('disabled', false); | |
$("#waitBox").html(""); | |
}); | |
return; | |
} else if (idtype == "2") { | |
swal({ | |
text: "請輸入居留證號碼。 ", | |
type: "warning" | |
}).then(function() { | |
$("#uidentity_card").focus(); | |
$("#btn_save").prop('disabled', false); | |
$("#waitBox").html(""); | |
}); | |
return; | |
} | |
} | |
if ($("#uphone").val() == "") { | |
swal({ | |
text: "請輸入行動電話。", | |
type: "warning" | |
}).then(function() { | |
$("#uphone").focus(); | |
$("#btn_save").prop('disabled', false); | |
$("#waitBox").html(""); | |
}); | |
return; | |
} | |
var county = $("#twzipcode").twzipcode('get', 'county'); | |
var district = $("#twzipcode").twzipcode('get', 'district'); | |
if (county == "") { | |
swal({ | |
text: "請選取縣市。", | |
type: "warning" | |
}).then(function() { | |
$("#btn_save").prop('disabled', false); | |
$("#waitBox").html(""); | |
}); | |
return; | |
} | |
if (district == "") { | |
swal({ | |
text: "請選取區/鄉鎮。", | |
type: "warning" | |
}).then(function() { | |
$("#btn_save").prop('disabled', false); | |
$("#waitBox").html(""); | |
}); | |
return; | |
} | |
if (uagree == "n") { | |
swal({ | |
text: "請同意免責聲明", | |
type: "warning" | |
}).then(function() { | |
$("#btn_save").prop('disabled', false); | |
$("#waitBox").html(""); | |
}); | |
return; | |
} | |
swal({ | |
title: "請確認以下資訊", | |
html: "敬請留意您所留的手機號碼,以免影響權益<br>是否確定送出?", | |
type: "warning", | |
showCancelButton: true, | |
confirmButtonColor: "#DD6B55", | |
confirmButtonText: "是", | |
cancelButtonText: "否" | |
}) | |
.catch (swal.noop) | |
.then((willDelete) => { | |
if (willDelete) { | |
$("#waitBox").html("<h4>資料送出中請稍候... <img id='wgif' src='"+contextPath+"images/ajax-loader.gif' /></h4>"); | |
// var formdata = new FormData(); | |
// formdata.append("city", county); | |
// formdata.append("dist", district); | |
// formdata.append("name", uname); | |
// formdata.append("phone", uphone); | |
// formdata.append("idtype", idtype); | |
// formdata.append("identity_card", uidentity_card); | |
// formdata.append("line_userid", line_userid); | |
// formdata.append("recaptcha", recaptcha); | |
var formdata = { | |
"city": county, | |
"dist": district, | |
"name": uname, | |
"phone": uphone, | |
"idtype": idtype, | |
"identity_card": uidentity_card, | |
"line_userid": line_userid, | |
"recaptcha": recaptcha | |
} | |
$.ajax({ | |
url: "https://369.pdis.tw/useregister", | |
type: 'POST', | |
// url: "./useregister", | |
// type: 'GET', | |
dataType: "json", | |
contentType: "application/json;charset=utf-8", | |
data: JSON.stringify(formdata), | |
processData:false, | |
success: function(rdata, status, xhr){ | |
if (rdata == "200") { | |
$("#btn_save").hide(); | |
$("#waitBox").html(""); | |
swal({ | |
html: "登錄成功<br>本活動於7月29日下5時前抽籤並公布得獎名單,抽籤結果將另行發送,敬請留意您所留的手機號碼,便於聯繫。<br> <br>注意:客委會保留隨時可修改、終止或變更本活動之權利,若有其他未盡事宜,悉依本會相關規定或解釋為準。", | |
type: "success" | |
}).then(function() { | |
liff.closeWindow(); | |
$("#twzipcode").twzipcode('reset'); | |
$("#uname").val(''); | |
$("#uidentity_card").val(''); | |
$("#uphone").val(''); | |
$('input[name="idtype"]')[0].checked = true; // 預設為身份證字號 | |
$('#uagree').prop('checked', false); | |
$("#btn_save").prop('disabled', false); | |
}); | |
} else if (rdata == "501") { | |
$("#waitBox").html(""); | |
swal({ | |
text: "手機檢核失敗。", | |
type: "error" | |
}).then(function() { | |
$("#btn_save").prop('disabled', false); | |
}); | |
} else if (rdata == "502") { | |
$("#waitBox").html(""); | |
if (idtype == "1") { | |
swal({ | |
text: "身份證號碼檢核失敗。", | |
type: "error" | |
}).then(function() { | |
$("#btn_save").prop('disabled', false); | |
}); | |
} else if (idtype == "2") { | |
swal({ | |
text: "居留證號碼檢核失敗。", | |
type: "error" | |
}).then(function() { | |
$("#btn_save").prop('disabled', false); | |
}); | |
} | |
} else if (rdata == "503") { | |
$("#waitBox").html(""); | |
if (idtype == "1") { | |
swal({ | |
text: "身份證號碼或手機號碼或此Line帳號已有註冊資料。", | |
type: "error" | |
}).then(function() { | |
$("#btn_save").prop('disabled', false); | |
}); | |
} else if (idtype == "2") { | |
swal({ | |
text: "居留證號碼或手機號碼或此Line帳號已有註冊資料。", | |
type: "error" | |
}).then(function() { | |
$("#btn_save").prop('disabled', false); | |
}); | |
} | |
} | |
}, | |
error: function(){ | |
console.log("ajax fail"); | |
$("#waitBox").html(""); | |
swal({ | |
html: "登錄成功<br>本活動於7月29日下5時前抽籤並公布得獎名單,抽籤結果將另行發送,敬請留意您所留的手機號碼,便於聯繫。<br> <br>注意:客委會保留隨時可修改、終止或變更本活動之權利,若有其他未盡事宜,悉依本會相關規定或解釋為準。", | |
type: "success" | |
}).then(function() { | |
$("#twzipcode").twzipcode('reset'); | |
$("#uname").val(''); | |
$("#uidentity_card").val(''); | |
$("#uphone").val(''); | |
$('input[name="idtype"]')[0].checked = true; // 預設為身份證字號 | |
$('#uagree').prop('checked', false); | |
$("#btn_save").prop('disabled', false); | |
}); | |
}, | |
complete: function(XMLHttpRequest, textStatus) { | |
$("#btn_save").prop('disabled', false); | |
$("#waitBox").html(""); | |
} | |
}); | |
} else { | |
//alert("no"); | |
$("#btn_save").prop('disabled', false); | |
} | |
}); | |
}) | |
function isPureChinese(input) { | |
var reg = /^[\u4E00-\u9FA5]+$/ | |
if (reg.test(input)) { | |
return true | |
} else { | |
return false | |
} | |
} | |
$("#aaaaa").click(function() { | |
$("#waitBox").html("<h4>資料送出中請稍候... <img id='wgif' src='"+contextPath+"images/ajax-loader.gif' /></h4>"); | |
}) | |
$("#btest").click(function() { | |
swal({ | |
title: "請確認以下資訊", | |
text: "敬請留意您所留的手機號碼,以免影響權益", | |
type: "warning", | |
showCancelButton: true, | |
confirmButtonColor: "#DD6B55", | |
confirmButtonText: "是", | |
cancelButtonText: "否" | |
}) | |
.catch (swal.noop) | |
.then((willDelete) => { | |
if (willDelete) { | |
//location.href = contextPath + "mgr/logout"; | |
} else { | |
//alert("no"); | |
$("#btn_save").prop('disabled', false); | |
} | |
}); | |
}) | |
}); |
不看還好,看了發現這個流程有點問題:
-
- 如果從第185行開始看,就會發現在收集好前端頁面使用者的登錄相關資訊之後,在經歷前面一些檢查,到此行開始就會使用jQuert AJAX進行發送請求了。
- 請求方式看起來很簡單,就是發送一個請求到「https://369.pdis.tw/useregister」並POST方法傳送,等到回應時分成兩個結果callback functions,分別是「success」與「error」,看起來回應的body (response body)都是一個自定義狀態文字並轉換成中文的訊息。
- success function callback還好,看起來沒有什麼問題,但是error function callback就有比較大的問題了,因為從上述的Chrome Console來看,我每次登錄的訊息都是跑到error function callback,那跑到這裡就算了,居然這裡出現的Render前端頁面尋居然都是:
1 | 登錄成功<br>本活動於7月29日下5時前抽籤並公布得獎名單,抽籤結果將另行發送,敬請留意您所留的手機號碼,便於聯繫。<br> <br>注意:客委會保留隨時可修改、終止或變更本活動之權利,若有其他未盡事宜,悉依本會相關規定或解釋為準。 |
那這樣一來,不管是成功還是失敗,永遠都不會顯示真正想要對應的訊息,那每次的登錄其實根本不會成功。
那大家都會以為成功,因為都有跳出成功登錄的頁面給使用者,那稍微看一下請求的網址是:「https://369.pdis.tw/useregister」,那我就從瀏覽器Console把cURL請求複製下來用自己的終端機送看看,結果發現我怎麼送都是405 Method Not Allowed回應狀態碼,並給我自定義的「200」response body。
那如果是利用cURL純粹發送一個GET方法的請求並傳response headers,會得到下列的圖示:
從上圖的response headers可以得知有幾個下列的訊息:
- 「access-control-allow-headers」用在跨網域使用
- 「access-control-allow-methods」允許幾個方法進行跨網域的請求
- 「access-control-allow-origin」允許跨網域的來源網址,這邊允許的是「*」,意思是都可以
- 「server: Caddy」,這是使用Go所開發的HTTP/2 server
結論
從上面的前端程式分析,我覺得有幾個問題需要解決的:
- 405 Method Not Allowed不應該在此出現,並每次都到jQuery AJAX 的error function callback。
- 目前猜測的原因,應該是後端在接收發送請求時處理不當,需要從後端檢查著手。
- 修改error function callback行為,會到error function callback,都是4xx或是5xx的HTTP狀態碼,就是代表有問題的,尤其是4xx的狀態碼代表跟client error有關,因此我覺得並不是顯示「登錄成功」等相關訊息,而是出現錯誤訊息給使用者,讓開發團隊知道jQuery AJAX並沒有進到success function callback,而是一直進入error function callback
- 若是正常流程,應該重複登錄會進入到success function callback並回應503自定義response body,並在前端頁面上顯示有「身份證號碼或手機號碼或此Line帳號已有註冊資料。」等字樣的文字訊息
- 使用者應該都沒有在客庄券2.0登錄成功過
更新
- 經過多方了解之後,這樣的流程實屬正常,回應錯誤的HTTP code事故意的,為了不要觸發某些驗證系統的功能,因此才出此下策的設計,之後要驗證就直接在手動撈取資料庫驗證即可。
- 這個流程之前在 mask-static 已經用過一次
- https://github.com/PDIS/emask-static/blob/master/Caddyfile#L16