(Web安全與防御通俗基礎(chǔ) 四) 前后臺(tái)數(shù)據(jù)交互 同源策略 為什么http請(qǐng)求是用戶(hù)可干預(yù)的 session機(jī)制

前述總結(jié)

通過(guò)前述文章,我們目前手頭的東西有些零散,我們有:一套html頁(yè)面、一個(gè)nginx下的html頁(yè)面站點(diǎn)、一個(gè)python制作的后臺(tái)服務(wù)站點(diǎn),還有一些其他雜七雜八的東西。就像前述文章所說(shuō):

  1. html文檔站點(diǎn)是通過(guò)nginx的方式組建的,他的地址是http://127.0.0.1:8023/,目前我們有一個(gè)demo1.html頁(yè)面。又如同我們之前所說(shuō),我們把頁(yè)面也叫做前臺(tái)。
  2. python站點(diǎn)是使用python語(yǔ)言編寫(xiě)的,當(dāng)然使用了pythonflask包,他的地址是http://127.0.0.1:8120/。目前里面至少有一個(gè)登錄方法。我們把python這套程序,也叫做后臺(tái)。

在真實(shí)情況下,前臺(tái)與后臺(tái)都是相互配合的。前臺(tái)為用戶(hù)提供的是畫(huà)面、是可展示的東西;而后臺(tái),則為前臺(tái)的正常運(yùn)行,提供了數(shù)據(jù)、邏輯、算力等方面的支持。就我們目前的程序而言,我們?nèi)绾巫屒芭_(tái)可以獲取后臺(tái)的數(shù)據(jù)呢?當(dāng)然是通過(guò)http協(xié)議。前臺(tái)向后臺(tái)發(fā)送http請(qǐng)求,并根據(jù)后臺(tái)的響應(yīng),做出自己的動(dòng)作。

本文中,我們將會(huì)做起前后臺(tái)的數(shù)據(jù)交互,并通過(guò)這個(gè)過(guò)程,簡(jiǎn)單理解同源策略、為什么http請(qǐng)求是用戶(hù)可干預(yù)的以及session機(jī)制

一 前臺(tái)的ajax請(qǐng)求

其實(shí)在瀏覽器上,你所請(qǐng)求的圖片、頁(yè)面、js和css等。都是http請(qǐng)求,只不過(guò)這些請(qǐng)求是無(wú)法實(shí)現(xiàn)動(dòng)態(tài)刷新的,也就是需要跟隨著整個(gè)頁(yè)面一起去做請(qǐng)求。頁(yè)面完成http請(qǐng)求,有兩種方式:

  1. 進(jìn)行頁(yè)面跳轉(zhuǎn)(相當(dāng)于整頁(yè)刷新),在跳轉(zhuǎn)時(shí)完成新的請(qǐng)求。
  2. 不依賴(lài)于整個(gè)頁(yè)面的刷新,只進(jìn)行局部的動(dòng)作。當(dāng)然并不代表請(qǐng)求完了之后不會(huì)做頁(yè)面的跳轉(zhuǎn)--XMLHttpRequest

我們今天要做的,是第二種,局部刷新請(qǐng)求(基于XMLHttpRequest的ajax)。我們手動(dòng)實(shí)現(xiàn)這個(gè)功能會(huì)有很多代碼,所以借助一個(gè)別人已經(jīng)封裝好的庫(kù)去完成:jquery。當(dāng)然它不止能做ajax。
需要首先引入這個(gè)包,之后基本的用法如下

<script src="./js/jquery.js"></script>
<script>
$.ajax({
        url:"請(qǐng)求目標(biāo)(URI)",
        data:"要傳遞的參數(shù)",
        method:"http方法(POST GET)",
        success:function(data){
            "請(qǐng)求成功后,需要進(jìn)行的處理"
        }
    })
</script>

回顧一下上篇中,我們調(diào)用python服務(wù)的登錄demo時(shí)發(fā)送的POST報(bào)文。

POST /data/login HTTP/1.1
Host: 127.0.0.1:8120
Content-Type: application/x-www-form-urlencoded
Content-Length: 31

username=admin&password=a123456

核心有四點(diǎn):

  1. 使用POST方式
  2. URI是:/data/login
  3. 服務(wù)器地址:127.0.0.1:8120
  4. 傳遞兩個(gè)參數(shù):usernamepassword

