前后端接口鉴权全解 Cookie/Session/Token 的区别
不知不觉也写得比较长了,一次看不完建议收藏夹!本文主要解释与请求状态相关的术语(cookie、session、token)和几种常见登录的实现方式,希望大家看完本文后可以有比较清晰的理解,有感到迷惑的地方可以点击阅读原文在评论区提出~
Cookie
众所周知,http 是无状态协议,浏览器和服务器不可能凭协议的实现辨别请求的上下文。
于是 cookie 登场,既然协议本身不能分辨链接,那就在请求头部手动带着上下文信息吧。
举个例子,以前去旅游的时候,到了景区可能会需要存放行李,被大包小包压着,旅游也不开心啦。在存放行李后,服务员会给你一个牌子,上面写着你的行李放在哪个格子,离开时,你就能凭这个牌子和上面的数字成功取回行李。
cookie 做的正是这么一件事,旅客就像客户端,寄存处就像服务器,凭着写着数字的牌子,寄存处(服务器)就能分辨出不同旅客(客户端)。
你会不会想到,如果牌子被偷了怎么办,cookie 也会被偷吗?确实会,这就是一个很常被提到的网络安全问题——CSRF。可以在这篇文章了解关于 CSRF 的成因和应对方法。
cookie 诞生初似乎是用于电商存放用户购物车一类的数据,但现在前端拥有两个 storage(local、session),两种数据库(websql、IndexedDB),根本不愁信息存放问题,所以现在基本上 100% 都是在连接上证明客户端的身份。例如登录之后,服务器给你一个标志,就存在 cookie 里,之后再连接时,都会自动带上 cookie,服务器便分清谁是谁。另外,cookie 还可以用于跟踪一个用户,这就产生了隐私问题,于是也就有了“禁用 cookie”这个选项(然而现在这个时代禁用 cookie 是挺麻烦的事情)。
设置方式
现实世界的例子明白了,在计算机中怎么才能设置 cookie 呢?一般来说,安全起见,cookie 都是依靠 set-cookie 头设置,且不允许 JavaScript 设置。
Set-Cookie: <cookie-name>=<cookie-value>
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Strict
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Lax
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=None; Secure
// Multiple attributes are also possible, for example:
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly
其中 <cookie-name>=<cookie-value> 这样的 kv 对,内容随你定,另外还有 HttpOnly、SameSite 等配置,一条 Set-Cookie 只配置一项 cookie。
Expires 设置 cookie 的过期时间(时间戳),这个时间是客户端时间。 Max-Age 设置 cookie 的保留时长(秒数),同时存在 Expires 和 Max-Age 的话,Max-Age 优先 Domain 设置生效的域名,默认就是当前域名,不包含子域名 Path 设置生效路径,/ 全匹配 Secure 设置 cookie 只在 https 下发送,防止中间人攻击 HttpOnly 设置禁止 JavaScript 访问 cookie,防止XSS SameSite 设置跨域时不携带 cookie,防止CSRF
发送方式
Cookie: <cookie-list>
Cookie: name=value
Cookie: name=value; name2=value2; name3=value3
Cookie: PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1
Session
客户端储存
const express = require('express')
var cookieSession = require('cookie-session')
const app = express()
app.use(
cookieSession({
name: 'session',
keys: [
/* secret keys */
'key',
],
// Cookie Options
maxAge: 24 * 60 * 60 * 1000, // 24 hours
})
)
app.get('/', function(req, res) {
req.session.test = 'hey'
res.json({
wow: 'crazy',
})
})
app.listen(3001)
Set-Cookie: session=eyJ0ZXN0IjoiaGV5In0=; path=/; expires=Tue, 23 Feb 2021 01:07:05 GMT; httponly
Set-Cookie: session.sig=QBoXofGvnXbVoA8dDmfD-GMMM6E; path=/; expires=Tue, 23 Feb 2021 01:07:05 GMT; httponly
服务器储存
store.generate 否决这个,容易看出这个是初始化使用的 Store.prototype.createSession 这个是根据 req 和 sess 参数在 req 中设置 session 属性,没错,就是你了
var express = require('express')
var parseurl = require('parseurl')
var session = require('express-session')
var app = express()
app.use(
session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
})
)
app.use(function(req, res, next) {
if (!req.session.views) {
req.session.views = {}
}
// get the url pathname
var pathname = parseurl(req).pathname
// count the views
req.session.views[pathname] = (req.session.views[pathname] || 0) + 1
next()
})
app.get('/foo', function(req, res, next) {
res.json({
session: req.session,
})
})
app.get('/bar', function(req, res, next) {
res.send('you viewed this page ' + req.session.views['/bar'] + ' times')
})
app.listen(3001)
两种储存方式的对比
Token
JWT
结构
Header
{
"alg": "HS256",
"typ": "JWT"
}
Payload
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
iss (issuer):签发人 exp (expiration time):过期时间 sub (subject):主题 aud (audience):受众 nbf (Not Before):生效时间 iat (Issued At):签发时间 jti (JWT ID):编号
Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
如何使用
HTTP authentication
OAuth 2.0
OAuth 2.0(RFC 6749)也是用 token 授权的一种协议,它的特点是你可以在有限范围内使用别家接口,也可以借此使用别家的登录系统登录自家应用,也就是第三方应用登录。(注意啦注意啦,OAuth 2.0 授权流程说不定面试会考哦!)
+--------+ +---------------+
| |--(A)- Authorization Request ->| Resource |
| | | Owner |
| |<-(B)-- Authorization Grant ---| |
| | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(D)----- Access Token -------| |
| | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource |
| | | Server |
| |<-(F)--- Protected Resource ---| |
+--------+ +---------------+
准备
获取 code
什么是授权临时票据(code)?答:第三方通过 code 进行获取 access_token 的时候需要用到,code 的超时时间为 10 分钟,一个 code 只能成功换取一次 access_token 即失效。code 的临时性和一次保障了微信授权登录的安全性。第三方可通过使用 https 和 state 参数,进一步加强自身授权登录的安全性。
https://open.weixin.qq.com/connect/qrconnect?
appid=APPID&
redirect_uri=REDIRECT_URI&
response_type=code&
scope=SCOPE&
state=STATE
#wechat_redirect
redirect_uri?code=CODE&state=STATE
redirect_uri?state=STATE
获取 token
https://api.weixin.qq.com/sns/oauth2/access_token?
appid=APPID&
secret=SECRET&
code=CODE&
grant_type=authorization_code
{
"access_token": "ACCESS_TOKEN",
"expires_in": 7200,
"refresh_token": "REFRESH_TOKEN",
"openid": "OPENID",
"scope": "SCOPE",
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
使用 token 调用微信接口
curl -H "Authorization: token OAUTH-TOKEN" https://api.github.com
{
"openid": "OPENID",
"nickname": "NICKNAME",
"sex": 1,
"province":"PROVINCE",
"city":"CITY",
"country":"COUNTRY",
"headimgurl":"https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
"privilege":[ "PRIVILEGE1" "PRIVILEGE2" ],
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
后续使用
// 登录
req.session.id = openid
if (req.session.id) {
// 已登录
} else {
// 未登录
}
// 退出
req.session.id = null
// 清除 session
为你的应用申请 ID 和 Secret 准备好重定向接口 正确传参获取 code <- 重要 code 传入你的重定向接口 在重定向接口中使用 code 获取 token <- 重要 传入 token 使用微信接口
其他方法
使用服务器储存 session 为基础,可以用类似 req.session.isLogin = true 的方法标志该 session 的状态为已登录。 使用客户端储存 session 为基础,设置 session 的过期日期和登录人就基本能用了。
{
"exp": 1614088104313,
"usr": "admin"
}
甚至你可以使用上面的知识自己写一个 express 的登录系统:
初始化一个 store,内存、redis、数据库都可以 在用户身份验证成功后,随机生成一串哈希码作为 token 用 set-cookie 写到客户端 再在服务器写入登录状态,以内存为例就是在 store 中添加哈希码作为属性 下次请求带着 cookie 的话检查 cookie 带来的 token 是否已经写入 store 中即可
let store = {}
// 登录成功后
store[HASH] = true
cookie.set('token', HASH)
// 需要鉴权的请求钟
const hash = cookie.get('token')
if (store[hash]) {
// 已登录
} else {
// 未登录
}
// 退出
const hash = cookie.get('token')
delete store[hash]
总结
cookie 是储存 session/session id/token 的容器 cookie 设置一般通过 set-cookie 请求头设置 session 信息可以存放在浏览器,也可以存放在服务器 session 存放在服务器时,以 session id 为钥匙获取信息 token/session/session id 三者的界限是模糊的 一般新技术使用 token,传统技术使用 session id cookie/token/session/session id 都是用于鉴权的实用技术 JWT 是浏览器储存 session 的一种 JWT 常用于单点登录(SSO) OAuth2.0 的 token 不是由应用端颁发,存在另外的授权服务器 OAuth2.0 常用于第三方应用登录