打造一個書籤管理網站

最近系上的OJ不幸陣亡,打算自己東敲西打搞出一個新的批改系統,適逢這次的作業是個書籤管理網,需要登入介面及支援線上修改,不妨就拿來小試身手吧 ~

目前整個網站都放在Github上,這篇文章只摘記了一些實作方法與概念,若有任何問題或改進方向歡迎留言及提出Issue。

網站概觀

Imgur

這是一個簡單的書籤管理網站,不妨就當成會走路的「我的最愛」吧。

使用者登入後,就能夠新增/刪改自己的書籤集。此外,連結多起來時,也提供了篩選功能可快速取得需要的書籤。

作業要求是這麼說啦,不過目前登入與註冊都蠻方便的,所以當成個線上筆記條也不賴。以後出門在外,瀏覽到了什麼精彩文章,也不必再發訊息給自己,就直接在LeafTag上留下足跡吧。

從登入頁面說起

Imgur
很熟悉嗎?其實是照著Github刻出來的(´・ω・`)

網站的運作流程大致是這樣:

1
2
3
4
5
              SESSION之壁
login.html --login--> index.php - 標籤管理
| | metaSearch.php - 資料搜尋
| | update.php - 個人資訊管理
register.html | 上述三者使用navbar.html切換

當登入成功後,會在伺服端建立$_SESSION['userID'],除了告知目前的使用者是誰外,也可透過檢測$_SESSION['userID']來阻止未登入用戶存取頁面:

1
2
3
4
5
6
7
//如果沒記錄登入訊息,就把用戶送回登入頁面
<?php
session_start();
if(!isset($_SESSION['userID'])){
header("Location: login.html");
}
?>

除了metasearch以外,各頁面對php的請求統一會送往action.php,在表單中會設置一個隱藏欄位或參數action,我透過switch(_POST["action"])來告知接下來伺服端該做什麼:

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
/**********************************************
Get the action command from client

Usage:
switch(_POST["action"])

Value:
- 0 : login
- 1 : Register
- 2 : Logout
- 3 : Add a tag
- 4 : Modify a tag
- 5 : Delete a tag
- 6 : Update personal data.
- 7 : List all the tags
**********************************************/
switch ($_POST["action"]) {
case '0':
login($link);
break;
case '1':
register($link);
break;
case '2':
logout();
break;
case '3':
addTag($link);
break;
case '4':
modifyTag($link);
break;
case '5':
deleteTag($link);
break;
case '6':
updateData($link);
break;
case '7':
listAllTags($link);
break;
default:
break;
}

比方login.html就向後端送出了表單內容與action=0;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function login($db){

$escapeUN = mysql_real_escape_string($_POST["username"]);
$query= "SELECT * FROM `user_table` WHERE username = '".$escapeUN."'";
$result= mysql_query($query,$db) or die ("Error in query: $query.". mysql_error());
$rows=mysql_fetch_array($result);
$password = $rows[1];
if(md5($_POST["password"]) == $password){
echo("Success");
$_SESSION["userID"] = $_POST["username"];
}
else{
echo("Wrong username or password.<br/>");
}
}

action.php 不會輸出任何頁面,而只簡單的回報工作成果,這全都要歸功於 AJAX 的發明。

利用AJAX來溝通吧

在上古時期,客戶端使用Form提交時,會把資料以POST/GET的方式丟到後端,伺服端處理好請求後,再回傳一個新的網頁,最糟糕的是,這個全新網頁和原本使用者用來提交資料的網頁幾乎一模一樣,無疑是在浪費頻寬與時間。
於是有人想到了,「我們每次提交表單時,就只更新需要更新的部分,別把整個頁面都重新整理」,這就是AJAX的中心思想。以login.html為例,使用者送出請求後,如果成功,直接登入系統,失敗則只在 Sign in 下追加一行提示訊息,而非把整個頁面重刷一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--login.html表單內容-->
<form id="user-information" method="post">
<input type="hidden" name="action" value="0">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" name="username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="pw" name="password" >
</div>
<input class="gitbuh-submit-button" type="button" value="Sign in" onClick="Submit()">
<div id="response-text" class="feedback"></div>
</form>

按下送出後,AJAX會將資料擲往伺服器,再將伺服器的輸出(echo/print)填入id為response-text的區塊中。

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
<!--JQuery with AJAX -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script>
//
var Submit=function(){
$.ajax({
//該傳往哪個php
url: "action.php",
//把表格序列化即是把資料串起來
//比如username=hello&passowrd=world
data: $('#user-information').serialize(),
//說明php的傳輸方式
type:"POST",
dataType:'text',

//AJAX執行成功時
//如果驗證成功,把使用者導向index.php
//如果驗證失敗,把伺服器的說明填到response-text裡
success: function(response){
if(response == "Success"){
document.location.href="index.php";
}else{
//show the error message in login.html
$('#response-text').html(response);
$('#response-text').fadeIn("slow");
}
},

//如果AJAX執行中發生了錯誤,提示Exception與AJAX status
error:function(xhr, ajaxOptions, thrownError){
alert(xhr.status);
alert(thrownError);
}
});
}
</script>

書籤的動態刪減

毫無疑問,做為一個書籤網站,如果每增加或刪除一個書籤,頁面就要重新整理一次,使用者體驗實在有待加強。要讓書籤的刪改儘可能自然,不妨讓AJAX一手包辦吧。在tag.js實作了新增、刪除、修改書籤這類基本操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//新增與修改書籤

var TagOperate=function(actionCode){

var para = $('#tagTable').serialize() + "&action=" + actionCode;
$.ajax({
url: "action.php",
data: para,
type:"POST",
dataType:'text',
success: function(response){
$('#tag-list').html(response);
$('#tag-list').fadeIn("slow");
},
error:function(xhr, ajaxOptions, thrownError){
alert(thrownError);
}
});
}

事實上,只要增加參數,再處理一下para就能將所有操作整合在一塊兒,不過我的php端已經繳交了,所以就…… ╮( ̄▽ ̄)╭

書籤搜尋

當初會想搞這個是想仿效一下Blog的篩選功能,可惜本錢不夠

Imgur

我的做法是單純的字串比對,找到符合的就留下來,不符合的就把它隱藏起來

1
2
<!-- HTML端的書籤搜尋欄 -->
<input class="form-control" type="text" id="tag-filter" placeholder="書籤搜尋"></input>

使用KeyUp監聽使用者的輸入,當按鍵一放開,就開始篩選需要的標籤。

1
2
3
4
5
6
//將KeyUp綁定在輸入欄上
$(document).ready(function(){
$("#tag-filter").keyup(tagSearch);
})

//

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
//KeyUp觸發時,開始篩選
var tagSearch=function(){
//取得使用者的關鍵字
var itag = $("#tag-filter").val();
//把所有tag都抓下來
var list = $(".tag-detail");

//為空代表使用者沒有查詢,即是所有標籤都符合
if(itag != ""){
//遍歷 - 如果比對失敗就隱藏,比對成功則留下來
var i=0;
for(i=0;i<list.length;i++){
if(!stringMatching(itag,list[i].innerHTML)){
$("#"+i).addClass("hideThis");
}else{
$("#"+i).removeClass("hideThis");
}
}
}else{
listAllTag();
}
}

//字串比對
function stringMatching(matcher,matchee){
var patternLength = matchee.length - matcher.length + 1;
var i = 0 , j = 0;
for(i = 0 ; i < patternLength ; i++)
for(j = 0 ; i+j < matchee.length && matchee[i+j] == matcher[j] ; j++)
if(j+1 == matcher.length)
return true;
return false;
}

css 中 hideThis的內容其實相當簡單:

1
2
3
.hideThis{
display:none;
}

這個網站安全嗎?

不安全,這只是個登入網站的小範例,請別直接移植它。
Imgur看吶 ~ 火狐的精美吐槽。

目前在網路安全上只做了兩點:

  • 擋 SQL injection

關於SQL注射式攻擊在網路上已一談再談,解決方法也很多,像是參數化查詢啦、弄個簡單的cleanInput()過濾輸入啦,而我採用的是最懶人的策略:mysql_real_escape_string,他可以在特殊字元前加上跳脫字元,比如'會變成\',這麼一來,即便使用者在輸入填上了SQL指令,也會被篩成一般的字串,能避免基本的SQL注射。

  • SQL 端的密碼以MD5呈現

我曾經想過先在用戶端取得密碼雜湊,再傳送至伺服端,避免監聽人抓到的是原始密碼。不過話說回來,這其實就是TLS/SSL的工作,只要有伺服端的控制權,要追加根本不成問題,自己刻一個簡直多此一舉,說不定還會留下漏洞給別人攻擊。

所以我就只在PHP端雜湊一下使用者的密碼囉,不過只防君子。以前曾看過一個使用者與密碼都是原始值的資料庫,這種做法其實相當危險,會讓管理者變成最恐怖的攻擊者。如果說在送到資料庫前多一層雜湊保護,至少能減少這類不必要的風險。