Flask 学习-31.flask_jwt_extended 验证token四种方headers/cookies/json/query_string

席烨
2023-12-01

前言

用户携带授权token访问时,其jwt的所处位置列表,默认是在请求头部headers中验证。
可以通过JWT_TOKEN_LOCATION进行全局配置,设置token是在请求头部,还是cookies,还是json, 还是查询参数query_string 四种方式。

JWT_TOKEN_LOCATION 全局配置

JWT_TOKEN_LOCATION 配置参数可以全局配置允许JWT执行以下操作的所有方式,发送到您的web应用程序。默认情况下,这将仅为headers

app.config["JWT_TOKEN_LOCATION"] = ["headers", "cookies", "json", "query_string"]

或者在 config.py 配置文件中

    # jwt 四种认证方式
    JWT_TOKEN_LOCATION = ["headers", "cookies", "json", "query_string"]

JWT 可以通过多种不同方式随请求一起发送。JWT_TOKEN_LOCATION 您可以通过配置选项控制您希望在 Flask 应用程序中接受 JWT 的方式。您还可以通过locations. jwt_required()
官方文档使用示例

from flask import Flask
from flask import jsonify

from flask_jwt_extended import create_access_token
from flask_jwt_extended import jwt_required
from flask_jwt_extended import JWTManager
from flask_jwt_extended import set_access_cookies
from flask_jwt_extended import unset_jwt_cookies

app = Flask(__name__)

# Here you can globally configure all the ways you want to allow JWTs to
# be sent to your web application. By default, this will be only headers.
app.config["JWT_TOKEN_LOCATION"] = ["headers", "cookies", "json", "query_string"]

# If true this will only allow the cookies that contain your JWTs to be sent
# over https. In production, this should always be set to True
app.config["JWT_COOKIE_SECURE"] = False

# Change this in your code!
app.config["JWT_SECRET_KEY"] = "super-secret"

jwt = JWTManager(app)


@app.route("/login_without_cookies", methods=["POST"])
def login_without_cookies():
    access_token = create_access_token(identity="example_user")
    return jsonify(access_token=access_token)


@app.route("/login_with_cookies", methods=["POST"])
def login_with_cookies():
    response = jsonify({"msg": "login successful"})
    access_token = create_access_token(identity="example_user")
    set_access_cookies(response, access_token)
    return response


@app.route("/logout_with_cookies", methods=["POST"])
def logout_with_cookies():
    response = jsonify({"msg": "logout successful"})
    unset_jwt_cookies(response)
    return response


@app.route("/protected", methods=["GET", "POST"])
@jwt_required()
def protected():
    return jsonify(foo="bar")


@app.route("/only_headers")
@jwt_required(locations=["headers"])
def only_headers():
    return jsonify(foo="baz")


if __name__ == "__main__":
    app.run()

测试验证

1.先测试token放到headers的情况,主要是以下2个视图

@app.route("/login_without_cookies", methods=["POST"])
def login_without_cookies():
    access_token = create_access_token(identity="example_user")
    return jsonify(access_token=access_token)


@app.route("/only_headers")
@jwt_required(locations=["headers"])
def only_headers():
    return jsonify(foo="baz")

先登录

POST http://127.0.0.1:5000/login_without_cookies HTTP/1.1
User-Agent: Fiddler
Host: 127.0.0.1:5000
Content-Type: application/json
Content-Length: 0

HTTP/1.1 200 OK
Server: Werkzeug/2.2.2 Python/3.8.5
Date: Thu, 01 Sep 2022 04:10:32 GMT
Content-Type: application/json
Content-Length: 365
Connection: close

{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2MjAwNTQzMiwianRpIjoiYzQzYTAwMTEtMzU5OC00OTYwLTgwYTAtODViMzExMGM3MjRiIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImV4YW1wbGVfdXNlciIsIm5iZiI6MTY2MjAwNTQzMiwiY3NyZiI6IjY4ZjZiOGFlLTcwZjYtNDIwNy05YjU3LWFiZTBmZTVjMzk0MyIsImV4cCI6MTY2MjAwOTAzMn0.ttnNTdyliZXAa2_d9esJeuofHnwEkfHqshJ15zkXKzs"
}

token 放到headers 中验证

