python-flask 技能点使用-02 请求钩子实现登录验证
- 场景分析
使用python flask开发web系统,该系统是基于用户认证鉴权的web系统,因此需要考虑验证用户登录是否合法的 问题,我们一般借助于Redis来实现分布式缓存技术来实现单点登录的场景,前端请求一般将token信息携带在请求Headers中,后端系统需要根据要验证的业务Url来从请求头中获取Token然后来验证,此方案使用简单的用户id+随机数+加密生成Token的方式来实现
- 业务代码
- 用户登录成功--存储缓存
第〇步 定义用户Token类+辅助加密类Base64Handler+Redis缓存工具对象RedisUtil class UserToken(object): def __init__(self, user_id, token, create_time, expire_time): self.user_id = user_id self.token = token self.create_time = create_time self.expire_time = expire_time @staticmethod def get_user_id_from_token(token): token_user_id = Base64Handler.base64_decode(token) data = token_user_id.split(" ") user_id = data[1] return user_id class Base64Handler(object): # 根据user_id生成原始字符串的token @staticmethod def gen_user_token_orign(user_id): user_id = str(user_id) init_token = str(uuid.uuid4()) final_token = init_token + " " + user_id return final_token # 根据user_id生成base64加密后的字符串的token @staticmethod def gen_user_token_encode(user_id): return Base64Handler.base64_encode(Base64Handler.gen_user_token_orign(user_id)) # 根据user_id生成带base64加密后的字符串的token,创建时间,过期时间等信息的对象UserToken的json字符串 @staticmethod def gen_user_token_info(user_id, times): token = Base64Handler.gen_user_token_encode(user_id) # time.mktime(time.localtime()) 获取的是秒级别 create_time = int(time.mktime(time.localtime())) expire_time = create_time + times return UserToken(user_id, token, create_time, expire_time) # base64 加密处理 @staticmethod def base64_encode(content): return base64.b64encode(content.encode(encoding='utf-8')) # base64 解密处理 @staticmethod def base64_decode(content): # 解密 b'hello world' decode = base64.b64decode(content) # hello world decode = decode.decode(encoding='utf-8') return decode # RedisClient客户端 redis_client = redis.Redis(host=app.config.get('REDIS_HOST'), port=app.config.get('REDIS_PORT'), password=app.config.get('REDIS_PASSWORD'), db=app.config.get('REDIS_DB')) # Redis缓存工具对象 class RedisUtil(object): # 验证Token是否生效 @staticmethod def validate_token(token): # 前端当token为null的时候默认传值 if 'token' == token: return False user_id = UserToken.get_user_id_from_token(token) # 1. 判断在redis中是否有缓存 redis_token_key = "token_" + user_id if redis_client.exists(redis_token_key) == 0: return False cache_data = redis_client.get(redis_token_key) if cache_data is None or CommonTool.is_none(cache_data): return False cache_data = cache_data.decode() user_token = json.loads(cache_data) cache_data_token = user_token['token'] if cache_data_token != token: print("------- cache_data_token != req_token: ") return False else: # 判断是否过期 cache_data_expire_time = int(user_token['expire_time']) current_time = int(time.mktime(time.localtime())) if current_time >= cache_data_expire_time: redis_client.delete(redis_token_key) print("------- current_time >= cache_data_expire_time ") return False return True # 用户登录token保存到redis @staticmethod def cached_user_token(user_id, user_token): redis_token_key = "token_" + str(user_id) redis_client.set(redis_token_key, user_token) 第一步 生成带过期时间和用户id的Token user_id = user.id expire_time = int(app.config.get('TOKEN_EXPIRED_TIME_M')) user_token = Base64Handler.gen_user_token_info(user_id, expire_time)
第二步 将token缓存到Redis RedisUtil.cached_user_token(user_id, json.dumps(user_token, cls=CustomerEncoder, indent=4))
第三步 将token返回给前端 res_data = {} user_token_dict = {} if isinstance(user_token, UserToken): user_token_dict['user_id'] = user_token.user_id # todo 返回值中不要带bytes类型的否则报错,此处涉及多次使用decode将bytes转为string user_token_dict['token'] = user_token.token.decode(encoding='utf-8') user_token_dict['create_time'] = user_token.create_time user_token_dict['expire_time'] = user_token.expire_time res_data['user_token'] = user_token_dict res_data['sys_user'] = JsonHandler.object_to_dict(user) res_data['sys_user']['password'] = '********' return JsonResponse.success(200, msg="login success", data=res_data) 辅助工具类 class JsonHandler(object): # 将对象转换为dict 只能转换表数据 @staticmethod def object_to_dict(row): d = {} for column in row.__table__.columns: d[column.name] = str(getattr(row, column.name)) return d # 将对象数组转换为List<dict> @staticmethod def objects_to_arrays(rows): arr = [] for row in rows: arr.append(JsonHandler.object_to_dict(row)) return arr # 将数据库查询行数据转为字典 @staticmethod def row_to_dict(row): return dict(row) @staticmethod def rows_to_arrays(rows): arr = [] for row in rows: arr.append(JsonHandler.row_to_dict(row)) return arr class JsonResponse(object): def __init__(self, code, msg, data, **kwargs): self.code = code self.msg = msg self.data = data self.pagination = kwargs # 指定一个类的方法为类方法,通常用self来传递当前类的实例--对象,cls传递当前类。 @classmethod def success(cls, code=200, msg="success", data=None): return cls(code, msg, data) @classmethod def success_page(cls, code=200, msg="success", data=None, **kwargs): return cls(code, msg, data, **kwargs) @classmethod def fail(cls, code=400, msg="fail", data=None): return cls(code, msg, data) def to_dict(self): return { "code": self.code, "msg": self.msg, "data": self.data, "pagination": self.pagination }
-
- 业务请求--验证登录信息
我们借助于请求钩子中的before_request 在请求前进行拦截,考虑到有些业务并不需要登录验证,例如用户登录、用户登出、用户注册等类似场景,如下所示代码
#不需要验证的接口地址
no_use_auth_urls = ["/admin/user/logout", "/admin/user/login", "/admin/user/register", "/admin/user/code", "/admin/menu/import", "/admin/menu/tree", "/admin/menu/tree/i18n", "/favicon.ico"] # 在每一次请求之前调用,这时候已经有了请求,可以在这个方法里面做请求的校验 # 如果请求的校验不成功,可以直接在此方法中进行响应,直接return之后那么就不会执行视图函数了 目前必须在这个文件中才会执行 @app.before_request def before_request(): url = request.url base_url = request.base_url root_url = request.root_url host_url = request.host_url full_path = request.full_path f_path = full_path.split("?") bus_url = str(f_path[0]) is_static = bus_url.startswith("/static") if bus_url not in no_use_auth_urls and is_static == False: print('before_request bus_url : ', bus_url) token = request.headers.get('Authorization') user_id = UserToken.get_user_id_from_token(token) if token is None: return JsonResponse.fail(9999, 'request headers missing Authorization') if not RedisUtil.validate_token(token): return JsonResponse.fail(9401, 'request headers missing Authorization or Authorization is expired,need to login') return None