【前后分离】写一个动态服务器

270 阅读15分钟

完整代码请看我的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文件中去

目标一:用户注册

效果

  1. 用户提交用户名和密码
  2. users.json里就新增了一行数据
  3. 用户注册成功就跳转到登录页面

思路

  1. 前端写一个form,让用户填写name和password
  2. 前端监听submit事件
  3. 前端发送post请求,数据位于请求体
  4. 后端接收post请求
  5. 后端获取请求体中的name和password
  6. 后端把数据写入数据库

具体步骤

1、前端写一个form,让用户填写name和password

  1. 新建register.html,这是注册页面(直接进去,不用设置路由,因为静态服务器)
  2. 写表单form,里面要有输入name和password的input以及注册button

2、前端监听submit事件

  1. 引用jQuery
  2. 直接在html里用script标签写js
  3. 监听表单标签的提交事件

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

目标二:登录页面

效果

  1. 用户提交用户名和密码
  2. 把这两数据和数据库里的比对
  3. 确实有用户名和密码,那就请求成功(返回状态码200),跳转到家页面;没有匹配的,那就请求失败(响应状态码400),

思路(前半部分同上)

  1. 前端写一个form,让用户填写name和password
  2. 前端监听submit事件
  3. 前端发送post请求,数据位于请求体
  4. 后端接收post请求
  5. 后端获取请求体中的name和password
  6. 后端把数据与数据库中数据对比

具体步骤

1、前端写一个form,让用户填写name和password

  1. 新建sign_in.html,这是登录页面(直接进去,不用设置路由,因为静态服务器)
  2. 写表单form,里面要有输入name和password的input以及登录button

2、前端监听submit事件

  1. 引用jQuery
  2. 直接在html里用script标签写js
  3. 监听表单标签的提交事件

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标记用户已登录

效果

  1. 当用户如果从登录页面登陆成功就跳转到家页面,家页面会有:欢迎回家
  2. 但是用户如果没有从登录页面登录却进了家页面,家页面会有:未登录

思路

具体步骤

  1. 写个/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>

  1. 在用户名密码对的那一瞬间发门票---在服务器/sign_in里第三步对比数据库中数据时,如果用户名密码都核对正确了,确实找到一个数据库中用户和他一样,马上设置Cookie(给门票),门票内容是logined=1
response.setHeader("Set-Cookie", `logined=1; HttpOnly`)
  1. 设置家页面/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

效果

  1. 当用户如果从登录页面登陆成功就跳转到家页面,家页面会有:xxx用户,欢迎回家
  2. 但是用户如果没有从登录页面登录却进了家页面,家页面会有:未登录

思路

把cookie门票的内容改成找到的数据库中那个用户的id,这样就可以随着cookie传来传去。 当进入家页面,有用户id的,就显示已登录,就把占位符替换了,没有的就是显示未登录

具体步骤

  1. 在用户名密码对的那一瞬间发门票---在服务器/sign_in里第三步对比数据库中数据时,如果用户名密码都核对正确了,确实有数据库中那个用户,就马上设置Cookie(给门票),门票内容是user_id=${user.id}数据库中那个用户的id
response.setHeader("Set-Cookie", `user_id=${user.id}; HttpOnly`);

  1. 在/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总结

  1. 服务器可以给浏览器下发Cookie
  • 通过Response Header
  • 具体语法见MDN
  1. 浏览器上的Cookie可以被篡改
  • 用开发者工具就能改
  • 弱智后端下发的Cookie用JS也能篡改(因为没写HttpOnly)
  1. 所以,服务器需要下发可以篡改,但是篡改了也不会有啥实际效果的Cookie
  • Cookie可包含加密后的信息(还得解密,麻烦)
  • 用session:Cookie也可只包含一个随机数id ,用session[随机数id]可以在后端拿到对应的信息,这个id无法被篡改 (但可以被复制,问题不大)

目标六:注销

思路

  1. 将session[随机数id]从session里删掉
  2. 将cookie从浏览器删掉(可选)即可

实现

  1. 前端制作注销按钮
  2. 前端监听注销按钮点击事件
  3. 前端发送delete session请求
  4. 后端接受delete session请求
  5. 后端获取当前session[随机数id],并将其删除
  6. 后端下发一个过期的同名Cookie (因为没有删除)
  • 安全起见,除了删除浏览器端的 Cookie,还需要把对应的 Session 数据删掉
  • 安全起见,不能用 JS 删 Cookie,应该使用 HTTP only 的 Cookie,然后 JS 发请求让服务器删 Cookie

bug:用户密码被泄露

目标七:防止密码泄露

  1. 不要存明文
  • 拿到明文之后,bcrypt.js 加密,得到密文
  • 将密文保存到users. json
  • 用户登录时,加密后对比密文是否一-致
  • 一致则说明用户知道密码,可以登录
  1. 不要用MD5
  • 傻才用MD5来处理密码
  • MD5甚至都不是一种加密算法,是hash算法
  • 不要被辣鸡教程误导
  • 深入阅读, 请看这篇博客

大总结

  1. 如何获取post请求体(后端知识)
  2. 如何使用Cookie (后端知识)
  • 永远不要用JS操作Cookie,要http-only
  1. 什么是Session (后端知识)
  • 就是后端的一个文件,保存会话数据,一般存用户信息
  1. 注册、登录、注销的实现(后端)
  • 每个Web程序员都应该搞清楚这些知识