Cookie與登錄注冊(cè)

首先,寫一個(gè)簡(jiǎn)單的注冊(cè)頁(yè)面sign_up.html(前端)

    <div class="form-wrapper">
        <h1>注冊(cè)</h1>
        <form>
            <div class="row">
                <label>郵箱</label>
                <input type="text" name="emali">
                <span class="error"></span>
            </div>
            <div class="row">
                <label>密碼</label>
                <input type="text" name="password">
                <span class="error"></span>
            </div>
            <div class="row">
                <label>確認(rèn)密碼</label>
                <input type="text" name="password_confirmation">
                <span class="error"></span>
            </div>
            <input type="submit" value="注冊(cè)">
        </form>
    </div>

把用戶輸入的內(nèi)容放到一個(gè)哈希中(前端)

        let $form = $('#signUpForm')
        let hash = {}
        $form.on('submit', (e) => {
            e.preventDefault()
            let need = ['email', 'password', 'password_confirmation']
            need.forEach( (name) => {
                let value = $form.find(`[name = ${name}]`).val()
                hash[name] = value
            })
        })  //得到的hash為 {email: "xx", password: "xx", password_confirmation: "xx"}

給server.js里添加一個(gè)路由,如果請(qǐng)求路徑是/sign_up,就顯示當(dāng)前目錄下的sign_up.html文件(后端)

 if(path === '/sign_up'){
    let string = fs.readFileSync('./sign_up.html', 'utf8')
    response.statusCode = 200
    response.setHeader('Content-Type', 'text/html;charset=utf-8')
    response.write(string)
    response.end()
 }

node server 8080 打開server,訪問(wèn)http://localhost:8080/sign_up就可以看到我們寫的注冊(cè)頁(yè)面。
嘗試發(fā)送一個(gè)Ajax post請(qǐng)求:jQuery.post() (前端)

$.post('/sign_up', hash)
      .then( (response) => {
          console.log(response) //得到一串符合html格式的字符串
      }, () => {
          console.log(‘error’)
      })

打印出來(lái)一個(gè)html格式的字符串,因?yàn)閟erver.js中寫明了只要請(qǐng)求路徑是/sign_up就表示請(qǐng)求成功,返回字符串。

由于這是一個(gè)post請(qǐng)求,如果在路由里將請(qǐng)求方式的限制為get,則會(huì)打印出‘error’:(后端)

 if(path === '/sign_up' && method === 'GET'){
    let string = fs.readFileSync('./sign_up.html', 'utf8')
    response.statusCode = 200
    response.setHeader('Content-Type', 'text/html;charset=utf-8')
    response.write(string)
    response.end()
 }

所以要再添加一個(gè)請(qǐng)求方式為post的路由:(后端)

 if(path === '/sign_up' && method === 'POST'){
    readBody(request).then( (body) => {
        response.statusCode = 200
        response.end()
    })    
 } //用戶post到/sign_up, 服務(wù)器讀取到請(qǐng)求的第四部分(Form Data),得到字符串'email=1&password=2&password_confirmation=3'

//由于請(qǐng)求的第四部分(Form Data)是分段上傳的,Node.js無(wú)法直接讀到其內(nèi)容,封裝以下函數(shù):
//作用是獲取請(qǐng)求第四部分?jǐn)?shù)據(jù),并返回一個(gè)Promise對(duì)象
function readBody(request){
  return new Promise((resolve, reject)=>{
    let body = []
    request.on('data', (chunk) => {
      body.push(chunk);
    }).on('end', () => {
      body = Buffer.concat(body).toString();
      resolve(body)
    })
  })
}

現(xiàn)在用戶向sign_up發(fā)送post請(qǐng)求,服務(wù)器得到的請(qǐng)求的第四部分是一個(gè)字符串'email=1&password=2&password_confirmation=3',使用split()方法將其變成哈希:(后端)

let strings = body.split('&') // ['email=1', 'password=2', 'password_confirmation=3']
let hash = {}
strings.forEach((string) => {
    let parts = string.split('=') // ['email', '1']
    let key = parts[0]
    let value = parts[1]
    hash[key] = (value) // hash['email'] = '1'
})

以上所做的事情總結(jié)起來(lái)就是:
客戶端收集form表單內(nèi)容到一個(gè)hash中,作為一個(gè)字符串傳給服務(wù)端,
服務(wù)端以一個(gè)字符串的形式拿到表單內(nèi)容,
再將其還原為一個(gè)hash。

