devStandard/docs/learning/4-nodejs/4.1-用户认证.md
2025-03-29 16:55:07 +08:00

5.4 KiB
Raw Permalink Blame History

用户认证

cookie、session、token、jwt、单点登录 https://zhuanlan.zhihu.com/p/281414244

  • 狭义 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

  • ==信息存在服务器, 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发给浏览器


|1208x324

refresh token

性能原因

  • access token 用来访问业务接口,由于有效期足够短,盗用风险小,也可以使请求方式更宽松灵活
  • refresh token 用来获取 access token有效期可以长一些通过独立服务和严格的请求方式增加安全性由于不常验证==也可以如前面的 session 一样处理, 存服务器, 并且能主动使其失效==

jwt

json web token, token 的一种实现 优点: 只在服务端校验, 不用存储 缺点: 服务端不能主动让 token 失效, 吊销只能用类似黑名单的方法 一定别在 token 中存放敏感信息, 能通过 base 64 解密 使用场景: 有效期短 其他方案: redis token / memory session

jwt 格式:

|1208x478

base 64 加密后的 header 和 base 64 加密后的 payload 使用.连接组成的字符串 (头部在前),然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分

密钥 secret 是保存在服务端

服务器应用在接受到 JWT 后对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个 Token 的内容被别人动过的,我们应该拒绝这个 Token

安装

npm install express-jwt

加入中间件

const expressJwt = require('express-jwt')
// let token = jwt.sign(用户数据,加密字符串,配置对象)
app.use(expressJwt({
  secret: 'secret12345'  // 签名的密钥 或 PublicKey
}).unless({
  path: ['/login', '/signup']  // 指定路径不经过 Token 解析
}))

生成 Token

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 来访问

app.get('/protected', function (req, res) {
  if (!req.user.admin)
    return res.sendStatus(401)
  res.sendStatus(200)
})

req. user 实际就是 JWT 的 payload 部分

{
  _id: '5dbbc7daaf7dfe003680ba39',
  admin: true,
  iat: 1572587484,
  exp: 1573192284
}

解析失败

如果解析失败,会抛出 UnauthorizedError可以通过后置中间件来捕获

app.use(function (err, req, res, next) {
  if (err.name === 'UnauthorizedError') {   
    res.status(401).send('invalid token')
  }
})

修改结果字段

默认解析结果会赋值在 req. user也可以通过 requestProperty 来修改

app.use(expressJwt({
  secret: 'secret12345',
  requestProperty: 'auth'
}))

单点登录

OAuth 2.0

一种第三方认证的解决方案

https://www.jianshu.com/p/c93823396844