【鉴权】jsonwebtoken
-
自写中间件如下
app.use(async (ctx, next) => { if(token.verify(ctx)){ await next(); } }) -
安装鉴权模块
npm i -S jsonwebtoken- 如果使用了token做请求鉴权,就没必要使用koa-session等会话管理模块
- 如果使用了手机验证码登录,就没必要使用koa-passport等登录注册模块
-
鉴权模块编写(位于
lib/utils/token.js)import { Log } from "./api_log.js"; const log = new Log("api_token"); import jwt from "jsonwebtoken"; import { responseError } from "./common.js"; import config from "config"; import { redisClient } from "../db/redis_core.js"; const OLD_JWT_TOKEN_SECRET = config.get("old_jtw_token_secret"); const NEW_JWT_TOKEN_SECRET = config.get("new_jtw_token_secret"); export const signToken = (userUuid, deviceUuid) => { let obj = { user_uuid: userUuid, device_uuid: deviceUuid }; let options = { expiresIn: '1h', }; return jwt.sign(obj, NEW_JWT_TOKEN_SECRET, options); } export const verifyToken = (ctx) => { const token = ctx.request.headers['x-access-token'] || ctx.request.body.token || ctx.request.query.token; if (!token) { return false; } try { let decoded = jwt.verify(token, NEW_JWT_TOKEN_SECRET); if (decoded) { ctx.api_user = decoded; return true; } else { decoded = jwt.verify(token, OLD_JWT_TOKEN_SECRET); if (decoded) { ctx.api_user = decoded; return true; } else { responseError(ctx, 401, "TOKEN_INVALID", "Token parse exception"); } } } catch(err) { if (err.name === 'TokenExpiredError') { responseError(ctx, 401, "TOKEN_EXPIRED", err.message); log.info("Token expired"); } else { responseError(ctx, 401, "TOKEN_INVALID", err.message); log.error("Other token exception - ", err); } } return false; } export const signRefreshToken = (userUuid, deviceUuid) => { let obj = { user_uuid: userUuid, device_uuid: deviceUuid }; let options = { expiresIn: '30d', }; const time = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60; redisClient.hSet('refresh_token_expire_time', userUuid, time); return jwt.sign(obj, NEW_JWT_TOKEN_SECRET, options); } export const verifyRefreshToken = (ctx) => { const token = ctx.request.headers['x-access-refresh-token'] || ctx.request.body.refresh_token || ctx.request.query.refresh_token; if (!token) { log.warn("Not found refresh token in request - ", ctx.url); responseError(ctx, 401, "REFRESH_TOKEN_REQUIRED", "Token is needed for the request."); return false; } try { let decoded = jwt.verify(token, NEW_JWT_TOKEN_SECRET); if (decoded) { ctx.api_user = decoded; return true; } else { decoded = jwt.verify(token, OLD_JWT_TOKEN_SECRET); if (decoded) { ctx.api_user = decoded; return true; } else { responseError(ctx, 401, "REFRESH_TOKEN_INVALID", "Token parse exception"); } } } catch(err) { if (err.name === 'TokenExpiredError') { responseError(ctx, 401, "REFRESH_TOKEN_EXPIRED", err.message); } else { responseError(ctx, 401, "REFRESH_TOKEN_INVALID", err.message); } } return false; } -
单纯的jwt无法实现token过期和踢人下线
- 需要采取方案
- 分为 refresh_token (30天过期)和 token (1小时过期)
- 登录,签发新的 refresh_token 时
- 里面默认有
iat记录了10位秒级时间戳,但这里不使用 - redis使用hSet存储
user_uuid:期望过期时间 对到'refresh_token_expire_time'中- 期望过期时间为:(这里是30天后)
Math.floor(Date.now() / 1000) + + 30 * 24 * 60 * 60
- 期望过期时间为:(这里是30天后)
- 里面默认有
- 每隔1小时,通过 refresh_token 申请新的token时,需要在取出 user_uuid 后
- redis使用hGet获取
'refresh_token_expire_time'中对应 user_uuid 的期望过期时间 - 对比现在的时间,一般来说都能放行,否则不让申请,直接踢出
- 除非对某个user_uuid键的值进行人工修改
- redis使用hGet获取
- 补充每次新设备上线,都要检查这个过期时间,可以复用这个申请新token的过程,即每次打开app都要让原token作废,并重新申请token
- 好处
- 多设备登录时,记录的是最新的期望过期时间
- 一旦把这个user_uuid下面过期时间的人工改掉,所有设备都申请不了新的token,如果用户重新打开,则立刻登出,如果用户一直在用,则最多1小时后踢掉
- 需要采取方案
-
如何减轻全部用户,都要查询redis的负担?
- 这里只有使用到refresh_token时,才会查询redis,双层架构本身就负担小
-
代码
- 签发 refresh token
export const signRefreshToken = (userUuid, deviceUuid) => { let obj = { user_uuid: userUuid, device_uuid: deviceUuid }; let options = { expiresIn: '30d', }; const time = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60; redisClient.hSet('refresh_token_expire_time', userUuid, time); return jwt.sign(obj, NEW_JWT_TOKEN_SECRET, options);
} ```
- 申请新token的路由
router.post('/login/refresh_token', async(ctx) => { log.info('login refresh_token') if(verifyRefreshToken(ctx)){ let expire_time = await redisClient.hGet('refresh_token_expire_time', ctx.api_user.user_uuid); let now_time = Math.floor(Date.now() / 1000); if(!expire_time || now_time < expire_time){ let token = signToken(ctx.api_user.user_uuid, ctx.api_user.device_uuid); ctx.status = 200; ctx.body = { token: token }; }else{ responseError(ctx, 400, "REFRESH_TOKEN_EXPIRED", "refresh token expired manully"); } } }) - 手动登出的路由
router.post('/login/logout',async(ctx) => { log.info('login logout') if(verifyRefreshToken(ctx)){ let time = Math.floor(Date.now() / 1000); redisClient.hSet('refresh_token_expire_time', ctx.api_user.user_uuid, time); ctx.status = 200; ctx.body = {}; } })
- 签发 refresh token