這就是前端向后臺(tái)傳數(shù)據(jù)的方式:前端把所有東西變成一個(gè)字符串傳給后臺(tái),后臺(tái)從字符串里按照所要的結(jié)構(gòu)解析出來(lái)。

現(xiàn)在服務(wù)器有了一個(gè)包含表單信息的hash,繼續(xù)進(jìn)行驗(yàn)證:(后臺(tái))

let {email, password, password_confirmation} = hash
if(email.indexOf('@') === -1){ //驗(yàn)證用戶輸入的郵箱中是否有@
    response.statusCode = 400
    response.setHeader('Content-Type', 'application/json;charset=utf-8') //jQuery只要發(fā)現(xiàn)響應(yīng)中說(shuō)了這是json就會(huì)自動(dòng)JSON.parse()
    response.write(`{
        "errors": {
            "email": "invalid"
        }
    }`)
}else if(password !== password_confirmation){ //驗(yàn)證兩次輸入的密碼是否匹配
    response.statusCode = 400
    response.write('password not match')
}else{
    response.statusCode = 200
}

(前端)

$.post('/sign_up', hash)
                .then( (response) => {
                    console.log(response)
                }, (request) => {
                    let {errors} = request.responseJSON
                    if(errors.email && errors.email === 'invalid'){
                        $form.find('[name = "email"]').siblings('.error')
                            .text('郵箱格式錯(cuò)誤')
                    }
                })

前端也可以在發(fā)起請(qǐng)求之前進(jìn)行一些驗(yàn)證:(前端)jQuery.each()

            $form.find('.error').each( (index, span) => {
                $(span).text('')
            })
            if(hash['email'] === ''){
                $form.find('[name = "email"]').siblings('.error')
                    .text('填郵箱呀同學(xué)')
                return
            }
            if(hash['password'] === ''){
                $form.find('[name = "password"]').siblings('.error')
                    .text('設(shè)密碼呀同學(xué)')
                return
            }
            if(hash['password_confirmation'] === ''){
                $form.find('[name = "password_confirmation"]').siblings('.error')
                    .text('驗(yàn)密碼呀同學(xué)')
                return
            }
            if(hash['password'] !== hash['password_confirmation']){
                $form.find('[name = "password_confirmation"]').siblings('.error')
                    .text('密碼不匹配')
                return
            }

現(xiàn)在,我們實(shí)現(xiàn)的功能是郵箱、密碼、驗(yàn)證密碼必填,密碼與驗(yàn)證密碼相同(前端驗(yàn)證),郵箱中必須有'@'(后端驗(yàn)證)。
由于前后端代碼都是JS,所以后端的驗(yàn)證前端都能做到。但是前端可以不驗(yàn),后端必須要驗(yàn)。例如用crul發(fā)請(qǐng)求的話,可以直接與服務(wù)器交流。所以不能依賴瀏覽器上的JS,必須確保后端是安全的。

驗(yàn)證成功之后,服務(wù)器需要將得到的信息保存到數(shù)據(jù)庫(kù)中。這里我們?cè)诋?dāng)前目錄下創(chuàng)建db/users文件作為數(shù)據(jù)庫(kù),初始化為一個(gè)空數(shù)組[]。(后端)

var users = fs.readFileSync('./db/users', 'utf8')
try{
  users = JSON.parse(users) // 如果users不符合JSON語(yǔ)法就放棄它,把它變成空數(shù)組
}catch(exception){
  users = []
}
users.push({email: email, password: password})
var usersString = JSON.stringify(users) //對(duì)象不能直接存,將其變成字符串
fs.writeFileSync('./db/users', usersString)
response.statusCode = 200

現(xiàn)在有個(gè)問(wèn)題,同一個(gè)郵箱可以多次注冊(cè)。服務(wù)器可以在將信息存入數(shù)據(jù)庫(kù)之前驗(yàn)證郵箱是否已注冊(cè):(后端)

var users = fs.readFileSync('./db/users', 'utf8')
try {
    users = JSON.parse(users) // 如果users不符合JSON語(yǔ)法就放棄它,把它變成空數(shù)組
} catch (exception) {
    users = []
}
let inUse = false
for(let i=0; i<users.length; i++){
    let user = users[i]
    if(user.email = email){
        inUse = true
        break
    }
}
if(inUse){
    response.statusCode = 400
    response.write('email in use')
}else{
    users.push({ email: email, password: password })
    var usersString = JSON.stringify(users) //對(duì)象不能直接存,將其變成字符串
    fs.writeFileSync('./db/users', usersString)
    response.statusCode = 200
}

