Node 缓存、安全与鉴权
- 1、Cookie
-
- [1.1 Set-Cookie](#1.1 Set-Cookie)
- [1.2 Cookie 的生命周期](#1.2 Cookie 的生命周期)
- [1.3 如何保证Cookie安全性](#1.3 如何保证Cookie安全性)
- [1.4 Cookie 的作用域](#1.4 Cookie 的作用域)
-
- [Domain 属性](#Domain 属性)
- [Path 属性](#Path 属性)
- [1.5 SameSite attribute](#1.5 SameSite attribute)
- [1.6 JS操作Cookie](#1.6 JS操作Cookie)
- [1.7 安全性](#1.7 安全性)
- [2、 Node缓存](#2、 Node缓存)
-
- [2.1 缓存作用](#2.1 缓存作用)
- [2.2 缓存类型](#2.2 缓存类型)
- 3、Node鉴权
-
- [3.1 HTTP Basic Authentication](#3.1 HTTP Basic Authentication)
- [3.2 session-cookie](#3.2 session-cookie)
-
- [3.2.1 cookie](#3.2.1 cookie)
- [3.2.2 session](#3.2.2 session)
- [3.3 Token 验证](#3.3 Token 验证)
-
- [3.3.1 Token认证流程](#3.3.1 Token认证流程)
- [3.3.2 Token和session的区别](#3.3.2 Token和session的区别)
- [3.4 OAuth(开放授权)](#3.4 OAuth(开放授权))
- [3.4.1 OAuth认证流程](#3.4.1 OAuth认证流程)
- [3.4.2 GitHub第三方登录示例](#3.4.2 GitHub第三方登录示例)
1、Cookie
服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。
Cookie 作用:
- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息);
- 个性化设置(如用户自定义设置、主题等);
- 浏览器行为跟踪(如跟踪分析用户行为等);
当服务器收到 HTTP 请求时,服务器可以在响应头里面添加一个 Set-Cookie
选项。浏览器收到响应后通常会保存下 Cookie
,之后对该服务器每一次请求中都通过 Cookie 请求头部将 Cookie 信息发送给服务器
。
1.1 Set-Cookie
Set-Cookie: <cookie 名>=<cookie 值>
服务器通过该头部告知客户端保存 Cookie 信息
javascript
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
[页面内容]
对该服务器发起的每一次新请求,浏览器都会将之前保存的 Cookie 信息通过 Cookie 请求头部再发送给服务器。
javascript
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
1.2 Cookie 的生命周期
Cookie 的生命周期包括:
- 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。会话期 Cookie 不需要指定过期时间(Expires)或者有效期(Max-Age);
- 持久性 Cookie:生命周期取决于过期时间(Expires)或有效期(Max-Age);
javascript
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
提示:当 Cookie 的过期时间被设定时,设定的日期和时间只与客户端相关,而不是服务端。
1.3 如何保证Cookie安全性
Secure 属性和HttpOnly 属性
- Secure: 表示只应通过被 HTTPS 协议加密过的请求发送给服务端;
- HttpOnly:JavaScript Document.cookie API 无法访问带有 HttpOnly 属性的 cookie;此类 Cookie 仅作用于服务器。例如,持久化服务器端会话的 Cookie 不需要对 JavaScript 可用,而应具有 HttpOnly 属性。
javascript
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
1.4 Cookie 的作用域
Domain 属性
Domain 指定了哪些主机可以接受 Cookie。如果不指定,默认为 origin,不包含子域名。如果指定了Domain,则一般包含子域名。因此,指定 Domain 比省略它的限制要少;
例如,如果设置 Domain=chenghuai.com,则 Cookie 也包含在子域名中(如dev.chenghuai.com)
Path 属性
Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)
例如,设置 Path=/a,则以下地址都会匹配:
- /a
- /a/b/
- /a/b/c
1.5 SameSite attribute
SameSite Cookie 允许服务器要求某个 cookie 在跨站请求时不会被发送,从而可以阻止(CSRF)。
Set-Cookie: key=value; SameSite=Strict
SameSite 可以有下面三种值:
- None:浏览器会在同站请求、跨站请求下继续发送 cookies,不区分大小写;
- Strict:浏览器将只在访问相同站点时发送 cookie;
- Lax:规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。
导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。
1.6 JS操作Cookie
通过 Document.cookie 属性可创建新的 Cookie,也可通过该属性访问非HttpOnly标记的 Cookie
javascript
document.cookie = "user=chenghuai";
document.cookie = "tasty_cookie=strawberry";
console.log(document.cookie);
// logs "user=chenghuai; tasty_cookie=strawberry"
1.7 安全性
减少 Cookie 的攻击的方法:
- 使用 HttpOnly 属性可防止通过 JavaScript 访问 cookie 值;
- 用于敏感信息(例如指示身份验证)的 Cookie 的生存期应较短,并且 SameSite 属性设置为Strict 或 Lax;
2、 Node缓存
2.1 缓存作用
1.为了提高速度,提高效率;
2.减少数据传输,节省网费;
3.减少服务器的负担,提高网站性能;
4.加快客户端加载网页的速度;
2.2 缓存类型
强制缓存
当客户端请求后,会先访问缓存数据库看缓存是否存在。如果存在则直接返回,不存在则请求真的服务器。
可以造成强制缓存的字段是 Cache-control 和 Expires
Expires
这是 HTTP 1.0 的字段,表示缓存到期时间,是一个绝对的时间 (当前时间+缓存时间)。在响应消息头中,设置这个字段之后,就可以告诉浏览器,在未过期之前不需要再次请求。
比如:Expires: Thu, 22 Mar 2029 16:06:42 GMT
缺点:若修改电脑的本地时间,会导致浏览器判断缓存失效 这里修重新修改缓存
Cache-control
在得知Expires的缺点之后,在HTTP/1.1中,增加了一个字段Cache-control,该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求
Q: Expires 和 Cache-control 区别是什么?
- Expires设置的是 绝对时间 Cache-control设置的是 相对时间;
- Cache-control 优先级 大于Expires
Cache-control: max-age=20 // 表示有效时间为20s
res.setHeader('Cache-control', 'no-store')
res.setHeader('Cache-control', 'max-age=20')
cache-control设置:
- no-cache:告诉浏览器忽略资源的缓存副本,强制每次请求直接发送给服务器,拉取资源,但不是"不缓存",相当于需要使用协商缓存,禁止使用强制缓存;
- no-store:强制缓存在任何情况下都不要保留任何副本,相当于不使用强制缓存和协商缓存;
- public 任何路径的缓存者(本地缓存、代理服务器),可以无条件的缓存改资源,不设置默认为public;
- private 只针对单个用户或者实体(不同用户、窗口)缓存资源;
对比缓存(协商缓存)
当强制缓存失效(超过规定时间)时,就需要使用对比缓存,由服务器决定缓存内容是否失效。对比缓存是可以和强制缓存一起使用。
last-modified
- 服务器在响应头中设置last-modified字段返回给客户端,告诉客户端资源最后一次修改的时间;
- Last-Modified: Sat, 30 Mar 2029 05:46:11 GMT
- 浏览器在这个值和内容记录在浏览器的缓存数据库中;
- 下次请求相同资源,浏览器将在请求头中设置if-modified-since的值(这个值就是第一步响应头中的Last-Modified的值)传给服务器;
- 服务器收到请求头的if-modified-since的值与last-modified的值比较,如果相等,表示未进行修改,则返回状态码为304;如果不相等,则修改了,返回状态码为200,并返回数据;
缺点:
- last-modified是以秒为单位的,假如资料在1s内可能修改几次,那么该缓存就不能被使用的;
- 如果文件是通过服务器动态生成,那么更新的时间永远就是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用;
Etag
Etag是根据文件内容,算出一个唯一的值。服务器存储着文件的 Etag 字段。
之后的流程和 Last-Modified 一致,只是 Last-Modified 字段和它所表示的更新时间改变成了 Etag 字段和它所表示的文件 hash,把 If-Modified-Since 变成了 If-None-Match。
服务器同样进行比较,命中返回 304, 不命中返回新资源和 200。 Etag 的优先级高于 Last-Modified
缺点:
- 每次请求的时候,服务器都会把文件读取一次,以确认文件有没有修改;
- 大文件进行etag 一般用文件的大小 + 文件的最后修改时间 来组合生成这个etag;
3、Node鉴权
目前常用的鉴权有四种:
- HTTP Basic Authentication
- session-cookie
- Token 验证
- OAuth(开放授权)
3.1 HTTP Basic Authentication
优点:
- 所有流行的网页浏览器都支持基本认证,但基本认证很少在可公开访问的互联网网站上使用,有时候会在小的私有系统中使用(如路由器网页管理接口);
- 开发时使用基本认证,是使用Telnet或其他明文网络协议工具手动地测试Web服务器。因为传输的内容是可读的,以便进行诊断;
缺点:
- 由于用户 ID 与密码是是以明文的形式在网络中进行传输的(尽管采用了 base64 编码,但是 base64 算法是可逆的),所以基本验证方案并不安全,如果没有使用SSL/TLS这样的传输层安全的协议,那么以明文传输的密钥和口令很容易被拦截。该方案也同样没有对服务器返回的信息提供保护;
- 现在的浏览器保存认证信息直到标签页或浏览器被关闭,或者用户清除历史记录。HTTP没有为服务器提供一种方法指示客户端丢弃这些被缓存的密钥。这意味着服务器端在用户不关闭浏览器的情况下,并没有一种有效的方法来让用户注销;
3.2 session-cookie
3.2.1 cookie
Http协议是一个无状态的协议,服务器不会知道到底是哪一台浏览器访问了它,因此需要一个标识用来让服务器区分不同的浏览器。cookie 就是这个管理服务器与客户端之间状态的标识。
cookie原理:
- 浏览器第一次向服务器发送请求,服务器在 response 头部设置 Set-Cookie 字段;
- 浏览器客户端收到响应就会设置 cookie 并存储;
- 在下一次该浏览器向服务器发送请求时,就会在 request 头部自动带上 Cookie 字段,服务器端收到该 cookie 用以区分不同的浏览器;
3.2.2 session
session 是会话的意思,浏览器第一次访问服务端,服务端就会创建一次会话,在会话中保存标识该浏览器的信息。它与 cookie 的区别就是 session 是缓存在服务端的,cookie 则是缓存在客户端,他们都由服务端生成,为了弥补 Http 协议无状态的缺陷。
session-cookie认证
- 服务器在接受客户端首次访问时在服务器端创建seesion,然后保存seesion(我们可以将seesion保存在 内存中,也可以保存在redis中,推荐使用后者),然后给这个session生成一个唯一的标识字符串,然后在 response header 中种下这个唯一标识字符串;
- 签名。这一步通过秘钥对sid进行签名处理,避免客户端修改sid;(非必需步骤)
- 浏览器中收到请求响应的时候会解析响应头,然后将sid保存在本地cookie中,浏览器在下次http请求的请求头中会带上该域名下的cookie信息。
- 服务器在接受客户端请求时会去解析请求头cookie中的sid,然后根据这个sid去找服务器端保存的该客户端的session,然后判断该请求是否合法。
redis
redis是一个键值服务器,可以专门放session的键值对。如何在koa中使用session:
javascript
const koa = require('koa')
const session = require('koa-session')
const redisStore = require('koa-redis')
const redis = require('redis')
const wrapper = require('co-redis')
const app = new koa()
const redisClient = redis.createClient(6379, 'localhost')
const client = wrapper(redisClient)
//加密sessionid
app.keys = ['session secret']
const SESS_CONFIG = {
key: 'kbb:sess',
// 此时让session存储在redis中
store: redisStore({ client })
}
app.use(session(SESS_CONFIG, app))
app.use(ctx => {
// 查看redis中的内容
redisClient.keys('*', (errr, keys) => {
console.log('keys:', keys)
keys.forEach(key => {
redisClient.get(key, (err, val) => {
console.log(val)
})
})
})
if (ctx.path === '/favicon.ico') return
let n = ctx.session.count || 0
ctx.session.count = ++n
ctx.body = `第${n}次访问`
})
app.listen(3000)
用户登录认证
使用session-cookie做登录认证时,登录时存储session,退出登录时删除session,而其他的需要登录后才能操作的接口需要提前验证是否存在session,存在才能跳转页面,不存在则回到登录页面。
- 在koa中做一个验证的中间件,在需要验证的接口中使用该中间件。
javascript
//前端代码
async login() {
await axios.post('/login', {
username: this.username,
password: this.password
})
},
async logout() {
await axios.post('/logout')
},
async getUser() {
await axios.get('/getUser')
}
javascript
//中间件 auth.js
module.exports = async (ctx, next) => {
if (!ctx.session.userinfo) {
ctx.body = {
ok: 0,
message: "用户未登录" };
} else {
await next();
}
};
//需要验证的接口
router.get('/getUser', require('auth'), async (ctx) => {
ctx.body = {
message: "获取数据成功",
userinfo: ctx.session.userinfo
}
})
//登录
router.post('/login', async (ctx) => {
const {
body
} = ctx.request
console.log('body', body)
//设置session
ctx.session.userinfo = body.username;
ctx.body = {
message: "登录成功"
}
})
//登出
router.post('/logout', async (ctx) => {
//设置session
delete ctx.session.userinfo
ctx.body = {
message: "登出系统"
}
})
3.3 Token 验证
token 是一个令牌,浏览器第一次访问服务端时会签发一张令牌,之后浏览器每次携带这张令牌访问服务端就会认证该令牌是否有效,只要服务端可以解密该令牌,就说明请求是合法的,令牌中包含的用户信息还可以区分不同身份的用户。一般 token 由用户信息、时间戳和由 hash 算法加密的签名构成。
3.3.1 Token认证流程
- 客户端使用用户名跟密码请求登录;
- 服务端收到请求,去验证用户名与密码;
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端;
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者Local Storage 里;
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token;
- 服务端收到请求,然后去验证客户端请求里面带着的 Token(request头部添加Authorization),如果验证成功,就向客户端返回请求的数据 ,如果不成功返回401错误码,鉴权失败;
3.3.2 Token和session的区别
session-cookie的缺点:
- 认证方式局限于在浏览器中使用,cookie 是浏览器端的机制,如果在app端就无法使用 cookie;
- 为了满足全局一致性,我们最好把 session 存储在 redis 中做持久化,而在分布式环境下,我们可能需要在每个服务器上都备份,占用了大量的存储空间;
- 在不是 Https 协议下使用 cookie ,容易受到 CSRF 跨站点请求伪造攻击。
token的缺点:
- 加密解密消耗使得 token 认证比 session-cookie 更消耗性能;
- token 比 sid 大,更占带宽;
两者对比,它们的区别显而易见:
- token 认证不局限于 cookie ,这样就使得这种认证方式可以支持多种客户端,而不仅是浏览器。且不受同源策略的影响;
- 不使用 cookie 就可以规避CSRF攻击;
- token 不需要存储,token 中已包含了用户信息,服务器端变成无状态,服务器端只需要根据定义的规则校验这个 token 是否合法就行。这也使得 token 的可扩展性更强。
3.4 OAuth(开放授权)
OAuth(Open Authorization)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容,为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。我们常见的提供OAuth认证服务的厂商有支付宝,QQ,微信。
3.4.1 OAuth认证流程
OAuth就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。
在前后端分离的情况下,我们常使用授权码方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。