今天继续 Vue + Flask 小知识系列,登陆 session 的相关管理
用户登陆系统之后,如果一段时间没有任何操作,session 需要有一个超时过期的动作,用户需要再次登陆才可以使用系统。
before_app_request 是 flask 提供的请求钩子,可以装饰一个函数,使其在每次请求之前执行。
@auth.before_app_request
def before_request():
if request.headers.get('Authorization', None) is not None:
token = request.headers['Authorization'].split(' ')[1]
try:
data = s.loads(token)
userId = data['userid']
if rd.hget(data['userid'], "token") == token:
operate_time = rd.hget(userId, "operate_time")
if time.time() - float(operate_time) > session_expired_time:
rd.hdel(userId, "token", "operate_time")
return jsonify({"code": 401, "msg": "login expired"}), 401
else:
rd.hset(userId, "operate_time", time.time())
else:
return jsonify({"code": 403, "msg": "token abnormal"}), 403
except:
pass
else:
pass
把用户 ID、token 和最近的操作时间保存到 redis 中,如果当前时间减去 redis 中保存的最近操作时间大于设置的超时时间,则返回 401 错误码。
改写登陆函数,设置 redis 哈希值
class LoginView(Resource):
def post(self):
try:
username = request.get_json()['username']
pwd = request.get_json()['password']
user = User.query.filter_by(username=username).first()
if user is not None and user.verify_password(pwd):
data = token.genTokenSeq(username)
h_dict = {"token": data['access_token'], "operate_time": time.time()}
rd.hmset(user.id, h_dict)
return {'code': 200, 'message': 'you are login now!', 'data': data}
else:
return {'code': 403, 'message': 'wrong account or password'}
except:
raise
用户登陆成功后,把用户 ID、token 和当前时间保存到 redis 中。
此时可以规定,401 错误码即为登陆超时错误码,使用 interceptors 拦截最为方便
Axios.interceptors.response.use(
response => {
return response;
},
error => {
if (error.response){
switch (error.response.status){
case 401:
sessionStorage.removeItem(Config.tokenKey);
localStorage.removeItem('accessToken');
router.replace({
path: '/login',
query: {redirect: router.currentRoute.fullPath}
});
// vueObj 是在 main.js 中定义的把 vue实例赋予window的全局变量,在全局都可以使用,相当于 this。
vueObj.$message.error("登陆session过期,请重新登陆");
break;
case 402:
console.log("do something");
break;
case 403:
sessionStorage.removeItem(Config.tokenKey);
localStorage.removeItem('accessToken');
router.replace({
path: '/login',
query: {redirect: router.currentRoute.fullPath}
});
vueObj.$message.error("登陆token异常,请重新登陆");
break
}
}
return Promise.reject(error.response.data);
}
)
如果返回错误码为 401,则清空 sessionStorage 和 localStorage 相关信息,并重定向到 login 路由地址。
在调用 logout 视图时,把对应的 token 保存到 redis 中,作为黑名单处理,黑名单内的 token 不再允许访问系统。
class LogoutView(Resource):
@token.tokenRequired
def get(self):
try:
token = request.headers['Authorization'].split(' ')[1]
rd.set("token" + str(time.time()), token, ex=int(token_expired_time) + 200, nx=True)
except:
raise
退出登陆的 token 保存以 token 为开头的 string 类型数据中
为 tokenRequired 装饰器增加判断
def tokenRequired(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'Authorization' in request.headers:
split_token = request.headers['Authorization'].split(' ')
if len(split_token) == 2 and split_token[0] == 'jwt':
token = request.headers['Authorization'].split(' ')[1]
else:
return {'code': 401, 'message': 'authorize failed'}, 401
else:
return {'code': 401, 'message': 'authorize failed'}, 401
token_list = []
if rd.keys("token*"):
for t in rd.keys("token*"):
token_list.append(rd.get(t))
if token in token_list:
return {'code': 401, 'message': 'token is blocked'}, 401
validator = validateToken(token)
if validator['code'] != 200:
if validator['message'] == 'toekn expired':
return validator, 402
else:
return validator, 401
elif validator['code'] == 200:
return f(*args, **kwargs)
return f(*args, **kwargs)
return decorated_function
增加了读取 redis 和判断 token 的逻辑。
前端修改 logout 函数
logout() {
logout.logout()
.then(res => {
console.log("成功退出登陆");
})
.catch(function(error){
console.log("网络错误");
})
sessionStorage.removeItem(this.$Config.tokenKey);
localStorage.removeItem('accessToken');
this.$router.push({path: '/login'});
}
至此,token 黑名单机制添加完毕。
首先定义一个 renew token 的 API 函数
class RenewTokenView(Resource):
@token.tokenRequired
def post(self):
old_token = request.headers['Authorization'].split(' ')[1]
userid = load_token.load_token(old_token)['userid']
refresh_token = request.get_json()['refresh_token']
validator = token.validateToken(refresh_token)
if validator['code'] != 200:
if validator['message'] == 'toekn expired':
return validator, 402
else:
return validator, 401
elif validator['code'] == 200:
new_token = token.genTokenSeq(userid=userid, onlyaccesstoken=True)
h_dict = {"token": new_token['access_token'], "operate_time": time.time()}
rd.hmset(userid, h_dict)
return {'code': 200, 'message': 'renew token successful!', 'data': new_token}
接下来在前端判断 token 是否快要过期,如果快要过期,则调用刷新 token 的接口,刷新 token。
var nowTime = new Date();
var tokenExpiredDate = new Date(localStorage.getItem("tokenExpiredDate"));
var checkTime = (tokenExpiredDate.getTime() - nowTime.getTime())/1000;
if(checkTime > 120) {
next();
}
else{
console.log("need refresh token");
var r_token = {
"refresh_token": localStorage.getItem("refreshToken")
};
refreshtoken.refreshtoken(r_token)
.then(res => {
console.log("send request");
if(res.data.code === 200){
localStorage.setItem("accessToken", res.data.data['access_token']);
localStorage.setItem("accessTokenExpiryTime", res.data.data['access_token_expire_in']);
var signTokenTime = new Date();
var tokenExpiredDate = new Date(signTokenTime.setSeconds(signTokenTime.getSeconds() + res.data.data['access_token_expire_in']));
localStorage.setItem("tokenExpiredDate", tokenExpiredDate);
next();
}
else{
Message.error("刷新token失败,请重新登陆");
next({path: '/login'});
}
})
.catch(function(error){
console.log(error);
Message.error("刷新token失败,请重新登陆");
next({path: '/login'});
})
}
至此,access_token 快过期后,主动去刷新 token 的功能也做好了。