【Python】Flask API 登录
2021/4/24 20:25:50
本文主要是介绍【Python】Flask API 登录,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
Flask API 登录
零、起因
最近要写uniapp客户端,服务器使用的是Python的Flask框架,为了实现用户登录,在网上查到了一些Flask的扩展,其中比较简单的就是flask_httpauth(此时版本__version__ = '4.2.1dev'),其官网给出的基本示例:
from flask import Flask from flask_httpauth import HTTPBasicAuth from werkzeug.security import generate_password_hash, check_password_hash app = Flask(__name__) auth = HTTPBasicAuth() users = { "john": generate_password_hash("hello"), "susan": generate_password_hash("bye") } @auth.verify_password def verify_password(username, password): if username in users and \ check_password_hash(users.get(username), password): return username @app.route('/') @auth.login_required def index(): return "Hello, {}!".format(auth.current_user()) if __name__ == '__main__': app.run()
浏览器访问127.0.0.1:5000会提示输入账号和密码才能访问页面,否则是错误提示。实现原理是基于http auth协议完成的,登录成功后不需要在浏览器里设置session,而是设置了一个请求头,拿上例第一个账号来说,在请求时添加请求头Authorization:Basic am9objpoZWxsbw==
就可以完成对用户的认证。因为机制简单,这非常适合uniapp这种客户端的程序编写,于是决定采用flask_httpauth完成对用户的认证。但是其中遇到了问题,例如其设置的请求头,用Base64算法解am9objpoZWxsbw==
,其解出来的值中有包含账号和密码,这样很容易造成密码泄露,因此我开始想办法把这里解出的密码换成用加盐散列算法generate_password_hash计算出的hash值。
壹、解决
通过对源码的阅读,我发现貌似没有相关函数可以支持把密码换成hash值,有另外一个类HTTPDigestAuth,但是需要存session,失去了简单的初衷。
首先API想访问受保护的函数就必须提供Authorization参数。分析发现默认的Authorization参数格式是Basic(空格)((用户名(冒号)密码)的base64编码)
于是写一个参数生成函数,API登录时首先访问这个函数验证账号密码后获取Authorization参数值。
@app.route('/api/login', methods=['POST']) def get_auth(): username = request.args.get('username') print(username) password = request.args.get('password') print(password) if username and password: if username in users and check_password_hash(users.get(username), password): print('登录成功') token = username + ':' + users.get(username) b64_token = base64.urlsafe_b64encode(token.encode("utf-8")) au = b64_token.decode("utf-8") return 'Basic {}'.format(au) else: return '账号或密码错误' else: return '参数不完整'
需要使用POST方法,在Query参数列表里传入username=hello&password=hello
,使用ApiPost接口测试软件发起请求后获得响应:
Basic am9objpwYmtkZjI6c2hhMjU2OjE1MDAwMCRNc2NEdDNJTSQyMmZlZGNmZTQwNDc3YzAyMzhjNGVkMmIxOTZiZjg5ODIyN2IyOGNlOTcxY2IzOTU2NjE3MWI1NTJhYTgzMzM3
使用Base64解出来是
john:pbkdf2:sha256:150000$MscDt3IM$22fedcfe40477c0238c4ed2b196bf898227b28ce971cb39566171b552aa83337
用户名和密码hash值。
接下来是密码验证部分,因为存储的用户数据里的密码就是hash值,因此直接判断相等否就行:
@auth.verify_password def verify_password(username, password): if username in users and users.get(username)==password: return username
再次使用ApiPost加上Header参数Authorization:Basic am9objpwYmtkZjI6c2hhMjU2OjE1MDAwMCRNc2NEdDNJTSQyMmZlZGNmZTQwNDc3YzAyMzhjNGVkMmIxOTZiZjg5ODIyN2IyOGNlOTcxY2IzOTU2NjE3MWI1NTJhYTgzMzM3
访问127.0.0.1:5000
成功返回Hello, john!
至此,uniapp登录的基本机制解决了,并且密码安全性也得到了提高。
但是发现浏览器不能正常登录了,在flask_httpauth源码部分貌似没找到生成Authorizationd的函数。项目是用的flask_login用在网页登录部分的,因此暂时不受影响,不过在后来的flask_httpauth源码阅读中貌似发现了它可以实现把明文密码替换成密码hash下发到浏览器。稍后再做分析。
贰、重写
就一直觉得秘钥生成和认证分在不同的地方总不对,而且没有对flask_httpauth有很深的了解,生成的秘钥格式有时候不一定对得上。因此决定仿造flask_httpauth自己写一个,就暂时叫flask_apiauth吧,因为主要是为API服务的。
源文件只有一个,一个类,仿造flask_httpauth的结构:
flask_httpauth.py
# coding:utf-8 # @Time : 2021/4/24 17:08 # @Author : minuy # @File : flask_apiauth.py from functools import wraps from flask import request, g import base64 class ApiAuth(object): def __init__(self, split_character=' '): # 分割词,最好唯一且不出现在账号里 self.split_character = split_character self.verify_password_callback = None self.error_content_callback = None def verify_password(self, f): """ 验证密码回调,此回调返回的非空数据将放在current_user中 """ print('设置密码验证函数') self.verify_password_callback = f return f def error_content(self, f): """ 错误数据回调,此回调应返回登录、验证失败回复给客户端的内容 """ print('设置错误内容函数') self.error_content_callback = f return f def get_token(self, username=None, password=None): """ 根据账号和密码(hash)生成token,用于登录函数 """ print('生成token') token = username + self.split_character + password return base64.urlsafe_b64encode(token.encode("utf-8")) def authentication_failed(self): """ 认证失败调用 """ print('验证密码失败') # 如果有错误内容处理,返回错误内容 if self.error_content_callback: print('返回自定义错误数据') return self.error_content_callback() else: # 否则返回文字,登录失败 return 'Login failed' @property def current_user(self): """ 登录后通过这个属性获取在verify_password函数里返回的内容(用户信息) """ if hasattr(g, 'flask_api_auth_user'): return g.flask_api_auth_user def login_required(self, f=None): """ 登录拦截,没有相应的请求头或者验证密码返回空值会返回错误信息 """ def login_required_internal(f): @wraps(f) def decorated(*args, **kwargs): auth_user = None if 'token' in request.headers: print('token存在') try: # 把账号和密码hash都一起打包到base64里 token = base64.urlsafe_b64decode(request.headers['token']).decode('utf-8') print('token:', token) # 账号和密码hash之间使用空格分割 user, hash_password = token.split(self.split_character, 1) print('user:', user, 'hash_password', hash_password) # 如果账号和密码都存在 if user and hash_password: auth_user = {'user': user, 'hash_password': hash_password} except (ValueError, KeyError): # 如果解析失败或者没有token print('token解析失败') pass # 没提交参数 else: # 在这里可以特别设置未登录的提醒 return self.authentication_failed() print('auth', auth_user) # 如果存在用户信息,开始验证密码 if auth_user: user = None # 如果有密码验证函数 if self.verify_password_callback: print('开始验证密码') user = self.verify_password_callback(auth_user.get('user'), auth_user.get('hash_password')) if user: print('密码验证成功') # 如果user不为空,加载 g.flask_api_auth_user = user if user is not True \ else auth_user.get('user') if auth_user else None # 如果user为空 if user in (False, None): return self.authentication_failed() else: # 用户信息不存在 return self.authentication_failed() return f(*args, **kwargs) return decorated if f: return login_required_internal(f) return login_required_internal
(注释打印可以去掉一下)
完成用户token生成(登录)和用户登录拦截(认证),众所周知,退出登录即把uniapp存储的token删除。
然后是示例代码:
test.py
from flask import Flask, request from werkzeug.security import generate_password_hash, check_password_hash from flask_apiauth import ApiAuth app = Flask(__name__) auth = ApiAuth() users = { "john": generate_password_hash("hello"), "susan": generate_password_hash("bye") } @app.route('/api/login', methods=['POST']) def get_auth(): username = request.args.get('username') print(username) password = request.args.get('password') print(password) if username and password: if username in users and check_password_hash(users.get(username), password): return auth.get_token(username, users.get(username)) else: return { 'code': '403', 'data': {}, 'message': '账号或密码错误' } else: return { 'code': '403', 'data': {}, 'message': '参数不完整' } @auth.error_content def error_content(): return { 'code': '403', 'data': {}, 'message': '请先登录' } @auth.verify_password def verify_password(username, password): if username in users and users.get(username) == password: print('密码正确') # 返回的数据是下面auth.current_user拿到的 return {'username': username, 'sex': '男'} @app.route('/') @auth.login_required def index(): return { 'code': '200', 'data': { 'name': auth.current_user.get('username'), 'sex': auth.current_user.get('sex') }, 'message': '成功' } if __name__ == '__main__': app.run()
使用ApiPost测试,登录、拦截功能正常,目前就这么用着先吧。
开源地址:Flask-APIAuth
叁、效果
这篇关于【Python】Flask API 登录的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-05-08有遇到过吗?同样的规则 Excel 中 比Python 结果大
- 2024-03-30开始python成长之路
- 2024-03-29python optparse
- 2024-03-29python map 函数
- 2024-03-20invalid format specifier python
- 2024-03-18pool.map python
- 2024-03-18threads in python
- 2024-03-14python Ai 应用开发基础训练,字符串,字典,文件
- 2024-03-13id3 algorithm python
- 2024-03-13sum array elements python