Cookie注册登录

cookies~

首先,写一个简单的注册页面sign_up.html(前端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div class="form-wrapper">
<h1>注册</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>确认密码</label>
<input type="text" name="password_confirmation">
<span class="error"></span>
</div>
<input type="submit" value="注册">
</form>
</div>

把用户输入的内容放到一个哈希中(前端)

1
2
3
4
5
6
7
8
9
10
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里添加一个路由,如果请求路径是/sign_up,就显示当前目录下的sign_up.html文件(后端)

1
2
3
4
5
6
7
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,访问http://localhost:8080/sign_up就可以看到我们写的注册页面。
尝试发送一个Ajax post请求:jQuery.post() (前端)

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

打印出来一个html格式的字符串,因为server.js中写明了只要请求路径是/sign_up就表示请求成功,返回字符串。

由于这是一个post请求,如果在路由里将请求方式的限制为get,则会打印出‘error’:(后端)

1
2
3
4
5
6
7
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()
}

所以要再添加一个请求方式为post的路由:(后端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 if(path === '/sign_up' && method === 'POST'){
readBody(request).then( (body) => {
response.statusCode = 200
response.end()
})
} //用户post到/sign_up, 服务器读取到请求的第四部分(Form Data),得到字符串'email=1&password=2&password_confirmation=3'

//由于请求的第四部分(Form Data)是分段上传的,Node.js无法直接读到其内容,封装以下函数:
//作用是获取请求第四部分数据,并返回一个Promise对象
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)
})
})
}

现在用户向sign_up发送post请求,服务器得到的请求的第四部分是一个字符串'email=1&password=2&password_confirmation=3',使用split()方法将其变成哈希:(后端)

1
2
3
4
5
6
7
8
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'
})

以上所做的事情总结起来就是:
客户端收集form表单内容到一个hash中,作为一个字符串传给服务端,
服务端以一个字符串的形式拿到表单内容,
再将其还原为一个hash。

这就是前端向后台传数据的方式:前端把所有东西变成一个字符串传给后台,后台从字符串里按照所要的结构解析出来。

现在服务器有了一个包含表单信息的hash,继续进行验证:(后台)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let {email, password, password_confirmation} = hash
if(email.indexOf('@') === -1){ //验证用户输入的邮箱中是否有@
response.statusCode = 400
response.setHeader('Content-Type', 'application/json;charset=utf-8') //jQuery只要发现响应中说了这是json就会自动JSON.parse()
response.write(`{
"errors": {
"email": "invalid"
}
}`)
}else if(password !== password_confirmation){ //验证两次输入的密码是否匹配
response.statusCode = 400
response.write('password not match')
}else{
response.statusCode = 200
}

(前端)

1
2
3
4
5
6
7
8
9
10
$.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('邮箱格式错误')
}
})

前端也可以在发起请求之前进行一些验证:(前端)jQuery.each()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$form.find('.error').each( (index, span) => {
$(span).text('')
})
if(hash['email'] === ''){
$form.find('[name = "email"]').siblings('.error')
.text('填邮箱呀同学')
return
}
if(hash['password'] === ''){
$form.find('[name = "password"]').siblings('.error')
.text('设密码呀同学')
return
}
if(hash['password_confirmation'] === ''){
$form.find('[name = "password_confirmation"]').siblings('.error')
.text('验密码呀同学')
return
}
if(hash['password'] !== hash['password_confirmation']){
$form.find('[name = "password_confirmation"]').siblings('.error')
.text('密码不匹配')
return
}

现在,我们实现的功能是邮箱、密码、验证密码必填,密码与验证密码相同(前端验证),邮箱中必须有‘@’(后端验证)。
由于前后端代码都是JS,所以后端的验证前端都能做到。但是前端可以不验,后端必须要验。例如用curl发请求的话,可以直接与服务器交流。所以不能依赖浏览器上的JS,必须确保后端是安全的。

验证成功之后,服务器需要将得到的信息保存到数据库中。这里我们在当前目录下创建db/users文件作为数据库,初始化为一个空数组[]。(后端)