參照我們上面說(shuō)的ajax的用法,實(shí)現(xiàn)用戶(hù)登錄我們可以組織的代碼如下:

// 獲取用戶(hù)輸入的數(shù)據(jù)
var usname=document.querySelector("#usname").value;
var password=document.querySelector("#password").value;
// 發(fā)起ajax請(qǐng)求
$.ajax({
        url:"http://127.0.0.1:8120/data/login",
        data:{"username":usname,"password":password},
        method:"POST",
        success:function(data){
            // 請(qǐng)求成功后,彈出響應(yīng)信息
            alert(data)
        }
    })

HTML頁(yè)面中,依賴(lài)jqueryhttp請(qǐng)求,大概如此。但當(dāng)我們真正去這樣運(yùn)行時(shí),會(huì)發(fā)現(xiàn)無(wú)法收到響應(yīng)。

無(wú)響應(yīng)信息

這是因?yàn)橐环N安全策略--同源策略。

二 同源策略

同源策略,是瀏覽器為了確保網(wǎng)頁(yè)數(shù)據(jù)安全,執(zhí)行的一種安全機(jī)制。


同源策略

所謂同源,即同域名(ip)、同協(xié)議、同端口。當(dāng)兩個(gè)頁(yè)面具有相同的以上三種信息時(shí),瀏覽器則認(rèn)為其是同源(同域)的;當(dāng)有一項(xiàng)不同,則認(rèn)為其是不同源(跨域)的。對(duì)于不同源的項(xiàng),瀏覽器會(huì)做出一些限制,比如:

  1. 禁止不同源的網(wǎng)頁(yè),相互之間操作彼此的DOM結(jié)構(gòu);
  2. 禁止不同源的網(wǎng)頁(yè),相互之間訪問(wèn)彼此的頁(yè)面存儲(chǔ);
  3. 禁止不同源的網(wǎng)頁(yè)(站),發(fā)起XMLHttpRequest(ajax請(qǐng)求)。

上述案例中,我們的頁(yè)面是127.0.0.1:8023下的,而試圖對(duì)127.0.0.1:8120發(fā)起ajax請(qǐng)求。顯然端口不同,違反了同源策略的。要讓網(wǎng)頁(yè)可以調(diào)取到后臺(tái)數(shù)據(jù),有兩個(gè)解決方案:

  1. 讓被請(qǐng)求站點(diǎn)127.0.0.1:8120告訴瀏覽器,同意接收非同源頁(yè)面發(fā)起的請(qǐng)求,我們叫做跨域訪問(wèn)。
  2. 讓被請(qǐng)求站點(diǎn)127.0.0.1:8120和發(fā)起請(qǐng)求的網(wǎng)頁(yè)127.0.0.1:8023位于同一個(gè)站點(diǎn)下。

我們下面將使用第二種方案,將兩個(gè)站點(diǎn)整合為一個(gè)。如果使用第一種方案,將有可能造成“跨站點(diǎn)請(qǐng)求偽造(CSRF)”攻擊,后續(xù)我們會(huì)對(duì)此進(jìn)行講解。

三 利用nginx整合站點(diǎn)

前述文章中,我們使用nginx讓網(wǎng)頁(yè)“跑了起來(lái)”,現(xiàn)在,我們還需要借助nginx去進(jìn)行兩個(gè)站點(diǎn)的整合。
依然需要打開(kāi)nginx的配置文件./nginx.conf,以下是我們之前的配置內(nèi)容

server {
        listen       8023;
        server_name  localhost;
        location / {
            root D:/html5_code_injection/;
        }
    }

現(xiàn)在,我們?cè)谠瓉?lái)的基礎(chǔ)上,擴(kuò)展一個(gè)路徑的配置,并將其映射到我們的 Python服務(wù)127.0.0.1:8120

server {
        listen       8023;
        server_name  localhost;
        location / {
            root D:/html5_code_injection/;
        }
        location /data/
        {
            proxy_pass   http://127.0.0.1:8120;
        }
    }

這樣,所有經(jīng)過(guò)127.0.0.1:8023指向/data/........的請(qǐng)求,均會(huì)被轉(zhuǎn)發(fā)到127.0.0.1:8120。我們的站點(diǎn)結(jié)構(gòu)變成了如下所示:

