前置代码下载
前言
在我们登录一个网页时,只要输入帐号密码,如果跟注册时账号密码相同,那么就会提示登录成功并跳转页面,如果失败则会提示失败。那么,用户从注册到登录究竟发生了哪些数据转换,前后端之间是如何交互的,这是今天我要描述的问题,此博客作为模拟用户登录小功能的一次记录,我将在这里复述我所学到的知识。
在开始之前,由于设备原因,我只能采用JSON文件作为数据库,一切操作都围绕JSON文件进行。
整个页面跳转过程是这样的:注册registered---登录login---首页index
我们首选要定一个小目标,先模拟一个注册功能
注册功能步骤
-
我们需要写一个注册页面
-
使用jquery的ajax进行数据传送,发给后端
-
后端接收数据,使用node.js来处理数据
-
将数据放入JSON文件中
当然,在此之前,我们需要先写好静态服务器的基础代码,这个代码在前置代码中可以查看。
准备工作
- 三个html页面:分别为登录、注册、首页
- 一个用简易node.js写的服务器
- 一个名为mysql的JSON文件用来当数据库
准备工作的代码在前置代码下载的init
文件夹中
注册页面使用ajax
如何创建form表单这里就不多介绍,这里贴一下ajax的jquery代码做进一步讲解
const $form = $('form')
$form.on('submit', (e) => {
e.preventDefault()
const userid = $form.find('input[name=userid]').val()//获取对应的值
const password = $form.find('input[name=password]').val()//获取对应的值
$.ajax({
url: './registered',
method: 'POST',
contentType: "text/json; charset=utf-8",
data: JSON.stringify({ //一定要转化成JSON字符串再传
userid: userid,
password: password
})
})
})
当form表单触发submit事件时调用jquery的ajax封装,由于提交表单时的默认行为是会刷新页面,所以我们先用e.preventDefault()
来阻止默认行为,然后用jquery的ajax封装传递一个对象,里面需要注意一定要使用JSON.stringfy()
来将用户写入的值传递出去,因为不这样做,导致我在写后端代码时打出来的传递值编码始终不对。
写服务器node.js
我们可以使用request.on('data',()=>{})
事件来获取AJAX传递过来的JSON数据,因为传递的字节可能是一段一段的,所以我们用一个[ ]
数组来防止传递过来的数据
const ajaxdata = []
request.on('data', (data) => {
ajaxdata.push(data) //这里的data就是ajax传递的数据
})
然后使用request.on('end',()=>{})
事件将下载好的代码全部接收过来
查询JSON文件里面的内容
// node读取json文件数据操作
const fs = require('fs') //固定写法操作文件
const data = fs.readFileSync('./mysql.json') //同步读取文件
const mysql = JSON.parse(data)
将ajax获得的数据给转成对象
const string = Buffer.concat(ajaxdata).toString() //将下载好的数据转成字符串
const jsondata = JSON.parse(string)
写数据进文件中
const newdata = {
id: id,
userid: jsondata.userid,
password: jsondata.password
}
mysql.push(newdata)
fs.writeFileSync('./mysql.json', JSON.stringify(mysql))
id取值
由于数据库中,所有数据表总是遵循绝对有一个唯一编码,所以我们在获取到ajax数据并处理好后,需要给一个id并将其跟随获取到的ajax数据保存进文件中
let id = mysql[mysql.length - 1] ? (mysql[mysql.length - 1].id + 1) : 1 //查询数组的最后一位,如果有就取它的id+1,没有就用1
注册页面写ajax传递成功的函数
由于ajax标准使用方式为promise对象,所以可以使用.then()
函数,当ajax成功时的promise对象自身的状态为fulfilled,那么就会调用then
函数的第一个函数参数,代码为
.then(()=>{ location.href='./login.html' })
到此,注册功能功能基本完成。
登录
登录时的操作跟注册时代码几乎相同,包括AJAX的代码,我们只需要将form表单中的注册改成登录即可,ajax成功后的代码修改成成功后跳转index.html
,失败过提示帐号密码错误即可。
这里介绍location.assign()
方法来跳转页面,因为如果使用location.href
来跳转,有可能跳不出提示框。
location.assign('./index.html')
服务器代码
我们只需要验证一下是否提交的ajax数据是否和json文件里的一样即可,在这里我使用Array.prototype.find
方法
这个方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。
代码如下:
if (path === '/public/login.html' && request.method === 'POST') {
let arr = []//用来接收下载的数据
request.on('data', (data) => {
arr.push(data)
})
request.on('end', () => {
const arrdata = JSON.parse(arr)
response.setHeader('Content-type', 'text/html;charset=UTF-8')
let sql = JSON.parse(fs.readFileSync('./mysql.json'))
const n = sql.find((val) => {
return val.password === arrdata.password && val.userid === arrdata.userid
}) //find方法来判断返回值
if (n === undefined) {
response.statusCode = 400
} else {
response.statusCode = 200
}
response.end('发送')
})
上面的代码通过find方法判断是否为undefined,如果是,说明在JSON文件中找不到对应的数据,那么前端返回“跳转失败”。
登录完成,再定一个小目标
注册到登录已经做完,此刻如果验证成功后应该跳转到首页了。这时我又有了新需求,当我登录后进入首页时,让首页显示用户已经登录。
但是此时遇到一个问题,怎样让服务器知道我是登录过才进入首页还是未登录过才进入首页的?两者的区别在于登录过后让首页提示用户已登录,未登录则让首页为原始状态
进入首页
cookie
现实生活中,我们如果想进入一个收费公园,那么肯定有人会验票,这个票据就是你有没有收过费的证明,那么上面的需求,跟这个是不是类似呢?
假设用户登录过,服务器发一个票给用户,那么是不是就可以实现登录后页面与未登录不一致的情况呢?
这个票就是cookie,它由浏览器保存,我们可以用服务器代码给成功登录的用户发cookie,怎么设置呢?我们将其设置在http的响应头中
当用户点击登录后,我们把这个cookie给到用户,set-cookie MDN
这里注意,cookie需要由后端代码设置,且设置为不允许前端修改 语法:
response.setHeader('Set-Cookie', 'xxx=xxx;HttpOnly')
但是,cookie有一个缺点,那就是即使写上不能篡改,但还是可以通过浏览器的Application修改,篡改后就相当于拿到门票,那么如果正好是其他用户的cookie,不就相当于拿到其他用户的数据了吗?
那么要怎样解决这个安全问题呢?
session会话
解决思路:其实这个问题无非就是cookie值取值问题,如果这个cookie够随机位数够多,那即使修改了cookie也没啥用,因为跟其他用户也不匹配。
所以我们可以这样解决:
再建一个JSON文件session.json
来存放随机数,让这个随机数与用户id做对应,只要cookie是这个随机数,就将对应id的用户信息传送出去。
这种方式有个很洋气的名字,叫session会话
那么我们设计的流程应当是这样的:
- 首先,用户登录首页
- 我们查用户的cookie中有没有我们给的指定的cookie
- 如果没有,那就跳默认首页
- 如果有,那么我们就把默认首页的用户名给改掉
这其中的数据流转应当是这样的:
如果获取了用户的cookie,那么做一个判断,看看是不是能够在session.json
中可以找到,如果能够找到,那么就代表的确是目标客户,我们就取对应的目标用户的id
那么session.json
内,我们可以放什么呢?就放一个随机数,然后内部放用户的id,类似于这样的结构 {"随机数",{id:xxx}}
登录时代码完善
const n = sql.find((val) => {
return val.password === arrdata.password && val.userid === arrdata.userid
}) //判断登录时帐号密码是否一致,如果一致则返回sql中对应的数据
if (n === undefined) { //不一致给个400返回码
response.statusCode = 400
} else {
response.statusCode = 200
const session = JSON.parse(fs.readFileSync('./session.json')) //读取新建的session文件
const random = Math.random() //设置一个随机数
session[random] = {
'userid': n.id
}
//把随机数和对应用户的数据放到session文件中
fs.writeFileSync('./session.json', JSON.stringify(session))
// 以随机数为cookie码返回给用户,这个cookie就是长串的钥匙
response.setHeader('Set-Cookie', `sessionid=${random};HttpOnly`)
}
上面的代码中我设置一个随机数cookie给用户,就相当于这个cookie是一串很长的钥匙,只要在用户进入首页时,查看有没有这个cookie,再查看cookie是否对的上即可
首页代码
if (path === '/index.html') {
response.statusCode = 200
response.setHeader('Content-type', 'text/html;charset=UTF-8')
let cookie = request.headers['cookie'] //找cookie
let mysql = JSON.parse(fs.readFileSync('./mysql.json')) //读取mysql中的数据
let session = JSON.parse(fs.readFileSync('./session.json')) //读取session中的数据
let sessionid
try {
sessionid = cookie.split(';').filter(s => s.indexOf("sessionid=") >= 0)[0].split("=")[1] //这里将拿到的cookie给拆开成数组并过滤查看有没有我指定过的cookie
} catch {} //由于可能存在没有找到的情况,所以这里用了try做错误处理
if (sessionid) { //如果有这个id,那么就判断是否跟session文件一致
let user = mysql.find((user) => {
return user.id === session[sessionid].userid
})
let string = ''
if (user) { //下面是一致的代码
string = fs.readFileSync('./public/index.html').toString().replace('{{用户名}}', "已登录")
response.write(string)
} else {
response.write(fs.readFileSync('./public/index.html'))
}
} else {
response.write(fs.readFileSync('./public/index.html'))
}
response.end()
尾言
上面的代码只是粗俗地描绘了前后端交互时会存在的一些问题与解决思路,在实际开发过程中我们将遇到更加复杂、更加涉及安全地问题,当不停试错不停修改逻辑时,也加深自身对程序的理解与敬畏。