前后端利用accessToken与refreshToken无感刷新

项目初衷

以jwt(由header,payload和signature组成)为例,用户登录成功,后端返回accessToken。前端保存,请求接口携带,一切都是水到渠成的
可是在acessToken失效时,你正好请求一次接口,接口就挂了,可能你就跳到登录页了,用户体验也不好。我们希望虽然过期了,但是token偷偷地刷新重新设置,但又不影响当前接口运行。如果你的方案是把accessToken的有效期设置地久一点,比如100年。你真的是个机智Boy。

方案

生成accessToken和refreshToken。这两个token都是用jwt生成的,payload中关于用户信息,是一致的,是同一账号登录,过期时间是不一样的,accessToken短一点,refreshToken长一点;签名也可以不一样。但是两者解码后用户信息是一样(登录时统一生成的)。 jwt是不会保存在内存和数据库的;推荐redis缓存,现在理解成键值的关系(其实hash会更好)。项目中关系,是accessToken过期了,但是本地缓存的refreshToken是有效时,请求刷新。

  • 用户初次进入应用,无accessToken,则跳到登录页登录;有accessToken的话,先进行校验是否过期,过期再校验refreshToken。如果refreshToken也过期,则清除缓存进入登录页,未过期就进入应用,同时根据refreshToken刷新生成的accessToken
  • 用户登录成功后返回accessToken与refreshToken。接口请求携带accessToken;接口通,本次请求结束;如果返回是accessToken过期的状态,根据refreshToken重新获取accesToken(重新走jwt.sign),redis也要更新refreshToken对应的accessToken值。当然两个都过期,就清除本地缓存,去登录页面。

在这里插入图片描述

前端

小demo来演示,accessToken过期后,如何无感刷新,重新请求接口。

演示

import axios from "axios";

let token = "default"; // default在这里表示失效的token值

function request(url, data = {}, callBack = null) {
  return new Promise(async (resolve, reject) => {
    axios(url, data)
      .then((res) => {
        if (callBack) {
          console.log("进入 callBack");
          callBack(res.data);
          return;
        }
        if (token === "accessToken") { // 此时accessToken为更新后的值。实际业务按code值判断
          resolve(res.data);
        } else if (token === "default") { // token状态码失效判断,重新请求刷新token。 实际业务按code值判断
          console.log("无token / token 校验失败");
          setToken().then(() => {
            request(url, data, resolve); // 以resolve对象赋值给callBack
          });
        }
      })
      .catch((err) => {
        reject(err);
      });
  });
}

function setToken() { // 此处是走刷新token的接口,当然如果接口说refreshToken也过期,则重置;重新登录
  return new Promise((resolve, reject) => {
    if (token === "default") { // 
      console.log("进入 token");
      token = "accessToken";
      resolve();
    } else {
      reject();
    }
  });
}

request("http://www.xxxx:8005/api/v1/category").then((res) => {
  console.log(res, 8888888);
});

运行结果如下
在这里插入图片描述
当然,你也可以模拟如果接口token未过期

let token = "accessToken";

在这里插入图片描述

后端

核心代码块

  • 登录成功返回accessToken, refreshToken;并将关联关系redis缓存起来
const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host, {auth_pass: REDIS_CONF.password});


// 管理登录
router.post("/login", async (ctx) => {
  const { email, password } = ctx.request.body
  // 验证账号密码是否正确
  const user = await UserModel.findOne({
      where: {
        email
      }
  })
  if(!user){
  	throw new Error("账号不存在")
  }
  // 解密
  const correct = bcrypt.compareSync(password, user.password);
  if(!correct ){
  	throw new Error("密码错误")
  }
  // 生成accessToken和refreshToken
  const access_token = generateToken(user.email)
  const refresh_token = generateReToken(user.email) 
  addRefreshTokenToList(
    refresh_token,
    email,
    access_token,
    global.config.security.re_expiresIn
  );   
  ctx.response.status = 200;
  ctx.body = {
    code: 200,
    msg: "登录成功",
    access_token,
    refresh_token,
    expiresIn:global.config.security.expiresIn,
    token_type: "Basic",
  };
})


// 颁布令牌
const generateToken = function (uid) {
  const secretKey = global.config.security.secretKey; // 全局变量控制
  const expiresIn = global.config.security.expiresIn;
  const token = jwt.sign({
    uid
  }, secretKey, {
    expiresIn: expiresIn
  })
  return token
}


// 颁布刷新令牌
generateReToken (uid, scope) {
  const secretKey = global.config.security.re_secretKey;
  const expiresIn = global.config.security.re_expiresIn;
  const re_token = jwt.sign({
    uid
  }, secretKey, {
    expiresIn: expiresIn
  })
  return re_token
}


// 重新刷新
router.post("/refresh", async (ctx) => {
  const body = ctx.request.body;
  const refreshToken = body.refresh_token;

  if (refreshToken) {
    const result = await redisClient.exists(refreshToken); // 是否refreshToken过期
    if (result) {
      let refreshTokenPayload;
      var decode = jwt.verify( // 根据refreshToken解码获取用户信息
        refreshToken,
        global.config.security.re_secretKey
      );
      const email = decode.uid;
      const accessToken = generateToken(email); // 重新生成accessToken
      const response = {
        access_token: accessToken,
        expires_in: global.config.security.expiresIn,
        token_type: "Basic",
      };
      updateRefreshTokenfromList(refreshToken, accessToken); // 更新refreshToken与accessToken的绑定关系
      ctx.response.status = 200;
      ctx.body = response;
    } else {
      ctx.response.status = 401;
      ctx.body = {
        error: "Unauthorized",
        msg: "The refresh token does not exist",
      };
    }
  } else {
    ctx.response.status = 400;
    ctx.body = {
      error: "Bad Request",
      msg: "The required parameters were not sent in the request",
    };
  }
});

// 可能会用到溢出禁用
router.post("/revoke", async (ctx) => {
  const body = ctx.request.body;
  const refreshToken = body.refresh_token;
  if (refreshToken) {
    const result = await hasRefreshToken(refreshToken);
    if (result) {
      removeRefreshTokenfromList(refreshToken);
      ctx.response.status = 204;
      ctx.body = {
        msg: "refresh token revoke",
      };
    } else {
      ctx.response.status = 401;
      ctx.body = {
        error: "Unauthorized",
        msg: "The refresh token does not exist",
      };
    }
  } else {
    ctx.response.status = 401;
    ctx.body = {
      error: "Unauthorized",
      msg: "The refresh token does not exist",
    };
  }
});






function addRefreshTokenToList(refreshToken, email, accessToken, exp) {
  redisClient.hmset(refreshToken, {
    email,
    accessToken,
  });
  redisClient.expire(refreshToken, exp);
}

function updateRefreshTokenfromList(refreshToken, accessToken) {
  redisClient.hset(refreshToken, "accessToken", accessToken);
}

function removeRefreshTokenfromList(refreshToken) {
  redisClient.del(refreshToken, redis.print);
}

function hasRefreshToken(refreshToken) {
  return new Promise((resolve, reject) => {
    redisClient.exists(refreshToken, function (err, data) {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
}

收工

  • 13
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值