整合后的網(wǎng)站結(jié)構(gòu)

web請(qǐng)求統(tǒng)一發(fā)送到8023端口,nginx會(huì)將所有請(qǐng)求默認(rèn)轉(zhuǎn)到HTML文檔的文件夾;URI/data開(kāi)頭的請(qǐng)求,則會(huì)被轉(zhuǎn)發(fā)到8120端口,也即python后臺(tái)。
這樣,我們剛才在頁(yè)面上實(shí)現(xiàn)的js代碼,也需要進(jìn)行調(diào)整。將url部分改成本站點(diǎn)的相對(duì)路徑

修改前后對(duì)比

最終,我們完整版的,js端的登錄請(qǐng)求方法如下

function login()
{
    // 獲取用戶(hù)輸入的用戶(hù)名及密碼
    var usname=document.querySelector("#usname").value;
    var password=document.querySelector("#password").value;
    // 進(jìn)行本地存儲(chǔ),這一步可以略過(guò)
    localStorage.setItem("usname",usname);
    localStorage.setItem("password",password);
    // 向后臺(tái)發(fā)送請(qǐng)求,驗(yàn)證用戶(hù)名和密碼是否正確
    $.ajax({
        url:"/data/login",
        data:{username:usname,password:password},
        method:"POST",
        success:function(data){
            alert(data);
        },
        error:function(data)
        {
            console.log(data);
        }
    })
}

從頁(yè)面上,已經(jīng)可以操作登錄


登錄成功

四 如何證明“你是你”

通過(guò)前述的一系列操作,我們已經(jīng)完成了一個(gè)通過(guò)前后臺(tái)互動(dòng),實(shí)現(xiàn)的登錄功能;甚至通過(guò)文件,實(shí)現(xiàn)了對(duì)“用戶(hù)名”和“密碼”的存儲(chǔ)。但接下來(lái),我們就會(huì)面臨另一個(gè)問(wèn)題:“如何證明"你是你"”。
為了體驗(yàn)這個(gè)問(wèn)題,我們需要把我們的程序添加一個(gè)新的功能:登錄請(qǐng)求完成后,我們要通過(guò)另一個(gè)請(qǐng)求,獲取登錄者的個(gè)人信息。在進(jìn)行這項(xiàng)體驗(yàn)的過(guò)程中,我們也會(huì)明白,為什么說(shuō)web請(qǐng)求不可靠

下面是我們的步驟:

  1. 創(chuàng)建兩個(gè)文件,用于存儲(chǔ)兩個(gè)用戶(hù)的信息,一個(gè)為admin.dat另一個(gè)為xiaohong.dat。格式內(nèi)容都可以隨意,我所存入的信息:
    admin.dat的內(nèi)容為:

    {
     "name":"小明",
     "age": 12
    }
    

    xiaohong.dat的內(nèi)容為:

    {
     "name":"小紅",
     "age": 11
    }
    
  2. 我們需要在前臺(tái)頁(yè)面上,添加一個(gè)button,當(dāng)被點(diǎn)下時(shí),調(diào)用后臺(tái)的方法,獲取登錄用戶(hù)的信息

    頁(yè)面添加按鈕

  3. 我們需要在后臺(tái)python程序中,添加一個(gè)接口,供前臺(tái)調(diào)用,這個(gè)接口的作用,就是讀取上面我們兩個(gè)用戶(hù)文件中的數(shù)據(jù),之后返回給前臺(tái)。

\color{red}{從后臺(tái)python程序的角度,試想一下有什么問(wèn)題?}
我們有兩個(gè)用戶(hù)文件:admin.datxiaohong.dat,如果python程序的代碼如下,應(yīng)該怎么確定要打開(kāi)哪個(gè)文件去讀取信息呢?

python代碼

