2016-02-13 78 views
3

我想要有条件地应用装饰器(Flask-HTTPAuth的login_required)。如果sky_is_blue == True,我想应用装饰器,如果为False,则不会。因为它可能会在应用程序的生命周期中发生变化(实际上在实践中并不那么多,但绝对是为了单元测试的目的,而且我对任何情况下的原因都很好奇),所以这需要在调用时发生。有条件地应用Flask-HTTPAuth的login_required修饰器

所以我在装饰器中包装了装饰器。

行为与False情况(不应用装饰器)中的预期行为一样,但在True情况下应用装饰器时遇到问题。我不确定这是我做错了什么,或者与Flask-HTTPAuth发生了奇怪的交互。

以下脚本演示了两个单元测试的问题。 test_sky_not_blue通过,但test_sky_blue失败并显示堆栈跟踪。

from flask import Flask 
from flask.ext.httpauth import HTTPBasicAuth 
from functools import update_wrapper, wraps 
from flask.ext.testing import TestCase 
import unittest 


app = Flask(__name__) 
app.config['TESTING'] = True 

sky_is_blue = True 
auth = HTTPBasicAuth() 


class ConditionalAuth(object): 
    def __init__(self, decorator): 
     print("ini with {}".format(decorator.__name__)) 
     self.decorator = decorator 
     update_wrapper(self, decorator) 

    def __call__(self, func): 
     print("__call__: ".format(func.__name__)) 

     @wraps(func) 
     def wrapped(*args, **kwargs): 
      print("Wrapped call, function {}".format(func.__name__)) 
      if sky_is_blue: 
       rv = self.decorator(func(*args, **kwargs)) 
       return rv 
      else: 
       rv = func(*args, **kwargs) 
       return rv 
     return wrapped 


@app.route('/') 
@ConditionalAuth(auth.login_required) 
def index(): 
    """ 
    Get a token 
    """ 
    return "OK" 


class TestSky(TestCase): 
    def create_app(self): 
     return app 

    def test_sky_blue(self): 
     global sky_is_blue 
     sky_is_blue = True 
     response = self.client.get('/') 
     self.assert200(response) 

    def test_sky_not_blue(self): 
     global sky_is_blue 
     sky_is_blue = False 
     response = self.client.get('/') 
     self.assert200(response) 


def suite(): 
    return unittest.makeSuite(TestSky) 

if __name__ == '__main__': 
    unittest.main(defaultTest='suite') 

完整的堆栈跟踪我得到的是:

Traceback (most recent call last): 
    File "test.py", line 64, in test_sky_blue 
    response = self.client.get('/') 
    File "/usr/local/lib/python2.7/site-packages/werkzeug/test.py", line 778, in get 
    return self.open(*args, **kw) 
    File "/usr/local/lib/python2.7/site-packages/flask/testing.py", line 108, in open 
    follow_redirects=follow_redirects) 
    File "/usr/local/lib/python2.7/site-packages/werkzeug/test.py", line 751, in open 
    response = self.run_wsgi_app(environ, buffered=buffered) 
    File "/usr/local/lib/python2.7/site-packages/werkzeug/test.py", line 668, in run_wsgi_app 
    rv = run_wsgi_app(self.application, environ, buffered=buffered) 
    File "/usr/local/lib/python2.7/site-packages/werkzeug/test.py", line 871, in run_wsgi_app 
    app_rv = app(environ, start_response) 
    File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1836, in __call__ 
    return self.wsgi_app(environ, start_response) 
    File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1820, in wsgi_app 
    response = self.make_response(self.handle_exception(e)) 
    File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1403, in handle_exception 
    reraise(exc_type, exc_value, tb) 
    File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1817, in wsgi_app 
    response = self.full_dispatch_request() 
    File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1477, in full_dispatch_request 
    rv = self.handle_user_exception(e) 
    File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1381, in handle_user_exception 
    reraise(exc_type, exc_value, tb) 
    File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1475, in full_dispatch_request 
    rv = self.dispatch_request() 
    File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1461, in dispatch_request 
    return self.view_functions[rule.endpoint](**req.view_args) 
    File "test.py", line 40, in wrapped 
    rv = self.decorator(func(*args, **kwargs)) 
    File "/usr/local/lib/python2.7/site-packages/flask_httpauth.py", line 48, in login_required 
    @wraps(f) 
    File "/usr/local/Cellar/python/2.7.11/Frameworks/Python.framework/Versions/2.7/lib/python2.7/functools.py", line 33, in update_wrapper 
    setattr(wrapper, attr, getattr(wrapped, attr)) 
AttributeError: 'str' object has no attribute '__module__' 

测试与Python 2.7.11,烧瓶HTTPAuth == 2.7.1,瓶== 0.10.1,任何见解将不胜赞赏。

回答

4

有趣的是,解决问题的有效方法是帮助解决问题。

的问题是在装饰呼叫括号:

rv = self.decorator(func(*args, **kwargs)) 

它更改为以下修复它:

rv = self.decorator(func)(*args, **kwargs) 

的装饰需要返回的功能,但通过传递参数func()直接我没有给它一个机会去做。

分解成一个单独的呼叫会作出更清楚,我想:

decorated_function = self.decorator(func) 
return decorated_function(*args, **kwargs)) 
1

有趣的问题。请注意,如果您只需要绕过认证逻辑,则可以更简单地执行此操作,而无需使用新的装饰器。刚刚纳入旁路逻辑到您的verify_password回调:

@auth.verify_password 
def verify(username, password): 
    if not sky_is_blue: 
     return True # let the request through, no questions asked! 
    # your authentication logic here 
    return False # this will trigger a 401 response 

现在你可以将login_required装饰像往常一样,和验证会成功,只要sky_is_blue == False

@app.route('/') 
@auth.login_required 
def index(): 
    """ 
    Get a token 
    """ 
    return "OK" 

希望这有助于!

+0

谢谢Miguel,特别是你在Flask-HTTPAuth上的工作!这是一个很好的建议,但是我想在这种情况下使用一个通用的解决方案,因为我还想有条件地应用[Flask-TokenAuth](https://github.com/al4/flask-tokenauth),我编写的模块是基于Flask-HTTPAuth和您的博客文章。最初我是通过对它进行分类来开始的,但是决定它已经变得足够不同以保证一个新的模块。 –

0

如果您需要对全部路径应用条件身份验证检查,而不在所有路由上定义login_required包装,则可以使用此解决方案。使用时只需将before_request钩:

@app.before_request 
def conditional_auth_check(): 
    if your_condition: 
     @auth.login_required 
     def _check_login(): 
      return None 

     return _check_login() 

login_required不一定需要直接包裹的路线。

相关问题