Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

【鉴权】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
      • 每隔1小时,通过 refresh_token 申请新的token时,需要在取出 user_uuid 后
        • redis使用hGet获取'refresh_token_expire_time'中对应 user_uuid 的期望过期时间
        • 对比现在的时间,一般来说都能放行,否则不让申请,直接踢出
        • 除非对某个user_uuid键的值进行人工修改
      • 补充每次新设备上线,都要检查这个过期时间,可以复用这个申请新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 = {};
          }
      })