用户登录功能
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;
}