客庄券登錄頁面前端研究與分析

前言

最近自從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>&nbsp;<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>&nbsp;<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);
}
});
})
});
view raw index.js hosted with ❤ by GitHub

不看還好,看了發現這個流程有點問題:

    • 如果從第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>&nbsp;<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