项目初衷
以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);
}
});
});
}
收工