170 lines
5.4 KiB
Markdown
170 lines
5.4 KiB
Markdown
# 用户认证
|
||
|
||
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
|
||
|
||

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

|
||

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

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

|
||
|
||
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'
|
||
}))
|
||
```
|
||
|
||
## 单点登录
|
||
|
||

|
||
|
||
## OAuth 2.0
|
||
|
||
> 一种第三方认证的解决方案
|
||
|
||
https://www.jianshu.com/p/c93823396844
|
||
|
||

|