用户携带授权token访问时,其jwt的所处位置列表,默认是在请求头部headers中验证。
可以通过JWT_TOKEN_LOCATION进行全局配置,设置token是在请求头部,还是cookies,还是json, 还是查询参数query_string 四种方式。
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"
}
通过标头工作 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;
}
如果您使用 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;
}
这看起来与 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;
}