Vue + Flask 小知识(七)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 今天继续 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 的功能也做好了。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
7月前
|
JavaScript NoSQL 前端开发
Vue + Flask 小知识(七)
Vue + Flask 小知识(七)
|
7月前
|
JavaScript 前端开发 API
Vue + Flask 小知识(一)
Vue + Flask 小知识(一)
|
JSON JavaScript 数据格式
FLASK+VUE+axios前后端交互
FLASK+VUE+axios前后端交互
404 0
|
7月前
|
JavaScript 前端开发 API
Vue + Flask 实现单页面应用
Vue + Flask 实现单页面应用
|
7月前
|
JavaScript 前端开发 Python
Vue + Flask 小知识(六)
Vue + Flask 小知识(六)
|
7月前
|
缓存 JSON JavaScript
Vue + Flask 小知识(五)
Vue + Flask 小知识(五)
|
7月前
|
存储 JavaScript 前端开发
Vue + Flask 小知识(四)
Vue + Flask 小知识(四)
|
7月前
|
JavaScript API Python
Vue + Flask 小知识(三)
Vue + Flask 小知识(三)
|
7月前
|
JavaScript 数据库 Python
Vue + Flask 小知识(二)
Vue + Flask 小知识(二)
|
JavaScript 前端开发 Python
Vue前后端页面下载功能实现演示,Python+flask提供后台下载服务
Vue前后端页面下载功能实现演示,Python+flask提供后台下载服务
270 0