完整代码请看我的GitHub项目Dynamic-Server
不管状态码404 200,不管里请求的路径存不存在于服务器,都会有他们对应的响应,只是状态码200表示你请求成功,Ajax的话可以去执行.then里第一个函数;只是状态码404表示你请求失败,Ajax的话会去执行.then里面第二个函数。
概念:动态服务器和静态服务器
判断依据——是否请求了数据库
- 这个服务器没有请求数据库,就是静态服务器(静态网页)
- 这个服务器请求了数据库,就是动态服务器(动态网页)
关于数据库
- 数据库不属于前端范围,但程序员应该懂一点数据库
基本操作
本博客直接用json文件当作数据库
在服务器的同级目录里新建db/users.json,文件内容的基本结构如下
[
{ "id": 1, "name": "jack", "password": "qqq" },
{ "id": 2, "name": "Rose", "password": "kkk" },
]
注:清空数据库的话变成一个空数组就行,千万不要全部清空
如何读数据库到服务器里
- 读数据库里的users.json文件的内容并且变成字符串,命名为usersString
- (反序列化,反字符串化)把符合JSON语法的字符串userString变成一个JS对象(数组)
如何在服务器里写数据库
- 在服务器里新建一个用户信息
- 把新的用户信息推送到刚刚在服务器中生成的的usersArray数组里
- (序列化,字符串化)把usersArray数组变回符合JSON语法的字符串
- 写回数据库的users.json文件中去
//fs用来读文件
const fs = require("fs");
//如何读数据库到服务器里
const usersString = fs.readFileSync("./db/users.json").toString();//读数据库里的users.json文件的内容并且变成字符串,命名为usersString
const usersArray = JSON.parse(usersString);//把符合JSON语法的字符串userString变成一个JS对象(数组)
//如何在服务器里写数据库
const user3 = { id: 3, name: "tom", password: "yyy" };//在服务器里新建一个用户信息
usersArray.push(user3);//把新的用户信息推送到刚刚在服务器中生成的的usersArray数组里
const string = JSON.stringify(usersArray);//把usersArray数组变回符合JSON语法的字符串
fs.writeFileSync("./db/users.json", string);//写回数据库的users.json文件中去
目标一:用户注册
效果
- 用户提交用户名和密码
- users.json里就新增了一行数据
- 用户注册成功就跳转到登录页面
思路
- 前端写一个form,让用户填写name和password
- 前端监听submit事件
- 前端发送post请求,数据位于请求体
- 后端接收post请求
- 后端获取请求体中的name和password
- 后端把数据写入数据库
具体步骤
1、前端写一个form,让用户填写name和password
- 新建register.html,这是注册页面(直接进去,不用设置路由,因为静态服务器)
- 写表单form,里面要有输入name和password的input以及注册button
2、前端监听submit事件
- 引用jQuery
- 直接在html里用script标签写js
- 监听表单标签的提交事件
3、前端发送post请求,数据位于请求体
当表单被提交,需要执行函数,函数内容为
-
阻止form表单的默认事件
-
获取用户数据:找到input的name为name的元素,取该元素的用户输入的值,命名为name;同理找到input的name为password的元素,取该元素的用户输入的值,命名为password
-
用jQuery.ajax发送POST请求:要把用户数据上传给服务器,服务器会把数据存到数据库
①POST请求
②请求的url是/register
③传给服务器的数据也就是请求体内容为刚刚获取的用户数据的json字符串
④请求体的类型也就是传给服务器的数据类型为json
⑤再用.then设置请求成功函数(跳转到"/sign_in.html"登录页面(提前写好
/sign_in.html文件))和失败后的函数(啥也不干)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!--手机端请移步淘宝抄就完事-->
<meta
name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"
/>
<title>注册</title>
</head>
<body>
<form id="registerForm">
<div>
<label>用户名 <input type="text" name="name"/></label>
</div>
<div>
<label>密码 <input type="password" name="password"/></label>
</div>
<div><button type="submit">注册</button></div>
</form>
<!--先引用jQuery后面的js才可以用-->
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<!-- 直接在html里面写js-->
<!--监听from表单元素的点击click事件,当click事件被触发时执行函数
①阻止form表单的默认事件;
②找到input的name为name的元素,取该元素的用户输入的值,命名为name;同理找到input的name为password的元素,取该元素的用户输入的值,命名为password
③直接用jQuery发Ajax请求:POST请求,请求的url是/register,传给服务器的数据也就是请求体内容为刚刚获取的用户数据的json字符串,请求体的类型也就是传给服务器的数据类型为json;;;再用.then设置请求成功和失败后的函数
-->
<!--阻止默认事件-->
<script>
const $form = $("#registerForm");
$form.on("submit", e => {
e.preventDefault();
const name = $form.find("input[name=name]").val();
const password = $form.find("input[name=password]").val();
console.log(name, password);
$.ajax({
method: "POST",
url: "/register",
contentType: "text/json; charset=UTF-8",
data: JSON.stringify({ name, password })
}).then(
() => {
alert("注册成功");
location.href = "/sign_in.html";
},
() => {}
);
});
</script>
</body>
</html>
4、后端接收post请求
写对应的路由/register,而且还得是POST请求!很重要!区分开来请求文件register.html
5. 后端获取请求体中的name和password
6.后端存储数据
先读数据库,在把新数据写入数据库
var http = require("http");
var fs = require("fs");
var url = require("url");
var port = process.argv[2];
if (!port) {
console.log("请指定端口号好不啦?\nnode server.js 8888 这样不会吗?");
process.exit(1);
}
var server = http.createServer(function(request, response) {
var parsedUrl = url.parse(request.url, true);
var pathWithQuery = request.url;
var queryString = "";
if (pathWithQuery.indexOf("?") >= 0) {
queryString = pathWithQuery.substring(pathWithQuery.indexOf("?"));
}
var path = parsedUrl.pathname;
var query = parsedUrl.query;
var method = request.method;
/******** 从这里开始看,上面不要看 ************/
console.log("有个傻子发请求过来啦!路径(带查询参数)为:" + pathWithQuery);
if (path === "/register" && method === "POST") {
response.setHeader("Content-Type", "text/html; charset=utf-8");
//一、读数据库
const userArray = JSON.parse(fs.readFileSync("./db/users.json"));
//二、获取请求体里的数据:name和password
//声明一个数组
const array = [];
//监听请求的上传事件,把里面的chunk数据push到数组,因为数据可能是一点一点上传的。那你每上传一点我就把你这一点push到这个数组
request.on("data", chunk => {
array.push(chunk);
});
//监听请求的结束事件,先把array里的数据变成字符串,这个字符串符合JSON语法,因为请求的时候设置了啊。然后在把字符串变成js对象
request.on("end", () => {
const string = Buffer.concat(array).toString();
const obj = JSON.parse(string); // 请求体里的数据获取成功:{name password}
})
//三、写入数据库
const lastUser = userArray[userArray.length - 1];
const newUser = { //新用户信息
id: lastUser ? lastUser.id + 1 : 1, //id是,如果最后一个用户信息存在,就是最后一个用户信息的id+1,否则就是1
name: obj.name,
password: obj.password
};
userArray.push(newUser);
fs.writeFileSync("./db/users.json", JSON.stringify(userArray));
response.end();
else { //否则还是静态页面
response.statusCode = 200;
// 默认首页
const filePath = path === "/" ? "/index.html" : path;
const index = filePath.lastIndexOf(".");
// suffix 是后缀
const suffix = filePath.substring(index);
const fileTypes = {
".html": "text/html",
".css": "text/css",
".js": "text/javascript",
".png": "image/png",
".jpg": "image/jpeg"
};
response.setHeader(
"Content-Type",
`${fileTypes[suffix] || "text/html"};charset=utf-8`
);
let content;
try {
content = fs.readFileSync(`./public${filePath}`);
} catch (error) {
content = "文件不存在";
response.statusCode = 404;
}
response.write(content);
response.end();
}
/******** 代码结束,下面不要看 ************/
});
server.listen(port);
console.log(
"监听 " +
port +
" 成功\n请用在空中转体720度然后用电饭煲打开 http://localhost:" +
port
);
目标二:登录页面
效果
- 用户提交用户名和密码
- 把这两数据和数据库里的比对
- 确实有用户名和密码,那就请求成功(返回状态码200),跳转到家页面;没有匹配的,那就请求失败(响应状态码400),
思路(前半部分同上)
- 前端写一个form,让用户填写name和password
- 前端监听submit事件
- 前端发送post请求,数据位于请求体
- 后端接收post请求
- 后端获取请求体中的name和password
- 后端把数据与数据库中数据对比
具体步骤
1、前端写一个form,让用户填写name和password
- 新建sign_in.html,这是登录页面(直接进去,不用设置路由,因为静态服务器)
- 写表单form,里面要有输入name和password的input以及登录button
2、前端监听submit事件
- 引用jQuery
- 直接在html里用script标签写js
- 监听表单标签的提交事件
3、前端发送post请求,数据位于请求体
当表单被提交,需要执行函数,函数内容为
-
阻止form表单的默认事件
-
获取用户数据:找到input的name为name的元素,取该元素的用户输入的值,命名为name;同理找到input的name为password的元素,取该元素的用户输入的值,命名为password
-
用jQuery.ajax发送POST请求:要把用户数据上传给服务器,服务器会把数据存到数据库
①POST请求
②请求的url是/sign_in
③传给服务器的数据也就是请求体内容为刚刚获取的用户数据的json字符串
④请求体的类型也就是传给服务器的数据类型为json
⑤再用.then设置请求成功函数(跳转到"/home.html"页面)和失败后的函数(啥也不干)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!--手机端请移步淘宝抄就完事-->
<meta
name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"
/>
<title>登录</title>
</head>
<body>
<form id="signInForm">
<div>
<label>用户名 <input type="text" name="name"/></label>
</div>
<div>
<label>密码 <input type="password" name="password"/></label>
</div>
<div><button type="submit">登录</button></div>
</form>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script>
const $form = $("#signInForm");
$form.on("submit", e => {
e.preventDefault();
const name = $form.find("input[name=name]").val();
const password = $form.find("input[name=password]").val();
console.log(name, password);
$.ajax({
method: "POST",
url: "/sign_in",
contentType: "text/json; charset=UTF-8",
data: JSON.stringify({ name, password })
}).then(
() => {
alert("登录成功");
location.href = "/home";
},
() => {}
);
});
</script>
</body>
</html>
4、后端接收post请求
写对应的路由/sign_in ,而且还得是POST,区分开来sign_in.html
也要设置/home.html的路由
5. 后端获取请求体中的name和password
6.后端把数据与数据库中数据对比
先读数据库,在把数据和数据库中数据对比
else if (path === "/sign_in" && method === "POST") {
//一、读数据库
const userArray = JSON.parse(fs.readFileSync("./db/users.json"));
//二、获取请求体里的数据:name和password
//声明一个数组
const array = [];
//监听请求的上传事件,把里面的chunk数据push到数组,因为数据可能是一点一点上传的。那你每上传一点我就把你这一点push到这个数组
request.on("data", chunk => {
array.push(chunk);
});
//监听请求的结束事件,先把array里的数据变成字符串,这个字符串符合JSON语法,因为请求的时候设置了啊。然后在把字符串变成js对象
request.on("end", () => {
const string = Buffer.concat(array).toString();
const obj = JSON.parse(string); // 请求体里的数据:name password
//三、对比数据库里的数据
//从数据库数组里找到和请求体数据一样的那个元素,找不到就是undefined
const user = userArray.find(
user => user.name === obj.name && user.password === obj.password
);
//找不到,那状态码就算404,请求失败
if (user === undefined) {
response.statusCode = 400;
response.setHeader("Content-Type", "text/json; charset=utf-8");
response.end('{"errorCode":4001}')
} else { //否则,请求成功,状态码200
response.statusCode = 200;
response.end()
}
})
}
else if (path === "/home"){
response.end()
}
Cookie
定义
- Cookie是服务器下发给浏览器的一段字符串
- 浏览器必须保存这个Cookie (除非用户删除)
- 之后发起相同二级域名请求(任何请求)时,浏览器必须附上Cookie(在请求头里)
以公园门票作为对比(画图)
- 假如你是公园检票员,你怎么知道谁能进谁不能进? 有票能进, 没票不能进
- Cookie就是门票 有Cookie就是登录了,没Cookie就没登录 那后端给浏览器下发一个Cookie不就完事了嘛
语法看MDN
注意
- 永远不要用前端设置cookie,只准在后端设置cookie
用这句
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly前端就不能折腾cookie了(不然用户也可以自己改了) - 开发者工具->Application->Cookie可以查看Cookie
目标三:用Cookie标记用户已登录
效果
- 当用户如果从登录页面登陆成功就跳转到家页面,家页面会有:欢迎回家
- 但是用户如果没有从登录页面登录却进了家页面,家页面会有:未登录
思路
具体步骤
- 写个/public/home.html,他将是家页面/home家页面的响应体内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>家页面</title>
</head>
<body>
<p>{{loginStatus}}</p>
<p><a href="sign_in.html">请登录</a></p>
</body>
</html>
- 在用户名密码对的那一瞬间发门票---在服务器/sign_in里第三步对比数据库中数据时,如果用户名密码都核对正确了,确实找到一个数据库中用户和他一样,马上设置Cookie(给门票),门票内容是logined=1
response.setHeader("Set-Cookie", `logined=1; HttpOnly`)
- 设置家页面/home.html的路由,如果有门票就显示已登录,没门票就显示未登录
else if (path === "/sign_in" && method === "POST") {
const userArray = JSON.parse(fs.readFileSync("./db/users.json"));
const array = [];
request.on("data", chunk => {
array.push(chunk);
});
request.on("end", () => {
const string = Buffer.concat(array).toString();
const obj = JSON.parse(string);
const user = userArray.find(
user => user.name === obj.name && user.password === obj.password
);
if (user === undefined) {
response.statusCode = 400;
response.setHeader("Content-Type", "text/json; charset=utf-8");
response.end('{"errorCode":4001}')
} else {
response.statusCode = 200;
response.setHeader("Set-Cookie", `logined=1; HttpOnly`) //发现用户名密码正确,马上给门票,也就是设置响应头里的Cookie
response.end()
}
})
}
else if (path === "/home.html"){
const cookie = request.headers['cookie'] //获取请求头里的Cookie
const homeHtml = fs.readFileSync("./public/home.html").toString(); //获得文件home.html的内容并变成字符串
let x = cookie==='logined=1'?'已登录':'未登录' //如果cookie==='logined=1'为真,也就是用户登录用户名密码正确,得到了cookie,x就为'已登录',否则就为'未登录'
const string = homeHtml.replace('{{loginStatus}}',`${x}`) //把字符串里的占位符替换成x
respose.write(string)
}
目标四:用cookie记录user.id
效果
- 当用户如果从登录页面登陆成功就跳转到家页面,家页面会有:xxx用户,欢迎回家
- 但是用户如果没有从登录页面登录却进了家页面,家页面会有:未登录
思路
把cookie门票的内容改成找到的数据库中那个用户的id,这样就可以随着cookie传来传去。 当进入家页面,有用户id的,就显示已登录,就把占位符替换了,没有的就是显示未登录
具体步骤
- 在用户名密码对的那一瞬间发门票---在服务器/sign_in里第三步对比数据库中数据时,如果用户名密码都核对正确了,确实有数据库中那个用户,就马上设置Cookie(给门票),门票内容是
user_id=${user.id}数据库中那个用户的id
response.setHeader("Set-Cookie", `user_id=${user.id}; HttpOnly`);
- 在/home家页面,找出cookie里的登录用户Id,如果有,那就在找出数据库中和他id一样的用户,如果还有,那响应体的内容就是xxx用户已登录;前面的东西都没有,那就不是从登录页面进来的,那响应体的内容及以上未登录。
else if (path === "/home") {
const cookie = request.headers["cookie"]; //获取请求头里的Cookie
try {
//尝试:从cookie里提取出登录用户Id,命名为userId
userId = cookie
.split(";")
.filter(s => s.indexOf("user_id") >= 0)[0]
.split("=")[1];
} catch (error) {} //如果失败了,也没啥
console.log(userId);
const homeHtml = fs.readFileSync("./public/home.html").toString(); //获得文件home.html的内容并变成字符串
let string;
//如果从cookie里确实有登录用户Id,存在,那就
if (userId) {
const userArray = JSON.parse(fs.readFileSync("./db/users.json")); //读数据库,成为数组userArray
const user = userArray.find(user => user.id.toString() === userId); //找出数据库中,他的id和登录用户id一样的用户,叫做user
if (user)
//如果数据库中确实有和登录用户id一样的用户
string = homeHtml
.replace("{{loginStatus}}", "已登录")
.replace("{{user.name}}", user.name);
} else {
string = homeHtml
.replace("{{loginStatus}}", "未登录")
.replace("{{user.name}}", "");
}
response.write(string);
response.end();
}
安全漏洞
门票上的内容(我们找到的数据库中那个用户的id)相当于通关密码了,那谁都可以用开发者工具(设置了HttpOnly)或js(没设置httponly)修改,我要是知道了数据库中马化腾用户的id,我就可以把我的user_id=马化腾的id,我就可以进入马化腾的家页面了
目标五:防篡改user. id
思路一:加密
- 将user_id加密发送给前端,后端读取user_id时解密,
- 此法可行,但是有安全漏洞
- 漏洞:加密后的内容可无限期使用
- 解决办法: JWT (以后学)
思路二:session!把信息隐藏在服务器
-
把用户信息放在服务器的session (会话)里,再给信息一个随机id
-
把随机id发给浏览器
-
后端下次读取到id时,通过session[id]获取用户信息
-
想想为什么用户无法篡改id (因为id 很长,而且随机)
-
session (会话)是文件。不能用内存,因为断电内存就清空
-
服务器给浏览器发的cookie里面是个服务器已经保存好的随机数,而不是用户真正的id,这个随机数在我服务器里的session文件里是对应着真正的用户id,只有服务器知道这串随机数对应的是哪个id。
-
服务器用随机数储存用户id,在用随机数找出用户id。同时浏览器只知道随机数
//先读出session文件
const session = JSON.parse(fs.readFileSync("./session.json").toString());
console.log("有个傻子发请求过来啦!路径(带查询参数)为:" + pathWithQuery);
if (path === "/sign_in" && method === "POST") {
const userArray = JSON.parse(fs.readFileSync("./db/users.json"));
const array = [];
request.on("data", chunk => {
array.push(chunk);
});
request.on("end", () => {
const string = Buffer.concat(array).toString();
const obj = JSON.parse(string); // 请求体里的数据:name password
const user = userArray.find(
user => user.name === obj.name && user.password === obj.password
);
if (user === undefined) {
response.statusCode = 400;
response.setHeader("Content-Type", "text/json; charset=utf-8");
response.end('{"errorCode":4001}');
} else {
//如果真的找到了数据库中有个用户,他的信息和用户刚刚输入的一样
response.statusCode = 200;
response.end();
const random = Math.random(); //生成一个随机数
session[random] = { user_id: user.id }; //给session对象新加一个属性,属性名就是那个随机数,属性值是{ user_id:从数据库中找到的那个用户的id }
fs.writeFileSync("./session.json", JSON.stringify(session)); //再把新session写回session文件中去
response.setHeader("Set-Cookie", `session_id=${random}; HttpOnly`); //立马发门票,门票内容是session_id=前面那个随机数
//也就是服务器给浏览器发的cookie是个我已经保存好的随机数,浏览器只得到随机数,只有我(服务器)知道这个随机数对应的是哪个用户的id
}
response.end();
});
} else if (path === "/home.html") {
const cookie = request.headers["cookie"]; //获取请求里的Cookie
let sessionId;
try {
//尝试:从门票内容里面提取出随机数。命名为sessionId(浏览器只能提供给我随机数,因为我只给他了随机数)
sessionId = cookie
.split(";")
.filter(s => s.indexOf("session_id=") >= 0)[0]
.split("=")[1];
} catch (error) {}
if (sessionId && session[sessionId]) {
//如果cookie里确实有随机数,而且我(服务器)再去我的session对象里找,发现session对象里也确实有叫随机数的属性(值是真正的用户id)
const userId = session[sessionId].user_id; //提取出用户id
const userArray = JSON.parse(fs.readFileSync("./db/users.json"));
const user = userArray.find(user => user.id === userId); //从数据库里找出用户id对应的那个用户
//把用户名替换就完事
const homeHtml = fs.readFileSync("./public/home.html").toString(); /
let string = "";
if (user) {
string = homeHtml
.replace("{{loginStatus}}", "已登录")
.replace("{{user.name}}", user.name);
}
response.write(string);
} else {
const homeHtml = fs.readFileSync("./public/home.html").toString();
const string = homeHtml
.replace("{{loginStatus}}", "未登录")
.replace("{{user.name}}", "");
response.write(string);
}
response.end();
}
Cookie / Session总结
- 服务器可以给浏览器下发Cookie
- 通过Response Header
- 具体语法见MDN
- 浏览器上的Cookie可以被篡改
- 用开发者工具就能改
- 弱智后端下发的Cookie用JS也能篡改(因为没写HttpOnly)
- 所以,服务器需要下发可以篡改,但是篡改了也不会有啥实际效果的Cookie
- Cookie可包含加密后的信息(还得解密,麻烦)
- 用session:Cookie也可只包含一个随机数id ,用session[随机数id]可以在后端拿到对应的信息,这个id无法被篡改 (但可以被复制,问题不大)
目标六:注销
思路
- 将session[随机数id]从session里删掉
- 将cookie从浏览器删掉(可选)即可
实现
- 前端制作注销按钮
- 前端监听注销按钮点击事件
- 前端发送delete session请求
- 后端接受delete session请求
- 后端获取当前session[随机数id],并将其删除
- 后端下发一个过期的同名Cookie (因为没有删除)
- 安全起见,除了删除浏览器端的 Cookie,还需要把对应的 Session 数据删掉
- 安全起见,不能用 JS 删 Cookie,应该使用 HTTP only 的 Cookie,然后 JS 发请求让服务器删 Cookie
bug:用户密码被泄露
目标七:防止密码泄露
- 不要存明文
- 拿到明文之后,bcrypt.js 加密,得到密文
- 将密文保存到users. json
- 用户登录时,加密后对比密文是否一-致
- 一致则说明用户知道密码,可以登录
- 不要用MD5
- 傻才用MD5来处理密码
- MD5甚至都不是一种加密算法,是hash算法
- 不要被辣鸡教程误导
- 深入阅读, 请看这篇博客
大总结
- 如何获取post请求体(后端知识)
- 如何使用Cookie (后端知识)
- 永远不要用JS操作Cookie,要http-only
- 什么是Session (后端知识)
- 就是后端的一个文件,保存会话数据,一般存用户信息
- 注册、登录、注销的实现(后端)
- 每个Web程序员都应该搞清楚这些知识