# 用户认证 cookie、session、token、jwt、单点登录 https://zhuanlan.zhihu.com/p/281414244 ## cookie * 狭义 cookie 过时技术 * 单个保存的数据不能超过 4 k, 单个域名数量有限 * 影响传输效率 * 明文存储在客户端, 安全性相对较低 * ==cookie: 只是浏览器存储的一小块数据, 也能用来存 token session, 每次浏览器发送请求就会带上 cookie 的数据== * 发送 cookie: `res.cookie('username','admin')` * 解析 cookie: cookie-parser // 先 npm 下载再使用 `app.use(cookieParser())` * 读取 cookie: req. cookie. username * cookie 是有有效期的, 一般是默认一次会话 (到浏览器关闭前) * 设置有效期 `res.cookie('username','admin', {expires:new Date(2088,11,1)})` 或者 `res.cookie('username','admin', {maxAge:毫秒})` * 部分删除: `res.clearCookie('username')` 相当于携带一个已过期的时间 * 全部删除: 发送到客户端后, 不能删除, 但可以发一个立即过期的, 来进行替换 ## session ![](https://img.081024.xyz/Pasted%20image%2020230326031222.png) * ==信息存在服务器, id 存一份在浏览器==, 只把信息对应的唯一 id 通过 cookie 发给用户, 用户请求数据在 cookie 里携带这个 id * express-session `app.use(session({secret: "加密的盐"}))` * 在服务器存储用户信息, 只把唯一 id 通过 cookie 发给用户 * 存储: `req.session.username = xxx` * 默认有效期: 一次会话 * 设置有效期: `req.session.cookie.maxAge = 毫秒` * 销毁: `req.session.destroy(callback)` * 存储在内存里, 重启就失效, 需要持久化 (Redis) ## token ==一种加密的文本字符串, 和 coookie session 不是同一维度的东西, 至于服务器存不存内存/redis ,浏览器存 cookie 或 storage, 通过 cookie 或者放请求头传给服务器, 和 token 无关== > session 的维护给服务端造成很大困扰,我们必须找地方存放它,又要考虑分布式的问题,甚至要单独为了它启用一套 Redis 集群 > 用户登录,服务端校验账号密码,获得用户信息, 把用户信息、token 配置编码成 token,发给浏览器 ![](https://img.081024.xyz/Pasted%20image%2020230326031429.png) ![|1208x324](https://img.081024.xyz/v2-ed5716bacaf7936a14553bb6723d2b4e_r.jpg) ## refresh token > 性能原因 * access token 用来访问业务接口,由于有效期足够短,盗用风险小,也可以使请求方式更宽松灵活 * refresh token 用来获取 access token,有效期可以长一些,通过独立服务和严格的请求方式增加安全性;由于不常验证,==也可以如前面的 session 一样处理, 存服务器, 并且能主动使其失效== ![](https://img.081024.xyz/Pasted%20image%2020230326032412.png) ## jwt > json web token, token 的一种实现 > 优点: 只在服务端校验, 不用存储 > 缺点: 服务端不能主动让 token 失效, 吊销只能用类似黑名单的方法 > 一定别在 token 中存放敏感信息, 能通过 base 64 解密 > 使用场景: 有效期短 > 其他方案: redis token / memory session jwt 格式: ![|1208x478](https://img.081024.xyz/v2-726d71fbe8fba4adb9c9f7b5e0854ce4_r.jpg) base 64 加密后的 header 和 base 64 加密后的 payload 使用.连接组成的字符串 (头部在前),然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分 密钥 secret 是保存在服务端 服务器应用在接受到 JWT 后对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个 Token 的内容被别人动过的,我们应该拒绝这个 Token ### 安装 ```sh npm install express-jwt ``` ### 加入中间件 ```js const expressJwt = require('express-jwt') // let token = jwt.sign(用户数据,加密字符串,配置对象) app.use(expressJwt({ secret: 'secret12345' // 签名的密钥 或 PublicKey }).unless({ path: ['/login', '/signup'] // 指定路径不经过 Token 解析 })) ``` ### 生成 Token ```js const jwt = require('jsonwebtoken') app.post('/login', function (req, res) { // 注意默认情况 Token 必须以 Bearer+空格 开头 const token = 'Bearer ' + jwt.sign( { _id: user._id, admin: user.role === 'admin' }, 'secret12345', { expiresIn: 3600 * 24 * 7 } ) res.json({ status: 'ok', data: { token: token } }) }) ``` ### 获取解析内容 在路由回调里通过 req. user 来访问 ```js app.get('/protected', function (req, res) { if (!req.user.admin) return res.sendStatus(401) res.sendStatus(200) }) ``` req. user 实际就是 JWT 的 payload 部分 ```js { _id: '5dbbc7daaf7dfe003680ba39', admin: true, iat: 1572587484, exp: 1573192284 } ``` ### 解析失败 如果解析失败,会抛出 UnauthorizedError,可以通过后置中间件来捕获 ```js app.use(function (err, req, res, next) { if (err.name === 'UnauthorizedError') { res.status(401).send('invalid token') } }) ``` ### 修改结果字段 默认解析结果会赋值在 req. user,也可以通过 requestProperty 来修改 ```js app.use(expressJwt({ secret: 'secret12345', requestProperty: 'auth' })) ``` ## 单点登录 ![](https://img.081024.xyz/Pasted%20image%2020230326033011.png) ## OAuth 2.0 > 一种第三方认证的解决方案 https://www.jianshu.com/p/c93823396844 ![](https://img.081024.xyz/Pasted%20image%2020230326043412.png)