1、很久以前,web大多是文档的浏览,所以服务器不需要记录谁访问了什么文档,每一次http请求都是一次全新的请求,不需要记录。
2、交互式web应用兴起,如在线购物网站,需要登录的网站,这些就需要管理回话,记录谁登陆了,做了什么事。所以服务器就想到给每一个人发一个会话标识(session id),就是一个随机的字符串,每个人的都不一样,发起http请求时携带上这个字符串,就可以区分谁是谁了。
3、这种方式对用户友善,但对服务器就很不友善了,成千上万的session严重影响了服务器的性能。使用不同的机器访问需要session的复制,非常麻烦;有人提出将session id 集中存储到一个地方,所有的机器都能访问,不用复制了,但如果该机器挂掉,所有人都需要重新登录一遍。一直没有很好地解决方案。
1、有服务器想,我为什么要保存session id,让客户端去保存该多好,但如果没有session id 如何验证合法用户哪?关键在于验证,假如A登录了,我给他一个令牌(token),里面包含了user id ,A下次http请求时通过http header带上token就可以验证了。但这和session id 没有本质区别,人人都可以伪造。
2、可以使用某个算法和一个只有我知道的密钥,对数据做一个签名,签名和数据一起作为token,因为别人不知道密钥,就无法伪造token了。
3、token我不保存,我只保存算法和密钥,当A带着token访问时,我用相同的算法和密钥对数据再次签名,如果得到的签名和A携带的签名一样,就说明A是合法用户,已经登录过了;如果不同,说明数据被人篡改过。
4、token 中的数据时铭文保存的,可以被别人看到,因此不能保存像密码那样的敏感信息。当然,如果token被偷走了,我会认为小偷是合法用户,这和session id被偷走是一样的。
通俗地讲就是验证当前用户的身份,证明“你是你自己”(比如:你每天上下班打卡,都需要通过指纹打卡,当你的指纹和系统里录入的指纹相匹配时,就打卡成功)
互联网中的认证:
用户授予第三方应用访问该用户某些资源的权限
实现授权的方式有:cookie、session、token、OAuth
实现认证和授权的前提是需要一种媒介(证书) 来标记访问者的身份
在一次会话中,可以向服务器发出N次请求。通过某个机制,可以让服务器和客户端建立联系。这种机制叫会话机制
如果没有会话机制,就没有现的互联网。
会话机制也不属于任何一门一样,是一门独立的技术。
解决的是保存用户浏览页面状态的问题。
任何一门后台语言,都实现了会话机制的。
会话:在多次HTTP连接间维护用户与同一用户发出的不同请求之间关联的情况称为维护一个会话(session)。
cookie是基于浏览器端的会话技术
session是基于服务端的会话技术
session通常都会依赖cookie。
HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每次请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法辨别上一次请求的发送者和这一次请求的发送者是否是同一人。所以服务器和浏览器为了进行会话跟踪(知道是谁在访问我),就必须去维护一个状态,这个状态用于告知服务器前后两次请求是否来自统一浏览器。
Session 和 Cookie 的主要目的就是为了弥补 HTTP 的无状态特性。
cookie 是一个非常具体的东西,指的就是浏览器里面能永久存储的一种数据。
Cookie是基于浏览端的会话技术。将数据保存在浏览器端。
cookie由服务器生成,发送给浏览器,浏览器把cookie以KV形式存储到某个目录下的文本文件中,下一次请求同一网站时会把该cookie发送给服务器。由于cookie是存在客户端上的,所以浏览器加入了一些限制确保cookie不会被恶意使用,同时不会占据太多磁盘空间。所以每个域的cookie数量是有限制的。
cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。
cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)。
属性 | 说明 |
---|---|
name=value | 名值对,表示cookie的名称,必选。//键值对,设置Cookie 的名称及相对应的值,都必须是字符串类型。如果值为 Unicode 字符,需要为字符编码。 如果值为二进制数据,则需要使用 BASE64 编码。 |
domain=xx.com | 控制域名访问,localhost:3000和17.0.0.1:3000不同。指定cookie被发送到哪台计算机上。正常情况下,cookie只被送回最初向用户发送cookie的计算机。如果设置domain=xx.com,则cookie会被发送到任何在xx.com域中的主机。如果domain为空,domain就设置为和提供cookie的服务器相同。如果domain不为空,但它的值和提供cookie的web服务器域名不符,这个cookie将被忽略。//指定 cookie 所属域名,默认是当前域名。 |
path=/directory | 控制后面路由路径访问。 只有访问/directory下面的页面时,cookie才被发送。如果指定了path,但path与当前访问的url不符,则此cookie被忽略。如果缺省,path的属性值为web服务器传给浏览器的资源的路径名。//指定 cookie 在哪个路径(路由)下生效,默认是 ‘/’。如果设置为 /abc,则只有 /abc 下的路由可以访问到该 cookie,如:/abc/read。 |
expires=date | 指定cookie失效的时间,如果没有指定,则cookie不会写入磁盘,只持续到当前会话结束(通常就是关闭浏览器)。该属性值DATE必须以特定的格式来书写:星期几,DD-MM-YY HH:MM:SS GMT,GMT表示这是格林尼治时间。反之,不以这样的格式来书写,系统将无法识别。// 过期时间,在设置的某个时间点后该 cookie 就会失效。一般浏览器的 cookie 都是默认储存的,当关闭浏览器结束这个会话的时候,这个 cookie 也就会被删除 |
secure | 如果设置了,则cookie只能通过ssl通道,即https。//该 cookie 是否仅被使用安全协议传输。安全协议有 HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效。 |
httpOnly | 如果给某个 cookie 设置了 httpOnly 属性,则无法通过 JS 脚本 读取到该 cookie 的信息,但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全 |
maxAge | 设置cookie 失效的时间,单位秒。如果为整数,则该 cookie 在 maxAge 秒后失效。如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie 。如果为 0,表示删除该 cookie 。默认为 -1。- 比 expires 好用。 |
其中,从浏览器端–>服务器端,cookie是以请求头的方式传递。
从服务端–>浏览器端,cookie是以响应头的方式来传递的。
任何一个cookie,都是有有效期的。
默认情况下,有效期是到浏览器关闭,浏览器一关掉,就失效。
Cookie的格式中,有一个expires键,就是用来设置有效期的。
在express中的res.cookie方法中,可以有如下两种设置方式:
都是一个时间戳。从1970-1-1到指定时间的毫秒数。
其中,maxAge用法更加简单,推荐使用。
关掉浏览器之后,只要是1分钟之内,再次打开浏览器,这个cookie是仍然有效的。
如果我们设置了有效期,此时cookie数据是保存到浏览器所在的计算机中的某个磁盘目录中。
使用res对象,cookie是作为响应头信息,从服务端发送到浏览器端的。
发送的标准格式:SetCookie:name=value;expires=date;path=/directory;domain=.xx.com;secure
使用原生的setHeader方法,来设置cookie:
const express = require('express');
const app = express();
app.get('/',(req,res)=>{
res.setHeader('set-cookie','username = 'ange');
res.send('响应中种植了cookie')
})
在express中,cookie的发送,其实已经封装了相应的方法 res.cookie
res.cookie('username','ange')
cookie是作为响应头信息从服务器发送到浏览器端的,需要遵循cookie的格式
浏览器端cookie的获取:
可以使用js ,使用document.cookie
这种cookie的用法,现在逐渐的被localStorage所取代。
服务器端cookie的获取:
任何一次http请求,浏览器都会自己携带cookie,作为请求头,向服务端发送http请求。
服务器接收cookie需要用到req 对象,默认情况下,req对象中没有对cookie进行解析,所以不能直接获取,所以需要第三方中间间 cookie-parser
安装cookie-parser
npm install cookie-parser
引入使用中间件
const cookieParser = require('cookie-parser');
app.use(cookieParser());
获取cookie
console.log(req.cookies)
const express = require("express");
var cookieParser = require('cookie-parser')
const app = express();
app.use(cookieParser())
app.get("/",(req,res)=>{
let last = req.cookies.last;
res.cookie("last",new Date().toLocaleString(),{maxAge:60000*60*24*365});
if(last){
res.send(`你上一次访问的时间是:
${last}`)
}else{
res.send("这是你第1次访问本网站
")
}
})
app.listen(3000)
const express = require("express");
const path = require("path");
var bodyParser = require('body-parser')
var cookieParser = require('cookie-parser')
const app = express();
app.use(cookieParser())
// 配置表单提交的数据
app.use(bodyParser.urlencoded({ extended: false }))
// 配置前端提交的json数据
app.use(bodyParser.json())
app.get("/",(req,res)=>{
res.sendFile(path.join(__dirname,"./views/index.html"))
})
app.get("/mine",(req,res)=>{
if(req.cookies.isLogin){
res.sendFile(path.join(__dirname,"./views/mine.html"))
}else{
res.redirect("/login")
}
})
app.get("/login",(req,res)=>{
res.sendFile(path.join(__dirname,"./views/login.html"))
})
app.post("/doLogin",(req,res)=>{
var username = req.body.username.trim();
var pwd = req.body.pwd.trim();
if(username == "admin" && pwd == "admin"){
res.cookie("isLogin",true,{maxAge:60000*60*24*7})
res.redirect("/mine")
}else{
res.redirect("/login")
}
})
app.get("/logout",(req,res)=>{
res.cookie("isLogin",true,{maxAge:-1})
res.redirect("/login")
})
app.listen(3000)
session认证流程:
SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态
默认的有效期就是当前会话结束(浏览器关闭)。
原因在于,session是基于cookie的,session的有效期其实就是cookie的有效期。
var session = require('express-session')
app.use(session({
secret: 'wangcai', //任意字符串
resave: false, //默认
saveUninitialized: true, //默认
cookie:{ maxAge:60000*10 }
})) // 因为session是基于cookie的,可以为空
设置session
设置,req.session.session名称 = 值
req.session.username = 'ange'
会自动生成一个sessionid,且作为响应头发送给浏览器端
将session信息,保存到服务端的session文件中,文件名就是sessionid
接下来后续的每一次请求,浏览器都会自己携带这个cookie信息,如下:
获取session
获取,req.session.session名称
删除session
删除对应的session,可以使用delete操作。
如果要删除所有的,可以直接将req.session设置为{}。或者调用destroy方法。
const express = require("express");
const path = require("path");
var bodyParser = require('body-parser')
var session = require('express-session')
const app = express();
// 配置session 不需要再配置cookie相关的
app.use(session({
secret: 'wangcai',
resave: false,
saveUninitialized: true,
cookie:{ maxAge:60000*10 } // 因为session是基于cookie的
}))
// 配置表单提交的数据
app.use(bodyParser.urlencoded({ extended: false }))
// 配置前端提交的json数据
app.use(bodyParser.json())
app.get("/",(req,res)=>{
res.sendFile(path.join(__dirname,"./views/index.html"))
})
app.get("/login",(req,res)=>{
res.sendFile(path.join(__dirname,"./views/login.html"))
})
app.post("/doLogin",(req,res)=>{
var username = req.body.username.trim();
var pwd = req.body.pwd.trim();
if(username=="admin" && pwd=="admin"){
req.session.isLogin = true;
res.redirect("/mine")
}else{
res.redirect("/login")
}
})
app.get("/mine",(req,res)=>{
if(req.session.isLogin){
res.sendFile(path.join(__dirname,"./views/mine.html"))
}else{
res.redirect("/login")
}
})
app.get("/logout",(req,res)=>{
req.session.destroy((err)=>{
res.redirect("/login")
})
})
app.listen(3000)
cookie是存储在客户端,session存储在服务端,客户端只存储sessionID。
建议: 将登陆信息等重要信息存放为session, 其他信息如果需要保留,可以放在cookie中。
访问资源接口(API)时所需要的资源凭证
简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)
特点:
每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里
基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
token 完全由应用管理,所以它可以避开同源策略
refresh token 是专用于刷新 access token 的 token。如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。
session是将要验证的信息存储在服务端,并以sessionID和数据进行对应,sessionID由客户端存储,在青丘时将sessionID也带过去,因此实现了状态的对应。
而token是在服务端将用户信息经过Base64Url斑马过后传给客户端,每次用户请求的时候都会带上这一段信息,因此客户端拿到此信息进行解密后就知道此用户是谁了,这个方法叫JWT。
token相较于session的优点在于,当后端系统有多台是,由于客户端访问时直接带着数据,因此不用做共享数据的操作。
详见阮一峰老师的 JSON Web Token 入门教程
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJuYW1lIjoiend6IiwiYWdlIjoiMTgifQ.
UQmqAUhUrpDVV2ST7mZKyLTomVfg7sYkEjmdDI5XF8Q
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature)。
header:
payload:
存放有效信息的地方
signature:
签证信息
密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和验证,所以需要保护好。
服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为
因为 JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要
因为 JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不·需要担心跨域资源共享问题(CORS)
因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制
npm i jsonwebtoken
//安装jwt 模块
let jwt = require('jsonwebtoken');
//定义密钥
const secretKey = 'wangcai';
//生成token,保存两分钟
let tokenStr = jwt.sign({username:username},secreKey,{expiresIn:'120s'});
//token返回客户端
res.send({
token:tokenStr,})
在服务端使用 req.headers 可以获取请求头信息,请求头信息中就有token信息
但一般我们会使用express-jwt 中间件,可以将token字符串解析还原成json对象。
安装
npm i [email protected]
let expressJWT = require('express-jwt');
//配置express-jwt
app.use(expressJwt({secret:secretKey}).unless({path:[/^\/api\//]}))
使用 req.user 就可以还原token中的信息。得到用户信息。
前端获取jwt
后台页面都需要携带 token 验证,可以使用请求拦截器
相同:
区别:
Token验证方式:由于服务器没有存储token数据,因此需要先从数据库中查询当前token,服务器再是验证否有效;
JWT验证方式:直接在服务端进行校验,并且不用查库。因为用户的信息及加密信息,在第二部分payload和第三部分签证中已经生成,只要在服务端进行校验就行,并且校验也是JWT自己实现的。
1.1 概念: 令牌, 是访问资源的凭证。
1.2 Token的认证流程:
1.3 特点:
这种方式的特点就是客户端的token中自己保留有大量信息,服务器没有存储这些信息。
**2.1 概念:**JWT是json web token缩写。可以使用在RESTFUL接口定义,也可以使用在普通的web。它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证。
2.2 组成:
JWT包含三个部分: Header头部,Payload负载和Signature签名。由三部分生成token,三部分之间用“.”号做分割。 列如 : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header 声明信息。 在Header中通常包含了两部分:type:代表token的类型,这里使用的是JWT类型。
alg:使用的Hash算法,例如HMAC SHA256或RSA. { “alg”: “HS256”, “typ”: “JWT” }这会被经过base64Url编码形成第一部分
Payload token的第二个部分是荷载信息,它包含一些声明Claim(实体的描述,通常是一个User信息,还包括一些其他的元数据)声明分三类: 1)Reserved Claims,这是一套预定义的声明,并不是必须的,这是一套易于使用、操作性强的声明。包括:iss(issuer) exp(expirationntime)sub(subject)、aud(audience)等 2)Plubic Claims, 3)Private Claims,交换信息的双方自定义的声明 { “sub”: “1234567890”, “name”: “John Doe”, “admin”: true } 同样经过Base64Url编码后形成第二部分
signature 使用header中指定的算法将编码后的header、编码后的payload、一个secret进行加密。例如使用的是HMAC SHA256算法,大致流程类似于: HMACSHA256( base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)这个signature字段被用来确认JWT信息的发送者是谁,并保证信息没有被修改 。
2.3 验证流程:
在头部信息中声明加密算法和常量,然后把header使用json转化为字符串
在载荷中声明用户信息,同时还有一些其他的内容,再次使用json把在和部分进行转化,转化为字符串
使用在header中声明的加密算法来进行加密,把第一部分字符串和第二部分的字符串结合和每个项目随机生成的secret字符串进行加密,生成新的字符串,此字符串是独一无二的
解密的时候,只要客户端带着jwt来发起请求,服务端就直接使用secret进行解密,解签证解出第一部分和第二部分,然后比对第二部分的信息和客户端穿过来的信息是否一致。如果一致验证成功,否则验证失败。
2.4 特点:
不对。对 session 来说,除非程序通知服务器删除一个 session,否则服务器会一直保留,程序一般都是在用户做 log off 的时候发个指令去删除 session。
然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分 session 机制都使用会话 cookie 来保存 session id,而关闭浏览器后这个 session id 就消失了,再次连接服务器时也就无法找到原来的 session。如果服务器设置的 cookie 被保存在硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 session id 发送给服务器,则再次打开浏览器仍然能够打开原来的 session。
恰恰是由于关闭浏览器不会导致 session 被删除,迫使服务器为 session 设置了一个失效时间,当距离客户端上一次使用 session 的时间超过这个失效时间时,服务器就认为客户端已经停止了活动,才会把 session 删除以节省存储空间。