這個(gè)問(wèn)題的解決,也有兩種途徑:

  1. 用戶(hù)是在前臺(tái)登錄的,前臺(tái)自然可以記錄下用戶(hù)名,之后獲取信息按鈕點(diǎn)擊后,前臺(tái)在發(fā)起的ajax請(qǐng)求中,添加入用戶(hù)名的信息。相當(dāng)于你每次來(lái)拜訪我,都提供一個(gè)名片,我每次根據(jù)名片知道你是誰(shuí)。
    前臺(tái)記錄方式示意
  2. 用戶(hù)登錄是需要經(jīng)過(guò)后臺(tái)認(rèn)證的,當(dāng)后臺(tái)認(rèn)證通過(guò)后,通過(guò)某種形式,將用戶(hù)名記錄在后臺(tái)程序范圍內(nèi),之后將所記錄的信息通過(guò)某種形式和真正的用戶(hù)(瀏覽器)進(jìn)行綁定。相當(dāng)于你把身份證復(fù)印件放我這,每次來(lái)我核對(duì)復(fù)印件。這種方式我們稍后詳細(xì)介紹。

這兩種方式,我們分別實(shí)現(xiàn)后進(jìn)行體驗(yàn)。

4.1 前臺(tái)記錄用戶(hù)信息方式

首先我們需要確認(rèn)前臺(tái)頁(yè)面(js)的登錄方法。確認(rèn)方法中使用了

localStorage.setItem("usname",usname);
localStorage.setItem("password",password);

這兩句的意思,是在頁(yè)面存儲(chǔ)中,存入用戶(hù)名和密碼。

js登錄方法

之后,需要在前臺(tái)頁(yè)面(js)新建一個(gè)獲取用戶(hù)信息的方法,這個(gè)方法需要:

  1. 讀取頁(yè)面存儲(chǔ)的用戶(hù)名(usname)
  2. 向后臺(tái)接口發(fā)起ajax請(qǐng)求,并且攜帶用戶(hù)名作為參數(shù)
  3. 顯示調(diào)取到的信息
    所以,這個(gè)方法的代碼我們定義為:
     function get_info()
     {
         // 讀取本地存儲(chǔ)中的usname
         unm=localStorage.getItem("usname");
         // 發(fā)起ajax請(qǐng)求
         $.ajax({
             url:"/data/info",
             // 參數(shù)攜帶了本地存儲(chǔ)的用戶(hù)名
             data:{uname:unm},
             method:"GET",
             success:function(data){
                 // 請(qǐng)求成功后,彈窗顯示響應(yīng)信息
                 alert(data);
             },
             error:function(data)
             {
                 console.log(data);
             }
         })
     }
    
  4. 需要在python端定義一個(gè)接口,接受http路徑為/data/infoGET請(qǐng)求,并且接受一個(gè)uname參數(shù),根據(jù)這個(gè)參數(shù)去讀取本地的用戶(hù)文件,所以,python的代碼如下:
     # 定義訪問(wèn)路徑為 /data/info 的get請(qǐng)求
     @app.route('/data/info',methods=['get'])
     def get_my_info():
         # 讀取 請(qǐng)求head 中的uname參數(shù)
         uname=request.args.get("uname")
         # 根據(jù)前臺(tái)傳遞的參數(shù),打開(kāi)本地用戶(hù)文件,需要使用UTF8方式打開(kāi),避免中文亂碼
         with open("./"+uname+".dat",encoding="utf8") as f:
             txt=f.read()
         # 返回讀取的內(nèi)容
         return txt
    

更改完成后,功能效果如下


正常流程

然而,我們說(shuō)\color{red}{用戶(hù)可以干預(yù)客戶(hù)與服務(wù)器之間傳送的所有數(shù)據(jù),服務(wù)器程序必須假設(shè)所有輸入的信息都是惡意的輸入}。
接下來(lái)我們演示一下前端傳參的不可靠性。
我們依然采用admin賬號(hào)登錄,正如上述所示,admin的姓名為小明,如果通過(guò)某些操作,可以獲取到服務(wù)器上的xiaohong賬號(hào)的信息,那么就可以看到前臺(tái)輸入的不可靠性。

用戶(hù)干預(yù)流程

所以這種流程是不可靠的,接下來(lái),我們來(lái)嘗試第二種方案,用python后臺(tái)程序記錄用戶(hù)信息。

4.2 session機(jī)制

4.2.1 為程序添加session機(jī)制

