文章目录
1. 生成私钥和公钥
# 打开 openssl
> openssl
# 生成私钥:
OpenSSL > genrsa -out rsa_private_key.pem 2048
# 根据私钥生成公钥:
OpenSSL > rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
2. 使用 jsonwebtoken 注册和验证 token
utils/token.js
const jwt = require('jsonwebtoken')
const fs = require('fs')
const path = require('path')
// 创建Token
const signToken = (user) => {
// 私钥
const privateKey = fs.readFileSync(path.join(__dirname, '../secretKeys/rsa_private_key.pem'))
const params = {
...user,
_id: user._id,
}
// 将登陆的用户信息生成token,使用私钥进行非对称加密,过期时间为 2h
return jwt.sign(params, privateKey, {
algorithm: 'RS256', expiresIn: '2h' })
}
// 校验Token
const verifyToken = (token) => {
// 公钥
const publicKey = fs.readFileSync(path.join(__dirname, '../secretKeys/rsa_public_key.pem'))
try {
// 根据公钥验证token
// 验证成功则返回注册token的原始信息
return jwt.verify(token, publicKey)
} catch (err) {
// 验证失败则返回err
// 处理非法token、过期token等问题
return null
}
}
module.exports = {
signToken,
verifyToken,
}
3. 登录,注册token并返回给客户端
control/account.js
const usersModel = require('../model/usersManage')
const {
getComparePwd } = require('../utils/getBcryptPwd')
const {
signToken } = require('../utils/token')
// 用户登录
const login = async (req, res, next) => {
res.set('Content-Type', 'application/json; charset=utf-8')
const {
username, password } = req.body
const user = await usersModel.findOneUser(username)
if (user) {
// 验证密码是否正确
const match = await getComparePwd(password, user.password)
if (match) {
// 生成token,并将token返回给前端
const token = signToken(user)
// res.set('X-Access-Token', token)
res.render('success', {
data: JSON.stringify({
token })
})
} else {
res.render('error', {
data: JSON.stringify("用户名或密码错误")
})
}
} else {
res.render('error', {
data: JSON.stringify("用户名或密码错误")
})
}
}
module.exports = {
login,
}
4. auth中间件验证 token
middlewares/auth.js
const {
verifyToken } = require('../utils/token')
const auth = (req, res, next) => {
const verifyResult = verifyToken(req.get('x-token'))
if (verifyResult) {
// 将从token中获取的用户信息通过res.locals传给下一个中间件
res.locals.currentUser = verifyResult;
next()
} else {
res.render('notLogin', {
data: JSON.stringify("请登录")
})
}
}
exports.auth = auth
routes/account.js
const express = require('express');
const router = express.Router();
const {
login, info } = require('../control/accounts')
const {
auth } = require('../middlewares/auth')
// 登录
router.post('/login', login);
// 用户信息
router.get('/info', auth, info);
module.exports = router;
5. 退出登录
JWT
的最大缺点是,由于服务器不保存 session
状态,因此无法在使用过程中废止某个 token
,或者更改 token
的权限。也就是说,一旦 JWT
签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。因此我们有以下几种方案来优化解决 JWT
无状态的问题。
方案:
5.1 黑名单校验
凡是退出登录的token
都放入黑名单中,定期清理。
每次用户请求服务器都校验token
是否在黑名单
5.2 版本号校验
访问时从token
中取出版本号和用户id 和 redis
中存储 用户id和版本号 做对比,不一致则不给访问。
用户登出的时候在redis
中把用户版本号加一。
5.3 过期时间校验
登录时token
附带创建时间。访问时校验redis
存储的过期时间,如果创建时间大于过期时间则不给访问。
5.4 Token副本校验
在redis
中存储token
副本,用户请求时候校验,如果redis
中不存在该副本则不给通过。
5.5 无为而治
只让前端清理token
,后端不理会。(大多数)
方案的选择
对于需要只有一人一终端登录的服务采用 【版本号校验】 解决方案
相当于乐观锁,但是是用redis来实现,速度快
还方便踢人下线
对于需要注销token的服务,采用 【Token副本校验 or 过期时间校验】解决方案
比查阅黑名单列表还要快得多
速度相当快
对于不需要注销token(大部分服务),则采用 【无为而治】解决方案
完美的JWT登录的使用方式