目录
Cookie的安全性
Cookie真的安全吗
Cookie内容为什么不能是用户名和密码
Session
Session的实现
session存储在内存中的优缺点
session存储在数据库中的实践
session存储在数据库中的优缺点
cookie由于保存在浏览器端,即用户个人电脑上,所以非常容易受到各种恶意攻击,而导致用户cookie被盗用·,为此浏览器为cookie设置了一些安全属性来限制对于cookie的操作以及cookie的发送。
httponly限制了cookie只能用于HTTP请求时携带,而不能使用javascript操作,如document.cookie操作cookie,这样可以有效保护cookie不被XSS的注入js脚本盗取。
secure限制了cookie只能用于HTTPS请求时携带,这样保护了cookie不会在网络中明文传输,即避免了网络传输被截获后,黑客可以直接得到明文cookie。
samesite默认限制了cookie在大部分HTTP请求中只能用于同站发送,比如浏览器网页请求的:
只有少部分情况时,比如a标签超链接跳转GET请求,form表单GET请求,这种导航跳转式的GET请求才可以跨站发送cookie。
所以samesite可以有效保障cookie不会被跨站请求伪造CSRF利用。
可能大家觉得cookie有了上面三个安全属性控制,已经很安全了,但其实不然,举个很容易理解的例子:
小明去网吧上网,登录某网站,并手贱地勾选了七天免登录,使用完后,小明直接关闭了浏览器,而没有清除浏览器cookie缓存。
然后,小王又使用了这台电脑,翻看了浏览器历史记录,然后点击了小明曾登录的网站,意外地发现可以免登录,且当前登录用户是小明,而小王又是一个程序员,他熟练地打开浏览器的cookie窗口,并发现了cookie中有类似于username和password的密文信息,于是它试着对username和password进行了破译,最终成功破译得到了明文的用户名和密码。
上面这个例子,小明用户名密码的泄漏原因并非来自于网络上的攻击,而是来自于网络外的攻击,由于小明的不良使用习惯,对cookie没有安全意识,导致了cookie泄漏,而这里的cookie内容又是用户名密码,进一步增加了用户名密码泄漏的风险。
所以,cookie不安全的根本原因是cookie将用户身份认证信息保存在了客户端主机上,而客户端主机没有像服务器主机一样的双重防护,即网络外的安全规范的人为操作 + 网络内的安全严谨的技术保障。虽然浏览器尽力在为客户端主机提供安全的技术保障,但是却无法强制每个用户都能进行安全规范的操作。
上面例子中,cookie的内容是用户名和密码,那必然决定了服务器获取到cookie后会基于解析出的用户名和密码进行身份认证。这产生了两个严重的安全问题:
为什么cookie有效期无意义了呢?
由于服务器是根据cookie解析出的用户名和密码进行身份认证的,所以有了cookie中的用户名和密码在任何时候都可以用于身份认证。
另外浏览器端支持手动修改cookie有效期,这可能导致cookie有效期被篡改而长期有效。
Cookie身份认证机制中,服务器端不需要记录生成cookie的有效期,cookie有效期的管理全权交给了浏览器,但是浏览器请求服务器时只会发送cookie内容,而不会发送cookie有效期属性,这导致了服务器无法验证cookie的有效期。
那么如何基于Cookie进行安全的身份认证呢?
首先,服务器不能将用户名和密码作为cookie交给浏览器保存,其次服务器需要记录生成cookie的有效期,不能依赖于浏览器管理cookie的有效期。
所以基于Cookie,产生了Session身份认证技术。
Session身份认证技术脱胎于Cookie,它主要是解决Cookie上述两个安全隐患,所以Session工作流程如下:
这里我们将保存在服务器端的用户登录状态数据称为session。
可以分析出,浏览器端不再保存实际的登录状态数据,只是保存了一个无业务意义的随机串sessionid,所以Session技术的安全性要比Cookie强。
sessionid的作用是,作为凭据交给浏览器端保存,解决了浏览器端cookie被破解后暴露出明文用户名密码的安全问题。sessionid还有一个作用就是,在服务器端,和session组成键值对,也可以理解为查找session的索引值。
Session技术实际上就是将登录状态数据从浏览器端转移到服务器端保存,即需要在服务器端考虑登录状态数据session的管理,具体管理行为可以参照浏览器端cookie的管理:
所以服务器端也需要对session考虑以上管理行为
服务器端既可以将session缓存在内存中,也可以将session持久化在硬盘中。但是缓存在内存中的session的生命周期不一定是会话级别的,持久化在硬盘中的session也可能是会话级别的。
为什么会这样呢?我们需要先了解会话生命周期的概念:
cookie的会话生命周期指的是:从浏览器请求服务器得到cookie开始,到浏览器关闭为止。
会话级别的cookie一定是保存在浏览器内存中的,而会话结束,意味着cookie要失效清除,同时会话结束,也意味着浏览器关闭,浏览器关闭会释放自身内存,此时内存中的cookie也会被清除,这就是会话级别cookie保存在浏览器内存中的原因。
类比可得,保存在服务器上的session会话生命周期指的是:从服务器接收浏览器请求生成session开始,到服务器关闭为止。
而服务器不会轻易关闭,这会导致session的会话生命周期很长,影响安全性。同时服务器也不会为了控制会话时间跨度而关闭自身。session理论上不存在会话生命周期。
但是一般我们可以设定固定时间为一个会话周期,比如20分钟为一次会话周期,20分钟到后,服务器强制清除对应session。
所以服务器端将session生命周期和其保存位置无关。
服务器端在保存session时,会生成一个sessionid,该sessionid是一个随机唯一的字符串,作用是作为在服务器端查询session的索引,和在浏览器端作为免登录的凭证。
所以服务器端获取session靠的是sessionid。
无论是cookie还是session,理论上都应该在其失效时清除,而不同的是清除手段。对于cookie而言,如果存储在内存中,则可以通过关闭浏览器释放内存来间接清除,如果存储在硬盘中,则需要浏览器内部代码逻辑清除。对于session,存储位置与生命周期无关,所以都只能通过代码清除,清除时,需要检查对应的session的有效期。
下面是基于express和cookie-parser开发的session中间件
const uuid = require('uuid').v1
module.exports = function(options = {}){
// session store
const SESSION = {}
return function(req, res, next) {
// 获取浏览器请求中的cookie:sessionid值,若首次访问,则sid为undefined,若非首次访问,则sid有值
let sid = req.cookies.sessionid
// 由于并不是每次浏览器请求都需要服务器响应set-cookie头,所以使用一个needCookie标识
let needCookie = false
// 根据sid到session_store中查询是否有对应session
let session = SESSION[sid]
// 如果session_store没有对应session 或者 session_store有对应session,但是已经失效,则需要重新生成新的sid和session,并且需要重新set-cookie给浏览器
if(!session || (+session.expires <= Date.now())) {
delete SESSION[sid]
sid = uuid()
needCookie = true
session = SESSION[sid] = Object.create(null)
}
// 由于session有效期要与cookie:sessionid的有效期保持一致,所以只能以服务器端设置的有效期为准,且只能使用有效日期,而不是存活时间maxAge
let expires
if(options.maxAge) {
expires = new Date(Date.now() + options.maxAge)
delete options.maxAge
options.expires = expires
} else if(options.expires) {
expires = options.expires
} else {
expires = new Date(Date.now() + 20 * 60 * 1000)
}
session.expires = expires
// 提供修改session有效期的入口,方便实现七天免登录这样的功能
session.setExpires = function(exp) {
session.expires = exp
options.expires = exp
needCookie = true
}
// 提供删除session的入口,方便实现注销登录的功能
session.destory = function(){
delete SESSION[sid]
}
req.session = session
if(needCookie) {
res.cookie('sessionid', sid, options)
}
next()
}
}
实现session身份认证机制-Node.js文档类资源-CSDN文库
以上是0积分可下载的示例项目链接。
优点:
缺点:
前面说过session既可以存储在服务器内存中,也可以存储在服务器本地硬盘中。常见的存储在服务器本地硬盘的形式有:文件和数据库。
其中数据库可以更好地管理数据,所以我们一般选择将session存储在数据库中。
下面是基于node express框架开发的将session存储进Mongodb数据库的中间件代码
const uuid = require('uuid').v1
const mongoose = require('mongoose')
const sessionSchema = new mongoose.Schema({
sessionid: {
type: String,
required: true
},
expires: {
type: Date,
default: new Date(Date.now() + 20 * 60 * 1000)
},
user: Object
})
const Session = mongoose.model('session', sessionSchema, 'session')
Session.prototype.setExpires = function(exp, res) {
this.expires = exp
res.setHeader('set-cookie', `sessionid=${this.sessionid};expires=${exp.toUTCString()};path=/;`)
}
Session.prototype.destory = async function(){
await this.deleteOne()
}
module.exports = function(options = {}) {
return async function(req, res, next) {
let sid = req.cookies.sessionid
let needCookie = false
let session = await Session.findOne({sessionid:{$eq:sid}})
if(!session || (+session.expires <= Date.now())) {
if(session){
await session.deleteOne()
}
if(options.maxAge) {
options.expires = new Date(Date.now() + options.maxAge)
delete options.maxAge
}
session = new Session({
sessionid: uuid(),
expires: options.expires
})
await session.save()
sid = session.sessionid
needCookie = true
}
req.session = session
if(needCookie) {
res.cookie('sessionid', sid, options)
}
next()
}
}
session存储到数据库实践-Node.js文档类资源-CSDN文库
0积分可下载的实践项目
优点
缺点
概念
session和cookie都是用户登录状态数据
工作流程
cookie由服务器生成,通过响应头Set-Cookie传递给浏览器,cookie由浏览器保存,每次请求服务器时,浏览器都会通过请求头Cookie将对应的cookie传递给服务器,服务器基于请求头Cookie信息进行身份认证。
session由服务器生成,并且保存在服务器,服务器在生成session的同时会生成与之一一对应的sessionid,sessionid有两个作用,一是在服务器端作为查找session的索引,二是作为登录凭据通过响应头Set-Cookie传递给浏览器,并保存在浏览器端。浏览器每次请求服务器都会通过请求头Cookie将sesionid传递给服务器,服务器通过sessionid查询到服务器端保存的session,并基于session进行身份认证。
安全性
在身份认证实践中,session比cookie更加安全,因为session机制会将用户登录状态数据保存在服务器端,浏览器端只有一个无意义字串sessionid。而cookie机制将用户登录状态数据保存在浏览器端。黑客盗取浏览器保存的cookie,要比盗取服务器保存的session,容易的多。
同时,cookie机制中,服务器无法验证cookie有效期,cookie有效期完全由浏览器管理,同时浏览器具备多种入口篡改cookie有效期,这可能导致cookie有效期被故意篡改导致长期有效。
而session机制中,服务器端可以在生成session时,保持session的有效期和作为cookie的sessionid的有效期一致,并且以服务器端保存的session的有效期为准,这保障了即使cookie:sessionid在浏览器端被篡改的长期有效,但是服务器端session的有效期却无法被篡改。
管理难度
cookie保存在浏览器端,浏览器已经有了一套成熟的管理机制:
保存:自动将服务器HTTP响应头中Set-Cookie信息保存在浏览器内存或硬盘中
获取:自动将保存在浏览器内存或硬盘中的cookie取出作为HTTP请求头Cookie发生给服务器
清除:自动清除内存或硬盘中失效的cookie
而session保存在服务器端,服务器本身没有内置管理session的机制,需要第三方工具或者人为开发管理代码
性能方面
另外cookie是将每个用户的登录状态数据分散保存在用户个人电脑上,对于服务器来说内存友好,性能友好。
session是将每个用户的登录状态数据集中保存在服务器内存或服务器连接的数据库中,对于服务器来说内存不友好,性能也不友好。
大部分跨站请求时,浏览器是禁止HTTP请求携带目的站点cookie的。
而session是基于cookie工作的,所以发生跨站请求时,session也是无法工作的。
另外浏览器提供了cookie禁用设置渠道,所以一旦用户禁用了cookie,那么session也是无法工作的。
express-session是express推荐的用于服务器端session管理的模块,下面是express-session使用示例
在express服务器中引入express-session模块,express-session模块对外暴露一个函数,如上图const session = require('express-session')中的session就是一个函数,该函数:
我们将该函数返回的express中间件注册到express服务器实例app上,则在服务器定义的HTTP请求对象req上会挂载一个session对象,我们可以通过req.session来操作服务器端session。
下面介绍一下入参配置对象的属性,及其作用
name | 可选属性,用于指定保存在浏览器cookie中的sessionid的名字,默认为'connect.sid' |
secret | 必选属性,用于指定保存在浏览器cookie中的sessionid的加密密钥,用于签名,检查sessionid是否被篡改,可以是一个字符串,或者一个数组,如果是一个数组,则选择数组第一个元素作为secret,secret需要尽可能地复杂,难以猜测,这样才能保障sessionid的安全 |
resave | 可选属性,默认值为true,表示强制将session保存回session store,即使session并没有发生改变。但是每次强制将session保存回session store会造成并行情况下的竞争,比如同一时刻,发生了两条请求,一条修改了session,一条没有修改session,则此时很难预测session store中的session是否会被发生改变。所以当前resave默认true的设定已经被废弃,我们需要考虑自己的session store是否实现了touch方法,是否为session设定了过期时间,然后才可以决定是否可以将resave设置为false |
saveUninitialized | 可选属性,默认值为true,表示强制将未初始化的session保存到session store。但是目前默认值true已经被废弃。 |
cookie | 可选属性,值是一个配置对象,用于指定保存在浏览器端cookie:sessionid的属性domain,path,maxAge,expires,httponly,secure,samesite |
rolling | 可选属性,默认值为false,当设置为true时,表示每次服务器响应时都会携带cookie:sessionid,且sessionid的有效期被重置为初始maxAge值,服务器端session也会被重置为初始maxAge值。 需要注意的是,当rolling设置为true,但是saveUninitialized被设置为false,未初始化的响应则不会携带cookie。 |
store | 可选属性,用于指定session store,默认的session store为服务器内存,此时需要注意服务器内存是否可能被撑爆,所以建议不要使用服务器内存作为session store。建议选择数据库作为session store,如redis,mongodb,mysql等,目前npm社区也很对express-session开发了上述数据库的接口,如connect-mongo,connect-redis,express-mysql-session |
genid | 可选属性,用于指定生成sessionid的函数,该函数的返回值将作为存放在浏览器端cookie中的sessionid的原始值,默认采用uid-safe |
proxy | 可选属性,在设置secure cookie时信任反向代理(通过“x - forward - proto” 头) 默认值为undefined,表示使用express的“信任代理”设置 可选值true,表示将使用x-forward-proto 可选值false,表示忽略所有头,只有tls/ssl传输是是安全的 |
unset | 可选属性,默认值为keep,可选值为destroy。 当unset为destory时,表示每次响应结束都会删除session. 当unset为keep时,表示session会被保留在session store中。 |
其中常用的有secret,name,cookie,rolling,store。
上面配置中name被指定为sessionid,所以在浏览器端存储的cookie的名称就是sessionid
浏览器端保存的cookie:sessionid的值为
s:HYm7whqjvhQ1CQVFCItHTNVTi_47CroP.t7ytiOOMSYIsLww5afl1dKXlTr0nh9bgEPZuhxmrDQo
发现该值被:和.分为了三段,可以猜测第一段值:s,可能是指定签名算法,第二段是sessionid的值,即存入session store的_id的值,第三段就是根据secret和指定签名算法对sessionid值进行加密的签名,这保证了第二段sessionid值一旦被篡改,服务器端可以通过第三段签名值判断出来。
另外从session store存储的session信息可以分析
{
"_id" : "HYm7whqjvhQ1CQVFCItHTNVTi_47CroP",
"expires" : ISODate("2022-03-23T11:57:21.390Z"),
"session" : "{\"cookie\":{\"originalMaxAge\":604800000,\"expires\":\"2022-03-23T11:57:21.135Z\",\"httpOnly\":true,\"path\":\"/\"},\"user\":{\"name\":\"qfc\",\"pass\":\"123456\"}}"
}
_id就是sessionid值,
expires是session有效期,
session分为cookie和user,其中cookie就是cookie:sessionid的属性,user就是登录状态数据
req.session可以用来操作session store,比如增删改查session store中的session信息。
req.session.destroy(callback) | 删除当前req.session在session store中的值 |
req.session.reload(callback) | 从session store中重载session对象到req.session上 |
req.session.save(callback) | 将req.session保存会session store,但是通常该方法不需要调用,因为在服务器响应前,都会自动调用该方法。 |
req.session.regenerate(callback) | 重新生成一个sessionid和session,并且会重载session到req.session上 |
req.session.touch() | 该方法用于更新session的maxAge,通常不需要调用,由其他中间件调用 |
req.sessionID | 只读属性,获取当前请求对应的sessionid |
req.session.id | req.sessionID的别名 |
req.session.cookie.maxAge | 重置session以及cookie:sessionid的存活时间,单位为毫秒数 |
req.session.cookie.expires | 重置session以及cookie:sessionid的有效日期,值为Date类型 |
req.session.cookie.originalMaxAge | 用于设置session以及cookie:sessionid的默认存活时间 |