Enjoy Your Coding

給初學者的網站開發分享

在這篇文章中,分享了一些後端開發經驗及注意事項。包含過去從入門學網站開發到前端(SPA)、後端分離開發中,經歷過的一些事情後的心得與改進方法等經驗分享。

前言

剛開始學網站開發時,是在我大二下學期(2013)的時候。那時候學網頁除了興趣外,也是因為期末專案要做出一個系統,藉由該次機會學習了Apache、PHP、HTML、CSS、JavaScript與MySQL等來完成網站。

相信很多後端工程師一開始應該都是全端工程師,從HTML開始學起,透過後端語言(PHP, Ruby, C#)等輸出HTML給用戶端,練習使用表單,搭配資料庫處理。

當時,對網站技術相當陌生,一開始在不了解HTTP協定、XSS等什麼的時候搞不太清楚下,看了文件串接了Facebook API及註冊登入等功能,基本的一些安全注意事項沒有做的很完善。

請做網站的朋友那時幫我做了點測試,一下子就出現XSS InjectionSQL Injection等可怕事情就出現了。更可怕的事情是我今天登入的是使用者A,卻能修改使用者B的密碼,像這樣的事情發生在購物網站上,一定天下大亂。

學期結束後,在暑假練習做了個人部落格與簡單的購物網站(不包含金流),同時程式碼也追求模組化,重複做的事情會另外寫一個function去做處理,所有來自用戶端的內容必先做前處理、檢查權限,包含了Injection的字串過濾、是否為該資料的擁有者等,每一步都做檢查,慢慢瞭解了主從式架構溝通與HTTP協定的運作原理。除此之外,還慢慢摸了jQuery、AngularJS、Ajax、JSON、API、SSO、ORM、MVC、Wireshark等事項,加速了網站的開發、基本的安全與模組化。

何謂主從式架構 (Client-Server Model)

生活中最好的例子就是網站(ex: 購物網站、新聞網站等),主從式架構分為兩層:用戶端(Client Side)與伺服器端(Server Side)

用戶端應用程式 (Client-Side Application)

包含了常用的瀏覽器、手機App、桌面視窗程式等。

伺服器端應用程式 (Server-Side Application)

包含了網站伺服器、資料庫、靜態檔案的存取等。

Image of Yaktocat

如上圖,在此架構中,當用戶端需要與伺服器交換資料時,用戶端會帶入一些資訊向伺服器端送出請求(Request),透過網際網路傳輸至伺服器端做處理後,會將資料回應(Response)給用戶端。

給初學者的注意事項

DRY原則

DRY是Don't Repeat Yourself的縮寫,簡單說撰寫程式碼時,模組化程式碼,能不重複的內容就不重複,重複的內容定義成function後續開發做使用。 若今天不同的頁面,但內容重複性很高,直接複製貼上建立新的檔案來完成,未來在維護上面有錯誤可能需要改兩個以上地方,若將來是別人接手維護時,造成無法理解程式碼、不易除錯、漏掉小地方,則會變成既有程式碼 Legacy Code技術債,不易維護與營運。

別隨意在正式機上除錯

盡可能避免在正式機上除錯,請架設另一台機器做除錯。為了除錯方便,有些人會直接在正式站台的主機上面進行除錯,過程中有可能因為一個不小心,就將伺服器上的重要資料刪除。我自己是傾向初期開發時,先建立好應用程式Log存取,以便未來開發過程中遇到bug時,方便除錯。針對不同的Log訊息來決定除錯的方式。

如果是應用程式錯誤,看一下Exception內容是什麼。若跟資料庫無關,則在開發電腦上,先模擬出幾種會造成該錯誤訊息的測試案例來進行除錯。修改完成後,先在測試機上做一樣測試案例,確保沒問題再發布至正式機。若非應用程式本身邏輯、流程問題,我會將正式機的資料庫複製一份到本地端進行除錯,接著再測試機上做測試,確保沒問題再發布至正式機。

雖著時代改變,我個人推薦可以將開發環境與正式環境使用Docker等Container技術建立,以便除錯與維護。

使用框架,但不要太過依賴

框架幫我們做好了基本的一些設計模式(Design Pattern)及基底,幫助我們避免了像是XSSSQL Injection等漏洞,大部分Web框架也提供了使用者驗證(Authentication)、資料庫的DAO、ORM,來幫我們快速開發,但像一些邏輯上的錯誤或架構上的擴充,還是要自己撰寫或找社群上的資源來完成。

個人覺得框架幫我們加速開發,但框架只作為我們程式碼的基底,更深的擴充架構(Facade、Microservices)、資訊安全、資源的重複利用是要靠自己完成的,不是框架沒提供的或上網找不到的功能就一定做不到,也記得找到範例程式碼時,不要直接複製或引用,須確認真的能使用才使用。

別直接使用網路上的程式碼

免錢的最貴,直接不看就複製使用也很可怕,不怕不能動,只怕有危險...。 這邊我們來舉一個CORS(跨網域存取)的例子做說明,基本上跨網域存取預設大部分伺服器是禁止的,除非有特殊需求需自己開放。特殊需求像是開發SPA(單頁應用程式)時,API的網域跟前端站台的網域不同,所以需要額外處理來做開放。

下列是其中一項搜尋找到的結果,直接用這是不好的,千萬別使用

header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');

基本上放上去後能動了!但卻有潛藏的資安漏洞在裡面。Access-Control-Allow-Origin*直接複製貼上就使用的人知道嗎? Access-Control-Allow-Origin*代表了任意網域都可進行存取相關資源。

相關設定與開放是要看實際需求,我們今天要解決的是跨網域問題,但不代表我們要將預設的限制全部開放,是否會有隱藏性的安全問題很難說。

上述案例建議透過仔細閱讀,並了解內容,依照專案需求進行修改使用。

建議使用時Access-Control-Allow-Origin指定單一網域就好,如下:

header('Access-Control-Allow-Origin: https://sudo.tw');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');

永遠不要相信來自用戶端的資料

不管有沒有HTTPS、API怎麼樣設計,都要假設用戶端使用者可能是機器人,也可能看得到資料傳輸的內容。

對網頁來說,只要透過瀏覽器的開發者工具監聽網路傳輸的部分,而且也可以看到JavaScript的原始碼,進行修改測試,了解API運作原理。

不是只有瀏覽器是可能危險的,對手機App、桌面應用程式來說,可以透過工具來將應用程式轉回去原始碼進行修改測試後,重新編譯使用,了解API運作原理。

因此,我們要假設用戶端的使用者看得到我們的用戶端應用程式原始碼,知道了與後端伺服器之間的溝通後,針對可能的程式漏洞進行測試,進而造成營運上的損失。

我們使用以下的JSON來做說明:

{
    "order_amount": 5000,
    "shopcart_list": [
        {
            "product_id": 1,
            "name": "洗衣精",
            "price": 500,
            "quantity": 5,
            "sub_total": 2500
        },
        {
            "product_id": 2,
            "name": "洗髮精",
            "price": 500,
            "quantity": 5,
            "sub_total": 2500
        }
    ],
    "user_id": 6
}

以上的JSON字串是用戶端傳遞給伺服器端的訂單資訊,針對幾個地方我們來做一下說明。 為何用戶端需傳遞user_idorder_amountnamepricesub_total等欄位呢? 這些欄位的資訊是後端本身就可以取得的,user_id可依靠Access-TokenCookie(Session),商品資訊(金額、名稱)可依靠product_id進行取得,sub_totalorder_amount一定要由後端這邊去運算,而不是用戶端算完後傳回給後端處理。

若直接相信用戶端傳遞的所有資訊,則會造成0元訂單、偽造使用者等問題,進而造成了許多營業上的損失。

上述的JSON結構建議改成下列內容來做處理,Access-Token則放在HTTP Headers裡

{
    "shopcart_list": [
        {
            "product_id": 1,
            "quantity": 5
        },
        {
            "product_id": 2,
            "quantity": 5
        }
    ]
}

別依靠HTTPS或框架來確保安全

若API設計有不安全的地方應該直接改進及修正,別認為使用HTTPS就一定安全,HTTPS僅確保資料從用戶端傳遞到伺服器端過程中,是加密過的,不被輕易解讀、破解而已。前面提到只要透過瀏覽器的開發者工具或反組譯程式碼,即使沒有透過Wireshark監聽封包,只要花點時間、耐心進行測試,也可了解API的運作及漏洞。

SQL 敘述句請在伺服器端產生

拜託!絕對不要讓用戶端來產生SQL敘述句,不管你資料庫是否有針對不同的資料庫使用者做SQL敘述句的限制,都不要讓用戶端來幫你組成SQL敘述句,也不要讓用戶端直接對伺服器的資料庫直接進行處理,能不讓用戶端的使用者知道你的資料庫架構是最好的!但重點不在這,有心人士只要稍微使用瀏覽器的開發者工具或反組譯應用程式回原始碼,再觀看一下每個連線的資料傳遞,也可以偽照SQL敘述句送去給伺服器做處理。

舉個例子,這是原本的SQL敘述句:

DELETE FROM `todo_list` WHERE `todo_list`.`id` = 1

而有心人士改成下述結果後重新送出請求(Request)

DELETE FROM `todo_list` WHERE `todo_list`.`id` >= 1

那這樣一定會造成伺服器天下大亂...

理想的做法應該是針對不同功能設計對應的API,所有的資料使用JSON、XML、Query String等格式傳遞。

針對不同功能設計對應的API,別丟給前端幫你

我們用PHP Laravel Resource Controllers來做一下說明,依照裡面的操作即可幫我們快速產生對該資料表的CRUD等行為,這樣的功能,省了我們開發中許多的時間。(P.S. 個人開發後端時完全不用這樣的機制自動產生API)

問題來了,將常用的資料表都產生出Resource Controller就完成了嗎?若開發這麼單純,我們也不需要特別分割出前後端工程師了,在前言中提到了今天登入的是使用者A,卻能修改使用者B的密碼這樣的事情就會發生,若要透過範本產生程式碼,中間的每個檢查過程還是不能漏掉。

除此之外,流程串接後端也要協助開發,針對不同功能設計對應的API。舉個例子,若使用者今天按下了一個訂單送出按鈕,這個功能過程中可能經歷了多張資料表的異動。雖然我們事先開好了針對不同資料表的CRUD的API,但我們不可能讓用戶端做一個功能呼叫了好幾個API來對資料庫做處理,這樣其實跟前端自己產生SQL的做法雷同,在設計上是相當不好的。除此之外,每一次的呼叫API若需檢查使用者權限等,不但佔用了連線數也影響了伺服器的效能。

筆者的做法是除了定義好API的規格外,若可呼叫一個API完成用戶端的一個動作,就該讓用戶端開發這邊流程簡單點,用戶端只需傳遞Access TokenCookie及資料(JSON)給伺服器端,在一次HTTP連線內做完事情給用戶端。

別傳不必要的資訊給用戶端

通常我們開發API的過程中,會搭配ORM、MVC等框架來實作,在開發過程中使用ORM輕鬆取得資料,透過MVC的框架來完成流程是相當方便的。然而在開發中,還是有些注意事項盡可能避免是最好的。

下列說明主要針對安全性與傳輸流量做說明。

  • 若資料為多筆(陣列),先過濾畫面上不必要呈現的欄位與不會用到的欄位,再將資料輸出,可省流量與效能。
  • 別一次將資料表裡的所有資料送出去,可透過分頁 Pagerization機制來實作,可省流量與效能。
  • 敏感性資料誤輸出,以聊天App為例,輸出的JSON,應避免掉其他使用者的密碼、電話、GPS等敏感資訊。

結論

永遠不要相信來自用戶端的資料

不要為了一時方便,而設計出不當的API

一時的偷懶,可能造成技術債倍數提升

別過於依靠框架