在web開(kāi)發(fā)過(guò)程中,提供了多種技術(shù),將服務(wù)端和客戶(hù)端“綁定”在一起,session就是其中一種。目前,你可以理解為這種機(jī)制是以瀏覽器為準(zhǔn)去判斷的;這種機(jī)制可以針對(duì)每個(gè)客戶(hù)端(瀏覽器)進(jìn)行身份綁定,也就是說(shuō):我用admin去登錄后,如果啟用了session機(jī)制,那么之后我在同一個(gè)瀏覽器里發(fā)送的每個(gè)請(qǐng)求,瀏覽器都可以自動(dòng)識(shí)別為是admin。
稍后我們會(huì)去簡(jiǎn)單介紹原理,目前我們先對(duì)程序進(jìn)行修改,添加入session機(jī)制后進(jìn)行體驗(yàn)。在上面的基礎(chǔ)上,我們最主要是需要對(duì)python后臺(tái)程序做修改:

  1. 修改python后臺(tái)的my_login()方法,加入session機(jī)制
    my_login方法的修改
  2. 修改python后臺(tái)的get_my_info()方法,將原來(lái)從request中獲取參數(shù)的部分,改為從session中提取用戶(hù)信息
    get_my_info方法的修改
  3. 前臺(tái)頁(yè)面(js)中,get_info()方法,不再傳送用戶(hù)名作為參數(shù)。當(dāng)然,不改也沒(méi)太大影響

經(jīng)過(guò)如此的修改,我們?cè)谶M(jìn)行信息獲取時(shí)就不會(huì)再通過(guò)頁(yè)面進(jìn)行參數(shù)傳遞,所以“不再存在”前面我們提到的問(wèn)題。之所以打上引號(hào),其實(shí)用戶(hù)還是可以進(jìn)行一定程度的干預(yù)的。我們先暫時(shí)不去考慮,簡(jiǎn)單介紹一下session機(jī)制是如何實(shí)現(xiàn)的。

4.2.2 session極簡(jiǎn)原理

通過(guò)信息獲取流程,我們首先來(lái)看看,與未加入session機(jī)制時(shí)相比,http通訊過(guò)程是否存在了什么不同。
這是未添加session機(jī)制時(shí)的請(qǐng)求頭

未添加session

這是添加session機(jī)制后的請(qǐng)求頭
添加session

我們可以看到,在請(qǐng)求頭里,多了一項(xiàng),叫Cookie,值是一串“亂七八糟”的數(shù)據(jù)(eyJ1c25hbWUiOiJhZG1pbiJ9.ZP6D6g.0yEMfRRLiMxwLoYviq05zumqQpA),我們暫時(shí)不去解釋這串?dāng)?shù)據(jù)的含義,目前我們可以把它理解為一串不會(huì)重復(fù)的隨機(jī)數(shù)。而我們的python后臺(tái)程序,其實(shí)就是通過(guò)這一串?dāng)?shù)據(jù)去識(shí)別瀏覽器的身份的,在后臺(tái)中,會(huì)將這一串?dāng)?shù)據(jù),與你存儲(chǔ)在session對(duì)象中的內(nèi)容進(jìn)行綁定。所以,雖然我們明面上沒(méi)有傳遞什么參數(shù),實(shí)際在request head里面,瀏覽器會(huì)悄悄給我們附帶上這一串?dāng)?shù)據(jù)。
那么,\color{red}{瀏覽器是怎么知道什么時(shí)候,設(shè)置什么字符串}的呢?上文中,我們初始加入session是在登錄過(guò)程中。我們來(lái)看一下加入session后,登錄方法的響應(yīng)頭
添加session功能 登錄方法的響應(yīng)頭

可以看到,響應(yīng)頭中有一個(gè)Set-Cookie項(xiàng),它后面對(duì)應(yīng)的,就是上面那串?dāng)?shù)。瀏覽器在識(shí)別到這個(gè)關(guān)鍵字后,就會(huì)保存下它后面的字符串,之后,會(huì)為\color{red}{同源}的所有請(qǐng)求,以Cookie的形式將這串字符串附帶到request head中。

因?yàn)槲覀兪窃诘卿涍^(guò)程中,向session中加入了用戶(hù)信息,所以在其他方法中去驗(yàn)證session中的信息,還可以起到驗(yàn)證用戶(hù)登錄的作用。如果用戶(hù)沒(méi)有登錄,那么session中肯定是沒(méi)有信息的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容