当 Flask-JWT-Extended 遇到 ExpiredSignatureError

最近我们在使用 Flask 开发后端接口的时候,遇到一个奇怪的问题。它的具体表现是:在默认的 Debug 环境下运行服务时,如果请求头带有一个已经过期的 Access Token 它能正常返回预期的 401 错误。但是一旦上线到线上环境(基于 uWSGI + Nginx),接口就会返回 500 错误,后台记录错误信息是 jwt.exceptions.ExpiredSignatureError: Signature has expired。经过一番排查,找到了问题的原因。

在 Stack Overflow 上有人提出了跟我们遇到的完全一样的问题。在这个提问下,有人引用了一个来自 Flask-JWT-Extended 官方代码仓库的 Issue

提出这个 Issue 的人也是遇到了完全一样的问题,而且描述得更详细。

对于样例代码:

'''In app.py'''
from flask_jwt_extended import JWTManager
from flask import Flask

jwtmanager = JWTManager()

def create_app():
    app = Flask(__name__)

def register_extensions(app):
    jwtmanager.init_app(app)


'''in views.py'''
from flask_restful import Resource, Api
from flask_jwt_extended import jwt_required

api = Api()

class TokenResource(Resource):
    method_decorators = [jwt_required]

class HelloWorld(TokenResource):
    def get(self):
        return 'Hello, World!'

api.add_resource(HelloWorld, '/hello')

该接口运行在调试模式下,可以正常返回预期的结果:

{
  "msg": "Token has expired"
}

但是如果使用 gunicorn 启动服务,则会返回 Internal Server Error:

{
    "message": "Internal Server Error"
}

在 Issue 的讨论中 Flask-JWT-Extended 项目的发起人给出了解决的方案,即在 Flask 的配置文件里增加一个配置:

PROPAGATE_EXCEPTIONS = True

这个问题同时也存在于一个已经被归档的项目 flask-jwt,并在 flask-jwt/issues/74 被提出过。

至于安全性问题,Flask-JWT-Extended 的作者认为这个设置并不会引起内部代码的对外泄露风险。并举了例子:

from flask import Flask, jsonify
from flask_jwt_extended import  JWTManager, jwt_required
from flask_jwt_extended.exceptions import NoAuthorizationError

app = Flask(__name__)

# Setup the Flask-JWT-Extended extension
app.config['JWT_SECRET_KEY'] = 'super-secret'
app.config['PROPAGATE_EXCEPTIONS'] = True
jwt = JWTManager(app)

@app.route('/login', methods=['POST'])
def login():
    raise ValueError("exception without errorhandler")

@app.route('/protected', methods=['GET'])
def protected():
    raise NoAuthorizationError("exception with errorhandler")

if __name__ == '__main__':
    app.run()
$ http POST :5000/login
HTTP/1.0 500 INTERNAL SERVER ERROR
Content-Length: 291
Content-Type: text/html
Date: Sun, 24 Jun 2018 03:38:22 GMT
Server: Werkzeug/0.14.1 Python/3.6.4

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request.  Either the server is overloaded or there is an error in the application.</p>


$ http :5000/protected
HTTP/1.0 401 UNAUTHORIZED
Content-Length: 38
Content-Type: application/json
Date: Sun, 24 Jun 2018 03:38:31 GMT
Server: Werkzeug/0.14.1 Python/3.6.4

{
    "msg": "exception with errorhandler"
}

关于 Flask 的 PROPAGATE_EXCEPTIONS 配置的作用,根据官方文档的描述:

Exceptions are re-raised rather than being handled by the app’s error handlers. If not set, this is implicitly true if TESTING or DEBUG is enabled.

根据描述,这个参数是用于控制异常是否重新抛出,而不是被应用的错误处理器处理。如果没有设置,当 TESTINGDEBUG 启用时,这个参数会被隐式设置为 True。这也就是为什么在调试模式下可以正常返回 401 错误,而在生产环境下会返回 500 错误的原因。

所以在使用 Flask-JWT-Extended 的时候,应该显性地在 Flask 的生产环境配置文件里设置 PROPAGATE_EXCEPTIONS = True 才能充分利用插件对异常的再处理能力。

更新于 2024-11-29