• 【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

    叁、效果

  • 相关阅读:
    用JavaScript 实现变速回到顶部
    导出数据到Excel
    Jquery ajax调用webService,远程访问出错解决办法
    火狐和IE的window.event对象详解
    硬盘、U盘添加漂亮背景
    JS 获取当前日期时间(兼容IE FF)
    Base64编码
    师生关系
    关于计算机导论的问题
    自我介绍
  • 原文地址:https://www.cnblogs.com/minuy/p/14697821.html
Copyright © 2020-2023  润新知