至此注冊(cè)過(guò)程完成。接著來(lái)做登錄功能。
首先寫一個(gè)登錄頁(yè)面sign_in.html:(前端)

<body>
    <div class="form-wrapper">
        <h1>登錄</h1>
        <form id="signInForm">
            <div class="row">
                <label>郵箱</label>
                <input type="text" name="email">
                <span class="error"></span>
            </div>
            <div class="row">
                <label>密碼</label>
                <input type="password" name="password">
                <span class="error"></span>
            </div>
            <input type="submit" value="登錄">
        </form>
    </div>
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
    <script>
        let $form = $('#signInForm')
        let hash = {}
        $form.on('submit', (e) => {
            e.preventDefault()
            let need = ['email', 'password']
            need.forEach( (name) => {
                let value = $form.find(`[name = ${name}]`).val()
                hash[name] = value
            })
            $form.find('.error').each( (index, span) => {
                $(span).text('')
            })
            if(hash['email'] === ''){
                $form.find('[name = "email"]').siblings('.error')
                    .text('填郵箱呀同學(xué)')
                return
            }
            if(hash['password'] === ''){
                $form.find('[name = "password"]').siblings('.error')
                    .text('填密碼呀同學(xué)')
                return
            }

            $.post('/sign_in', hash)
                .then( (response) => {
                    console.log(response)
                }, (request) => {
                    let {errors} = request.responseJSON
                    if(errors.email && errors.email === 'invalid'){
                        $form.find('[name = "email"]').siblings('.error')
                            .text('郵箱格式錯(cuò)誤')
                    }
                })
        })
    </script>
</body>

在服務(wù)器上給sign_in.html寫一個(gè)路由:(后端)

    else if (path === '/sign_in' && method === 'GET') {
        let string = fs.readFileSync('./sign_in.html', 'utf8')
        response.statusCode = 200
        response.setHeader('Content-Type', 'text/html;charset=utf-8')
        response.write(string)
        response.end()
    } else if (path === '/sign_in' && method === 'POST') {
        readBody(request).then((body) => {
            let strings = body.split('&') // ['email=1', 'password=2']
            let hash = {}
            strings.forEach((string) => {
                let parts = string.split('=') // ['email', '1']
                let key = parts[0]
                let value = parts[1]
                hash[key] = decodeURIComponent(value) // hash['email'] = '1'
            })
            
            response.end()
        })
    } 

登錄頁(yè)面與注冊(cè)頁(yè)面相似,客戶端將表單信息收集到一個(gè)哈希中,在發(fā)起請(qǐng)求時(shí)傳一個(gè)字符串給后端;后端將得到的字符串解析為哈希。

后端得到包含email和password的哈希后,與數(shù)據(jù)庫(kù)進(jìn)行對(duì)比(看數(shù)據(jù)庫(kù)里有沒(méi)有一樣的email和password),認(rèn)證用戶:(后端)

            let { email, password } = hash
            var users = fs.readFileSync('./db/users', 'utf8')
            try {
                users = JSON.parse(users) // []
            } catch (exception) {
                users = []
            }
            let found
            for (let i = 0; i < users.length; i++) {
                if (users[i].email === email && users[i].password === password) {
                    found = true
                    break
                }
            }
            if (found) {
                response.statusCode = 200
            } else {
                response.statusCode = 401
            }

登錄成功時(shí)跳轉(zhuǎn)到首頁(yè):(前端)

$.post('/sign_in', hash)
                .then( (response) => {
                    window.location.href = '/'
                }, (request) => {
                    alert('郵箱與密碼不匹配')
                })

現(xiàn)在有一個(gè)問(wèn)題:即使沒(méi)有登錄,用戶也可以直接訪問(wèn)首頁(yè),與登錄后看到的頁(yè)面相同。
這里我們需要用到Cookies。HTTP Set-Cookie
服務(wù)器在認(rèn)證用戶成功后,給返回的響應(yīng)頭中添加Set-Cookie:(后端)

if (found) {
    response.setHeader('Set-Cookie', `sign_in_email=${email}`)
    response.statusCode = 200 
} 