1
2
3
4
5
6
7
8
9
10
var users = fs.readFileSync('./db/users', 'utf8')
try{
users = JSON.parse(users) // 如果users不符合JSON语法就放弃它,把它变成空数组
}catch(exception){
users = []
}
users.push({email: email, password: password})
var usersString = JSON.stringify(users) //对象不能直接存,将其变成字符串
fs.writeFileSync('./db/users', usersString)
response.statusCode = 200

现在有个问题,同一个邮箱可以多次注册。服务器可以在将信息存入数据库之前验证邮箱是否已注册:(后端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var users = fs.readFileSync('./db/users', 'utf8')
try {
users = JSON.parse(users) // 如果users不符合JSON语法就放弃它,把它变成空数组
} 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) //对象不能直接存,将其变成字符串
fs.writeFileSync('./db/users', usersString)
response.statusCode = 200
}

至此注册过程完成。接着来做登录功能。
首先写一个登录页面sign_in.html:(前端)

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
45
46
47
48
49
50
51
52
53
54
55
<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('填邮箱呀同学')
return
}
if(hash['password'] === ''){
$form.find('[name = "password"]').siblings('.error')
.text('填密码呀同学')
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('邮箱格式错误')
}
})
})
</script>
</body>

在服务器上给sign_in.html写一个路由:(后端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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()
})
}

登录页面与注册页面相似,客户端将表单信息收集到一个哈希中,在发起请求时传一个字符串给后端;后端将得到的字符串解析为哈希。

后端得到包含email和password的哈希后,与数据库进行对比(看数据库里有没有一样的email和password),认证用户:(后端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
}

登录成功时跳转到首页:(前端)

1
2
3
4
5
6
$.post('/sign_in', hash)
.then( (response) => {
window.location.href = '/'
}, (request) => {
alert('邮箱与密码不匹配')
})

现在有一个问题:即使没有登录,用户也可以直接访问首页,与登录后看到的页面相同。
这里我们需要用到CookiesHTTP Set-Cookie
服务器在认证用户成功后,给返回的响应头中添加Set-Cookie:(后端)

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

选中开发者工具的Preserve log,再次登录。
我们可以看到,在向sign_in发起请求时,得到的响应头中有一项Set-Cookie: sign_in_email=1@luke.com,而请求成功后接着对主页发起的请求的请求头中带着这个Cookie:Cookie: sign_in_email=1@luke.com

这就是Cookie的作用:服务器响应中给页面发送一个Cookie,之后同源发起的请求都带着这个Cookie作为识别。
理解:第一次进公园时售票员给你两天的票(Set-Cookie),票就是Cookie,两天内可以多次进入公园,每次都要带着票给售票员看。

Cookie特点:

  1. 服务器通过Set-Cookie响应头设置Cookie
  2. 浏览器得到Cookie之后,每次请求都要带上Cookie
  3. 服务器读取Cookie就知道登录用户的信息

问题:

  1. 我在Chrome登录得到了Cookie,用Safari访问,Safari会带上Cookie吗?
    no
  2. Cookie存在哪?
    Window存在C盘的一个文件里,其他系统也都存在硬盘的一个文件里。
  3. Cookie能作假吗?
    可以。Chrome开发者工具Application-Cookies就可以改。所以Cookie是不安全的。HttpOnly可以限制。
  4. Cookie有有效期吗:
    默认有效期20分钟左右,由浏览器决定。后端可以强制设置有效期。

现在我们让登录的用户在跳转到首页时可以看到自己的密码:(后台)

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
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

//遍历数据库,找到与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
}
}

//如果找到用户信息,将用户的密码显示在页面中
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:

1
2
3
<body>
<h1>你的密码是:'__password__'</h1>
</body>

总结一下流程:

  1. 用户打开sign_up注册,向服务器发送post请求,发送email,password,password_confirmation;
  2. 服务器验证通过,把用户信息写进数据库,并告诉用户注册成功;
  3. 用户打开sign_in登录,发送post请求,发送email和password;
  4. 服务器认证通过,Set-Cookie;
  5. 用户带着Cookie打开首页,发送get请求;
  6. 服务器读取Cookie,从Cookie中得到email,根据email从数据库找到匹配的用户信息写入页面;
  7. 这样登录的用户进入首页时可以看到自己的信息;
  8. 没有登录的访客进入首页看到不同的页面。

完整代码