GET http://127.0.0.1:5000/only_headers HTTP/1.1
User-Agent: Fiddler
Host: 127.0.0.1:5000
Content-Type: application/json
Content-Length: 0
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2MjAwNTQzMiwianRpIjoiYzQzYTAwMTEtMzU5OC00OTYwLTgwYTAtODViMzExMGM3MjRiIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImV4YW1wbGVfdXNlciIsIm5iZiI6MTY2MjAwNTQzMiwiY3NyZiI6IjY4ZjZiOGFlLTcwZjYtNDIwNy05YjU3LWFiZTBmZTVjMzk0MyIsImV4cCI6MTY2MjAwOTAzMn0.ttnNTdyliZXAa2_d9esJeuofHnwEkfHqshJ15zkXKzs

HTTP/1.1 200 OK
Server: Werkzeug/2.2.2 Python/3.8.5
Date: Thu, 01 Sep 2022 04:13:14 GMT
Content-Type: application/json
Content-Length: 19
Connection: close

{
  "foo": "baz"
}

访问/protected 地址也是可以的,默认四种验证方式都支持。

2.cookies验证

在web网站中,使用cookies保存token会比较常见,主要用到登录和退出登录方法

@app.route("/login_with_cookies", methods=["POST"])
def login_with_cookies():
    response = jsonify({"msg": "login successful"})
    access_token = create_access_token(identity="example_user")
    set_access_cookies(response, access_token)
    return response


@app.route("/logout_with_cookies", methods=["POST"])
def logout_with_cookies():
    response = jsonify({"msg": "logout successful"})
    unset_jwt_cookies(response)
    return response

访问/login_with_cookies登录后会返回cookies

POST http://127.0.0.1:5000/login_with_cookies HTTP/1.1
User-Agent: Fiddler
Host: 127.0.0.1:5000
Content-Type: application/json
Content-Length: 0

HTTP/1.1 200 OK
Server: Werkzeug/2.2.2 Python/3.8.5
Date: Thu, 01 Sep 2022 04:16:24 GMT
Content-Type: application/json
Content-Length: 32
Set-Cookie: access_token_cookie=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2MjAwNTc4NCwianRpIjoiZWNhYWViYTEtNDZiNC00ZGM1LWEyMzItMTJjYTcyYTgzN2U3IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImV4YW1wbGVfdXNlciIsIm5iZiI6MTY2MjAwNTc4NCwiY3NyZiI6IjYxNTAwMjk3LTg1ZTctNGY1Zi04Zjk4LTVkZDVlOGY4MTBhZCIsImV4cCI6MTY2MjAwOTM4NH0.B2L-WGf92ZORlcfvx5PqR9XSWx_rm_fSxwmQsHeRBdE; HttpOnly; Path=/
Set-Cookie: csrf_access_token=61500297-85e7-4f5f-8f98-5dd5e8f810ad; Path=/
Connection: close

{
  "msg": "login successful"
}

带上cookie的请求

GET http://127.0.0.1:5000/protected HTTP/1.1
User-Agent: Fiddler
Host: 127.0.0.1:5000
Content-Type: application/json
Content-Length: 0
Cookie: access_token_cookie=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2MjAwNTc4NCwianRpIjoiZWNhYWViYTEtNDZiNC00ZGM1LWEyMzItMTJjYTcyYTgzN2U3IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImV4YW1wbGVfdXNlciIsIm5iZiI6MTY2MjAwNTc4NCwiY3NyZiI6IjYxNTAwMjk3LTg1ZTctNGY1Zi04Zjk4LTVkZDVlOGY4MTBhZCIsImV4cCI6MTY2MjAwOTM4NH0.B2L-WGf92ZORlcfvx5PqR9XSWx_rm_fSxwmQsHeRBdE; HttpOnly; Path=/

HTTP/1.1 200 OK
Server: Werkzeug/2.2.2 Python/3.8.5
Date: Thu, 01 Sep 2022 04:18:50 GMT
Content-Type: application/json
Content-Length: 19
Connection: close

{
  "foo": "bar"
}

如果是post请求,请求头部还需带上X-CSRF-TOKEN

