首先,寫一個(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):
- 服務(wù)器通過(guò)
Set-Cookie響應(yīng)頭設(shè)置Cookie - 瀏覽器得到Cookie之后,每次請(qǐng)求都要帶上Cookie
- 服務(wù)器讀取Cookie就知道登錄用戶的信息
問(wèn)題:
- 我在Chrome登錄得到了Cookie,用Safari訪問(wèn),Safari會(huì)帶上Cookie嗎?
no - Cookie存在哪?
Window存在C盤的一個(gè)文件里,其他系統(tǒng)也都存在硬盤的一個(gè)文件里。 - Cookie能作假嗎?
可以。Chrome開發(fā)者工具Application-Cookies就可以改。所以Cookie是不安全的。HttpOnly可以限制。 - 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é)一下流程:
- 用戶打開sign_up注冊(cè),向服務(wù)器發(fā)送post請(qǐng)求,發(fā)送email,password,password_confirmation;
- 服務(wù)器驗(yàn)證通過(guò),把用戶信息寫進(jìn)數(shù)據(jù)庫(kù),并告訴用戶注冊(cè)成功;
- 用戶打開sign_in登錄,發(fā)送post請(qǐng)求,發(fā)送email和password;
- 服務(wù)器認(rèn)證通過(guò),Set-Cookie;
- 用戶帶著Cookie打開首頁(yè),發(fā)送get請(qǐng)求;
- 服務(wù)器讀取Cookie,從Cookie中得到email,根據(jù)email從數(shù)據(jù)庫(kù)找到匹配的用戶信息寫入頁(yè)面;
- 這樣登錄的用戶進(jìn)入首頁(yè)時(shí)可以看到自己的信息;
- 沒(méi)有登錄的訪客進(jìn)入首頁(yè)看到不同的頁(yè)面。
完