用户登录功能

HTTP 本身就是个 无状态 协议,也就是你在一个 API 中设置一个变量,后一次请求再去访问这个 API 也拿不到这个变量了,服务器很健忘,马上就忘了客户端了。

这样你想想,怎么去实现一个购物车呀? 但是 session 会话(或者也叫对话,但是 session 的中文本意其实是“一段时间”)机制可以解决这个问题。

实际上 IT 界,每次引入一个新技术,都是为了解决一个实际问题的。例如,人们发明 cookie 就是为了解决,服务器和客户端互不相认的问题。但是 cookie 有了,新问题来了,那就 cookie 的存储能力有限,如果一个用户买了1000000件商品,那么这些商品信息要存在哪里呢?

Session 基本原理

当前最流行的做法就是,在每次有一个新的浏览器用户登录进来,服务器端都会为这个浏览器开辟一个小的内存区域(注意,这个区域可以认为是一个文件,但是这个文件是存在服务器端的),通常的术语就叫”又创建了一个新的 session“,那么在这个 session 之中,服务器就可以为这位特定用户(其实就是为特定的那个浏览器)保存任意信息到 session 之中了(例如,用户名,购物车中的商品)。但是同一时刻,连接到服务器的浏览器可能成千上万,那么服务器是如何区分不同用户的浏览器的呢?简单的答案就是,使用 cookie 。

具体来说,每个服务器端的 session 被创建的时候,都会有一个类似于文件名的东西,叫做 sessionId 。每个 session 在服务端被创建的时候,sessionId 都会自动的被发送到浏览器的 cookie 中,这样,每个浏览器就跟自己在服务器上的 session 有了绑定关系了。

代码演示

自己手动基于 cookie 机制,去服务器上开辟 session 比较麻烦。所以各大语言框架都有自己的 session 管理接口,例如,express 的接口就是 express-session

所以此时,peter 到服务器端,添加如下 API

const session = require('express-session')

app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true
}))

app.post('/login', function(req, res){
  let username =  req.body.username;
  req.session.username = username ;
  res.send(req.session.username);
})

这样,就可以达成如下效果:

浏览器从客户端发起请求,请求格式为

POST /login Content-Type: application/json {"username": "peter"}

这样,服务器端用

req.body.username

这一个变量,就可以接收到 peter 这个字符串了。

然后把 ”peter“ 这个字符串保持到 session 中,具体语句就是

req.session.username = username

之后,服务器会自动的把 sessionId 返回给浏览器,存储到 cookie 之中

注意:req.session 接口,内置 cookie 操作功能

这样,浏览器每次就可以认领自己的 session 来拿到自己的用户名了。

补充:Session 是会话的概念,一次会话其实就是一段时间,从用户登录网站开始,到用户退出网站结束的这个时间区间,就叫一次会话。上面的 req.session 是一个临时的存储区域,这个存储区域的生命周期是一次会话。

使用 curl 测试

命令如下:

$ curl -v -X POST -H "Content-Type: application/json" -d '{"username": "peter"}' http://tiger.haoduoshipin.com/login
> POST /login HTTP/1.1
> Host: localhost:3005
> Content-Type: application/json

...

< HTTP/1.1 200 OK
< set-cookie: connect.sid=s%3A37nZnzBeXX3E-vO9gmSatGf8wDfWcH_K.tKDCw3NGRdfwjPCetq%2BYEQXwiXlFcjHPCkug1zAmOFg; Path=/; HttpOnly
...
peter%

上面信息重要是分两部分:请求和响应

具体来说:

  • 请求部分可以看到发出的请求是 POST /login Content-Type: application/json {"username": "peter"} 这个是符合 API 规范的,所以应该能够得到正确的返回

  • 响应的第一行 < HTTP/1.1 200 OK ,200 表示一切正常

  • set-cookie 是我们要查看的重点,我们可以看到 req.session 接口可以正确的返回 sessionId 给浏览器

浏览器中的行为

这个我们来写一个前后端不分离的项目来演示。

$ mkdir session
$ cd session

初始化一个 nodejs 的项目

$ npm init -y

安装 express

$ npm i --save express

下面来写一个最简单 http 服务器

const express = require('express');
const app = express();

app.listen(3006, function(){
  console.log('running on port 3006...');
})

下面来实现一个 API ,返回一个静态 HTML 页面

+ app.get('/', function(req, res){
+  res.sendFile('index.html', {root: 'public'});
+ })

然后添加 public/index.html 如下

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  index.html
</body>
</html>

同时,package.json 中做如下修改

+   "start": "nodemon index.js"

这样,我后台运行

$ npm start

然后用 curl 测试一下 API

$ curl localhost:3006/
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  index.html
</body>
</html>

浏览器中打开 http://localhost:3006/ 也能够看到 index.html 的内容。

但是,此时有一个小问题,就是浏览器中访问 http://localhost:3006/index.html 结果访问不到,报错信息是

Cannot GET /index.html

对应服务器端 index.js 文件中,要做这样的修改

const app = express();
-
+app.use(express.static('public'));

app.get('/', function(req, res){
  res.sendFile('index.html',{root:'public'})
})

上面这一行的作用,就是把 public/ 文件夹 架设成了一个静态( static )服务器,也就 public/ 中所有的 html 页面,未来都可以不走 API ,直接在浏览器中,用链接的形式访问到。

添加 login.html

首先到 index.html 中,添加 login 链接

</head>
<body>
-  index.html
+  <a href="/login.html">login</a>
</body>
</html>

创建 public/login.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>login</title>
</head>
<body>
  <form method="post" action="/login">
    <label for="username">用户名</label>
    <input name="username" type="text">
    <input type="submit">
  </form>
</body>
</html>

书写服务器对应接口

安装 body-parser

$ npm i --save body-parser

index.js 中修改如下:

app.use(express.static('public'));
+const bodyParser = require('body-parser');
+app.use(bodyParser.urlencoded({ extended: false }));//parse application/x-www-form-urlencoded

app.get('/', function(req, res){
  res.sendFile('index.html', {root: 'public'});
})

+app.post('/login', function(req, res){
+  console.log(req.body);
+})

app.listen(3006, function(){
  console.log('running on port 3006...');
})

重定向 redirect

index.js 做出如下修改

app.post('/login', function(req, res){
+ let username = req.body.username;
+ //User.find({username:username})
+ //如果数据库中能找到这个用户,同事密码匹配,这样才算登陆成功
   console.log(req.body);
+  if(true) {
+    res.redirect('/');
+  }//页面重定向
 })

上面 redirect 接口实现的是 页面重定向 ,具体的效果就是,前端页面会自动跳转到指定页面,对应上面的情况,就是自动跳转到首页。

使用 req.session 接口

安装 express-session

$ npm i --save express-session

index.js 修改如下

const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: false }))//parse application/x-www-form-urlencoded

+const session = require('express-session')
+app.use(session({
+  secret: 'keyboard cat',
+  resave: false,
+  saveUninitialized: true
+}))//使用 req.session 接口

app.post('/login', function(req, res){
  let username = req.body.username;
+ req.session.username = username;
  //User.find({username:username})
  //如果数据库中能找到这个用户,同事密码匹配,这样才算登陆成功
  if(true){
    res.redirect('/')
  }//页面重定向
})

app.listen(3006,function () {
console.log('running on port 3006...');
})

上面代码有了,就可以拥有一个特殊的变量了 req.session 保存到这个变量中的数据,可以在各个 API 之间(页面之间)共享。只要本次会话不结束,这个变量就不会消失。

观察浏览器中的现象

浏览器中,我们到 login 页面,填写用户名,提交,这样后端执行的是

app.post('/login')

那么里面会有 session 的操作,也就是

req.session.username = username

也就是,服务器端的 session 已经创建了。

那么浏览器中的体现就是有一个 cookie 被创建了,到 Application -> storeage -> Cookie -> localhost:3006 之下,就可以看到,有这样的 cookie 数据:

connect.sid   xxxxxfdsjfkldsjfklsdjxxxx

上面的 sid 意思就是 Session Id ,也就是是服务器端 req.session 的对应的 id 。由于,客户端,每次请求都会携带 cookie 去服务器端,所以后续每次请求,都可以拿到服务器端 req.session 中存储的数据。

例如:

app.get('/hello', function(req, res){
  res.send(req.session.username)
})

小陷阱:后台每次添加代码,服务器都会重启,所以要重新执行一下 POST /login 生成一下 session 才能测试。

解决页面缓存问题

现在修改 index.js

app.get('/', function(req, res){
+ console.log('home page', req.session.username);
  res.sendFile('index.html', {root: 'public'});
})

我们期待的是,每次访问 / 这个 API ,都能打印出 sesssion 变量的值。但是实际中能否达成呢?

实际中是不能打印出来的。甚至我们直接到 app.get(‘/’) 这个 API 中, 我们打印一个 Hello

console.log('hello')

其实都打印不出来了,因为直接访问 http://localhost:3006/ 这个位置,就相当于访问 http://localhost:3006/index.html 而我们已经把 public 文件夹架设成静态服务器了,而且其中确实有 index.html 文件,所以 app.get(‘/’) 这个接口根本就不会执行了。

解决方法就是删除:

app.use(express.static('public'));

这样 API 就又恢复正常了。

恢复 login 页面

由于取消了静态服务器,所以 login.html 也直接访问不到了,所以 index.js 要做这样的修改

res.sendFile('index.html', {root: 'public'});
})

+app.get('/login', function(req, res){
+  res.sendFile('login.html', {root: 'public'});
+})

app.post('/login', function(req, res){
let username = req.body.username;

index.html 修改如下

</head>
<body>
-  <a href="/login.html">login</a>
+  <a href="/login">login</a>
</body>

这样,我们再去 login.html 测试一下,重定向到 / 之后,就可以看到打印出提交的用户名了。

使用 pug 模板

现在在 GET / 这个接口中,我们有了 req.session.username 也就是登录用户的用户名,但是现在我们想要在页面中显示这个变量,这个就不能直接用 index.html 了,而要使用一种模板语言。模板有多种,其中 pug 是常见的一种。

先来装包:

$ npm i --save pug

index.js 代码修改如下

const express = require('express');
const app = express();
// app.use(express.static('public'));//把 pubilc/ 文件夹 架设成一个静态页面

const session = require('express-session')
app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true
}))//使用 req.session 接口

const bodyParser =require('body-parser')
app.use(bodyParser.urlencoded({extended:false}))//parse application/x-www-form-urlencoded

+const pug = require('pug');
+app.set('view engine', 'pug')

app.get('/',function (req,res) {
  console.log('home page',req.session.username);
- res.sendFile('index.html',{root:'public'})
+ let currentUser = req.session.username;
+ res.render('index',{username:currentUser})//渲染视图模板
})

删除 public/index.html ,新增 views/index.pug 文件

html
  body
    p 你好,
      span= username
        pre !欢迎登录!

这样,再次登录一下,跳转到首页,就能显示出当前用户名了。

添加 logout 功能

index.js 中

app.get('/login', function(req, res){
  res.sendFile('login.html', {root: 'public'});
})

+app.get('/logout', function(req, res){
+  req.session.destroy();
+  res.redirect('/');
+})

views/index.pug 的功能为:

html
  style
    include main.css
  body
    if username
      p 你好,
        span= username
          pre !欢迎登录!
            a(href='./logout') 退出
    else
      p 你好,
        a(href='./login') 请登录

单页面应用中 Cookie 使用不方便了

涉及到跨域问题,在 React-Axios 环境下,收发 cookie 默认都是不允许的,所以 req.session 的使用意义不大了

来瞅瞅整个 session 的代码

package.json

{
  "name": "session",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "nodemon index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.17.1",
    "express": "^4.15.2",
    "express-session": "^1.15.1",
    "pug": "^2.0.0-beta11"
  }
}

index.js

const express = require('express');
const app = express();
// app.use(express.static('public'));//把 pubilc/ 文件夹 架设成一个静态页面

const session = require('express-session')
app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true
}))//使用 req.session 接口

const bodyParser =require('body-parser')
app.use(bodyParser.urlencoded({extended:false}))//parse application/x-www-form-urlencoded

const pug = require('pug');
app.set('view engine','pug')

app.get('/',function (req,res) {
  console.log('home page',req.session.username);
  // res.sendFile('index.html',{root:'public'})
  let currentUser = req.session.username;
  res.render('index',{username:currentUser})
})
app.get('/login',function (req,res) {
  res.sendFile('login.html',{root:'public'})
})
app.get('/logout',function (req,res) {
  req.session.destroy();
  res.redirect('/');
})
app.post('/login',function (req,res) {
  let username = req.body.username;
  req.session.username = username;
  //User.find({username:username})
  //如果数据库中能找到这个用户,同事密码匹配,这样才算登陆成功
  if(true){
    res.redirect('/')
  }//页面重定向
})

app.listen(3006,function () {
  console.log('running on port 3006...');
})

pubilc/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <a href="/login">login</a>
</body>
</html>

pubilc/login.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>login</title>
</head>
<body>
  <form action="/login" method="post">
    <label for="username">用户名</label>
    <input type="text" name="username">
    <input type="submit">
  </form>
</body>
</html>

views/index.pug

html
  style
    include main.css
  body
    if username
      p 你好,
        span= username
          pre !欢迎登录!
            a(href='./logout') 退出
    else
      p 你好,
        a(href='./login') 请登录

views/main.css

span{
  color: red;
}