POST http://127.0.0.1:5000/protected HTTP/1.1
User-Agent: Fiddler
Host: 127.0.0.1:5000
Content-Type: application/json
Content-Length: 0
Cookie: access_token_cookie=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2MjAwNTc4NCwianRpIjoiZWNhYWViYTEtNDZiNC00ZGM1LWEyMzItMTJjYTcyYTgzN2U3IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImV4YW1wbGVfdXNlciIsIm5iZiI6MTY2MjAwNTc4NCwiY3NyZiI6IjYxNTAwMjk3LTg1ZTctNGY1Zi04Zjk4LTVkZDVlOGY4MTBhZCIsImV4cCI6MTY2MjAwOTM4NH0.B2L-WGf92ZORlcfvx5PqR9XSWx_rm_fSxwmQsHeRBdE; HttpOnly; Path=/
X-CSRF-TOKEN: 61500297-85e7-4f5f-8f98-5dd5e8f810ad

HTTP/1.1 200 OK
Server: Werkzeug/2.2.2 Python/3.8.5
Date: Thu, 01 Sep 2022 04:23:36 GMT
Content-Type: application/json
Content-Length: 19
Connection: close

{
  "foo": "bar"
}

3.json 中带上access_token

POST http://127.0.0.1:5000/protected HTTP/1.1
User-Agent: Fiddler
Host: 127.0.0.1:5000
Content-Type: application/json
Content-Length: 370

{ 
    "access_token" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2MjAwNTc4NCwianRpIjoiZWNhYWViYTEtNDZiNC00ZGM1LWEyMzItMTJjYTcyYTgzN2U3IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImV4YW1wbGVfdXNlciIsIm5iZiI6MTY2MjAwNTc4NCwiY3NyZiI6IjYxNTAwMjk3LTg1ZTctNGY1Zi04Zjk4LTVkZDVlOGY4MTBhZCIsImV4cCI6MTY2MjAwOTM4NH0.B2L-WGf92ZORlcfvx5PqR9XSWx_rm_fSxwmQsHeRBdE"
}

HTTP/1.1 200 OK
Server: Werkzeug/2.2.2 Python/3.8.5
Date: Thu, 01 Sep 2022 05:04:24 GMT
Content-Type: application/json
Content-Length: 19
Connection: close

{
  "foo": "bar"
}

4.query_string查询字符串

请求参数带上‘jwt=${token}’,示例

GET http://127.0.0.1:5000/protected?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2MjAwNTc4NCwianRpIjoiZWNhYWViYTEtNDZiNC00ZGM1LWEyMzItMTJjYTcyYTgzN2U3IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImV4YW1wbGVfdXNlciIsIm5iZiI6MTY2MjAwNTc4NCwiY3NyZiI6IjYxNTAwMjk3LTg1ZTctNGY1Zi04Zjk4LTVkZDVlOGY4MTBhZCIsImV4cCI6MTY2MjAwOTM4NH0.B2L-WGf92ZORlcfvx5PqR9XSWx_rm_fSxwmQsHeRBdE HTTP/1.1
User-Agent: Fiddler
Host: 127.0.0.1:5000
Content-Type: application/json
Content-Length: 0

HTTP/1.1 200 OK
Server: Werkzeug/2.2.2 Python/3.8.5
Date: Thu, 01 Sep 2022 05:06:40 GMT
Content-Type: application/json
Content-Length: 19
Connection: close

{
  "foo": "bar"
}

Web 浏览器中使用javascript 获取token

Headers

通过标头工作 JWT 是一个非常简单的过程。您需要做的就是在登录时存储令牌,并在每次向受保护路由发出请求时将令牌添加为标头。注销就像删除令牌一样简单。

async function login() {
  const response = await fetch('/login_without_cookies', {method: 'post'});
  const result = await response.json();
  localStorage.setItem('jwt', result.access_token);
}

function logout() {
  localStorage.removeItem('jwt');
}

async function makeRequestWithJWT() {
  const options = {
    method: 'post',
    headers: {
      Authorization: `Bearer ${localStorage.getItem('jwt')}`,
    }
  };
  const response = await fetch('/protected', options);
  const result = await response.json();
  return result;
}

Cookies

