今天继续 Vue + Flask 小知识系列,登陆 session 的相关管理
登陆 session 超时
用户登陆系统之后,如果一段时间没有任何操作,session 需要有一个超时过期的动作,用户需要再次登陆才可以使用系统。
使用 before_app_request
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 中。
通过 axios interceptors 拦截
此时可以规定,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 路由地址。
登出后 token 处理
在调用 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 黑名单机制添加完毕。
刷新 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 的功能也做好了。