選中開發(fā)者工具的Preserve log,再次登錄。
我們可以看到,在向sign_in發(fā)起請(qǐng)求時(shí),得到的響應(yīng)頭中有一項(xiàng)Set-Cookie: sign_in_email=1@luke.com,而請(qǐng)求成功后接著對(duì)主頁(yè)發(fā)起的請(qǐng)求的請(qǐng)求頭中帶著這個(gè)Cookie:Cookie: sign_in_email=1@luke.com。

這就是Cookie的作用:服務(wù)器響應(yīng)中給頁(yè)面發(fā)送一個(gè)Cookie,之后同源發(fā)起的請(qǐng)求都帶著這個(gè)Cookie作為識(shí)別。
理解:第一次進(jìn)公園時(shí)售票員給你兩天的票(Set-Cookie),票就是Cookie,兩天內(nèi)可以多次進(jìn)入公園,每次都要帶著票給售票員看。

Cookie特點(diǎn):

  1. 服務(wù)器通過(guò)Set-Cookie響應(yīng)頭設(shè)置Cookie
  2. 瀏覽器得到Cookie之后,每次請(qǐng)求都要帶上Cookie
  3. 服務(wù)器讀取Cookie就知道登錄用戶的信息

問(wèn)題:

  1. 我在Chrome登錄得到了Cookie,用Safari訪問(wèn),Safari會(huì)帶上Cookie嗎?
    no
  2. Cookie存在哪?
    Window存在C盤的一個(gè)文件里,其他系統(tǒng)也都存在硬盤的一個(gè)文件里。
  3. Cookie能作假嗎?
    可以。Chrome開發(fā)者工具Application-Cookies就可以改。所以Cookie是不安全的。HttpOnly可以限制。
  4. Cookie有有效期嗎:
    默認(rèn)有效期20分鐘左右,由瀏覽器決定。后端可以強(qiáng)制設(shè)置有效期。

現(xiàn)在我們讓登錄的用戶在跳轉(zhuǎn)到首頁(yè)時(shí)可以看到自己的密碼:(后臺(tái))

if (path === '/') {
        let string = fs.readFileSync('./index.html', 'utf8')
        
        //從cookies里拿到用戶的email
        let cookies = request.headers.cookie.split('; ')   // ['email=1@', 'a=1', 'b=2']
        let hash = {}
        for (let i = 0; i < cookies.length; i++) {
            let parts = cookies[i].split('=')
            let key = parts[0]
            let value = parts[1]
            hash[key] = value
        }
        let email = hash.sign_in_email
       
        //遍歷數(shù)據(jù)庫(kù),找到與cookie里email匹配的用戶信息
        let users = fs.readFileSync('./db/users', 'utf8')
        users = JSON.parse(users)
        let foundUser
        for (let i = 0; i < users.length; i++) {
            if (users[i].email === email) {
                foundUser = users[i]
                break
            }
        }
        
        //如果找到用戶信息,將用戶的密碼顯示在頁(yè)面中
        if (foundUser) {
            string = string.replace('__password__', foundUser.password)
        } else {
            string = string.replace('__password__', '不知道')
        }
        response.statusCode = 200
        response.setHeader('Content-Type', 'text/html;charset=utf-8')
        response.write(string)
        response.end()
    }

index.html:

<body>
    <h1>你的密碼是:'__password__'</h1>
</body>

總結(jié)一下流程:

  1. 用戶打開sign_up注冊(cè),向服務(wù)器發(fā)送post請(qǐng)求,發(fā)送email,password,password_confirmation;
  2. 服務(wù)器驗(yàn)證通過(guò),把用戶信息寫進(jìn)數(shù)據(jù)庫(kù),并告訴用戶注冊(cè)成功;
  3. 用戶打開sign_in登錄,發(fā)送post請(qǐng)求,發(fā)送email和password;
  4. 服務(wù)器認(rèn)證通過(guò),Set-Cookie;
  5. 用戶帶著Cookie打開首頁(yè),發(fā)送get請(qǐng)求;
  6. 服務(wù)器讀取Cookie,從Cookie中得到email,根據(jù)email從數(shù)據(jù)庫(kù)找到匹配的用戶信息寫入頁(yè)面;
  7. 這樣登錄的用戶進(jìn)入首頁(yè)時(shí)可以看到自己的信息;
  8. 沒(méi)有登錄的訪客進(jìn)入首頁(yè)看到不同的頁(yè)面。

完整代碼

最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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