如果您使用 Web 浏览器,Cookie 是处理 JWT 的绝佳方式。与标头方法相比,它们提供了一些不错的好处:它们可以配置为仅通过 HTTPS 发送。这可以防止 JWT 通过不安全的连接意外发送并可能受到损害。
它们存储在一个仅限 http 的 cookie 中,这可以防止 XSS 攻击能够窃取底层 JWT。您的 Flask 应用程序可以隐式刷新即将到期的 JWT,这简化了保持活动用户登录的逻辑。下一节将详细介绍!
当然,在使用 cookie 时,您还需要做一些额外的工作来防止跨站请求伪造 (CSRF) 攻击。在这个扩展中,我们通过称为双重提交验证的东西来处理这个问题。
双重提交验证背后的基本思想是,仅当请求中还存在特殊的双重提交令牌时,来自 cookie 的 JWT 才会被认为是有效的,并且双重提交令牌不能是由 Web 浏览器自动发送的东西(即它不能是另一个cookie)。
默认情况下,我们通过在有人登录时设置两个 cookie 来完成此操作。第一个 cookie 包含 JWT,并且在该 JWT 中编码的是双重提交令牌。此 cookie 设置为仅 http,因此无法通过 javascript 访问(这是防止 XSS 攻击能够窃取 JWT 的原因)。我们设置的第二个 cookie 仅包含相同的双重提交令牌,但这次是在 javascript 可读的 cookie 中。每当发出请求时,它都需要包含一个X-CSRF-TOKEN标头,其中包含双重提交令牌的值。如果此标头中的值与存储在 JWT 中的值不匹配,则请求被踢出无效。
因为双重提交令牌需要作为标头出现(不会在请求中自动发送),并且在不同域上运行的一些恶意 javascript 将无法读取您网站上包含双重提交令牌的 cookie,我们已成功阻止任何 CSRF 攻击。
这确实意味着每当您发出请求时,您都需要手动包含X-CSRF-TOKEN标头,否则您的请求也将被踢出无效。让我们看看如何做到这一点:

async function login() {
  await fetch('/login_with_cookies', {method: 'post'});
}

async function logout() {
  await fetch('/logout_with_cookies', {method: 'post'});
}

function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
}

async function makeRequestWithJWT() {
  const options = {
    method: 'post',
    credentials: 'same-origin',
    headers: {
      'X-CSRF-TOKEN': getCookie('csrf_access_token'),
    },
  };
  const response = await fetch('/protected', options);
  const result = await response.json();
  return result;
}

请注意,还有其他 CSRF 选项,例如在表单中查找双重提交令牌、更改 cookie 路径等,可用于根据应用程序的需要进行定制。请参阅 跨站点请求伪造选项https://flask-jwt-extended.readthedocs.io/en/stable/options/#cross-site-request-forgery-options了解详细信息。

查询字符串

您还可以将 JWT 作为查询字符串的一部分发送。但是,请务必注意,在大多数情况下,我们建议不要这样做。它可能会导致一些不明显的安全问题,例如将 JWT 保存在浏览器历史记录中或将 JWT 登录到后端服务器,这都可能导致令牌受损。但是,此功能可能会提供一些有限的用处,例如发送密码重置链接,因此我们在此扩展程序中支持它。

async function login() {
  const response = await fetch('/login_without_cookies', {method: 'post'});
  const result = await response.json();
  localStorage.setItem('jwt', result.access_token);
}

function logout() {
  localStorage.removeItem('jwt');
}

async function makeRequestWithJWT() {
  const jwt = localStorage.getItem('jwt')
  const response = await fetch(`/protected?jwt=${jwt}`, {method: 'post'});
  const result = await response.json();
  return result;
}

JSON

这看起来与 Headers 方法非常相似,只是我们将 JWT 作为 JSON Body 的一部分而不是 header 发送。请注意,HEAD 或 GET 请求不能将 JSON 正文作为请求的一部分,因此这仅适用于 POST/PUT/PATCH/DELETE/等操作。
在大多数情况下,在 JSON 正文中发送 JWT 可能不是很有用,但无论如何我们都包含了它的选项。

async function login() {
  const response = await fetch('/login_without_cookies', {method: 'post'});
  const result = await response.json();
  localStorage.setItem('jwt', result.access_token);
}

function logout() {
  localStorage.removeItem('jwt');
}

// Note that if we change the method to `get` this will blow up with a
// "TypeError: Window.fetch: HEAD or GET Request cannot have a body"
async function makeRequestWithJWT() {
  const options = {
    method: 'post',
    body: JSON.stringify({access_token: localStorage.getItem('jwt')}),
    headers: {
      'Content-Type': 'application/json',
    },
  };
  const response = await fetch('/protected', options);
  const result = await response.json();
  return result;
}
 类似资料: