如何向 Flask 和 React 应用程序添加登录身份验证。
在 Flask 扩展中,flask
使用装饰器可以轻松添加身份验证。我有一篇关于如何向 Flask 应用程序添加基本身份验证的文章,您可以在这里阅读。@login_required
Flask-login
但是,由于您将使用 API 端点,因此无法使用上述方法,因为当@login_required
装饰器发现未经身份验证的用户尝试访问受保护的页面时,它会将应用程序重定向到HTML page
。这违背了创建 API 端点的想法,因为 API 仅设计用于返回json
格式的数据。
在本系列的这一部分中,你将学习如何为上一篇构建的已连接的 React 和 Flask 应用添加身份验证功能。身份验证将通过 Flask 扩展程序flask-jwt-extended完成。
先决条件
1) 对 Flask 框架有初级理解。如果你是新手,Flask
可以看看我的文章,了解如何设置 Flask 项目以及如何将其与Jinja模板引擎配合使用。
2)强烈建议您阅读上一篇文章。您也可以在 Github repo中获取相关文件。
3) 熟悉的基础知识ReactJs
。您将使用useState
钩子,从 API 端点获取数据axios
,并使用react-router-dom
来处理组件的路由。
让我们开始吧!!
Flask 后端
安装烧瓶延伸部分。
导航到backend
目录并运行:
pip install flask-jwt-extended
注意:如果您克隆了 repo,则不需要运行上面的命令,只需按照README.md
文件中的说明设置您的 flask 应用程序即可。
基础.py
/profile
您将向上一节教程中创建的 API 端点添加身份验证。导航到base.py
您在应用程序后端目录中创建的脚本,以创建 token(login) 和 logout API 端点。
令牌(登录)API 端点
import json
from flask import Flask, request, jsonify
from datetime import datetime, timedelta, timezone
from flask_jwt_extended import create_access_token,get_jwt,get_jwt_identity, \
unset_jwt_cookies, jwt_required, JWTManager
api = Flask(__name__)
api.config["JWT_SECRET_KEY"] = "please-remember-to-change-me"
jwt = JWTManager(api)
@api.route('/token', methods=["POST"])
def create_token():
email = request.json.get("email", None)
password = request.json.get("password", None)
if email != "test" or password != "test":
return {"msg": "Wrong email or password"}, 401
access_token = create_access_token(identity=email)
response = {"access_token":access_token}
return response
@api.route('/profile')
def my_profile():
response_body = {
"name": "Nagato",
"about" :"Hello! I'm a full stack developer that loves python and javascript"
}
return response_body
让我们回顾一下上面的代码:
首先,从已安装的扩展中导入所需的功能flask_jwt_extended
。
from flask_jwt_extended import create_access_token,get_jwt,get_jwt_identity, \
unset_jwt_cookies, jwt_required, JWTManager
接下来,使用密钥配置 flask 应用程序实例,JWT
然后将其作为参数传递给JWTManager
函数并分配给jwt
变量。
api.config["JWT_SECRET_KEY"] = "please-remember-to-change-me"
jwt = JWTManager(api)
APItoken
端点将包含一个POST
请求方法。每当用户提交登录请求时,都会提取电子邮件地址和密码,并将其与硬编码的电子邮件地址(测试)和密码(测试)进行比较。请注意,在理想情况下,您需要将提取的登录详细信息与数据库中的数据进行比较。
如果登录详细信息不正确,则会将Wrong email or password
带有状态代码的错误消息发送回用户。401
UNAUTHORIZED Error
return {"msg": "Wrong email or password"}, 401
否则,如果确认登录信息正确,则通过将 赋值email
给identity
变量,为该电子邮件地址创建一个访问令牌。最后,将令牌返回给用户。
access_token = create_access_token(identity=email)
response = {"access_token":access_token}
return response
要测试这一点,请使用以下命令启动后端服务器
npm run start-backend
请注意,上面的命令是package.json
在 React 前端的文件中指定的。这在本系列的上一篇中已经完成。如果您还没有查看过,请前往那里了解如何设置。但是,如果您已经克隆了代码库,请继续。
接下来,打开PostmanPOST
并向此 API 端点发送请求:
http://127.0.0.1:5000/token
您会收到一条500 internal server
错误消息👇 检查您的终端,您也会看到该错误消息👇
AttributeError: 'NoneType' object has no attribute 'get'
POST
发生错误的原因是,当您向 API 端点发出请求时没有指定登录详细信息,因此None
将值作为参数传递给request.json.get
函数。
返回POSTMAN
并将登录详细信息与POST
请求一起传递。 请确保按照上图所示调整设置。
发出请求后,您将以以下形式获取访问令牌:
"access_token":"your access token will be here"
您可以尝试输入错误的电子邮件或密码来查看401 UNAUTHORIZED error
注销 API 端点
@api.route("/logout", methods=["POST"])
def logout():
response = jsonify({"msg": "logout successful"})
unset_jwt_cookies(response)
return response
当logout
调用 API 端点时,response
会传递给unset_jwt_cookies
函数,该函数删除包含用户访问令牌的 cookie,并最终将成功消息返回给用户。
Postman
再次转到并向logout
API 端点发出 POST 请求:
http://127.0.0.1:5000/logout
刷新令牌
生成的令牌始终会有一个lifespan
有效期,超过该有效期后就会失效。为了确保用户登录时不会发生这种情况,您必须创建一个函数,在令牌接近其有效期时刷新它。
首先,指定lifespan
生成的令牌的 并将其添加为应用程序的新配置。
注意:您可以根据应用程序的需要更改时间。
api.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)
然后,在函数上方👇下方创建函数create_token
:
@api.after_request
def refresh_expiring_jwts(response):
try:
exp_timestamp = get_jwt()["exp"]
now = datetime.now(timezone.utc)
target_timestamp = datetime.timestamp(now + timedelta(minutes=30))
if target_timestamp > exp_timestamp:
access_token = create_access_token(identity=get_jwt_identity())
data = response.get_json()
if type(data) is dict:
data["access_token"] = access_token
response.data = json.dumps(data)
return response
except (RuntimeError, KeyError):
# Case where there is not a valid JWT. Just return the original respone
return response
装饰器after_request
确保refresh_expiring_jwts
在向受保护的 API 端点发出请求后运行该函数/profile
。该函数将 API 调用的响应作为参数/profile
。
然后,获取用户令牌的当前到期时间戳,并将其与timestamp
令牌的指定到期时间戳(设置为 30 分钟)进行比较。您也可以更改此设置。
如果用户令牌的到期时间戳恰好距离到期还有 30 分钟,则该用户的令牌将更改为一个有效期为 1 小时的新令牌,并将新令牌附加到返回给用户的响应中。但如果令牌尚未到期,则将原始响应发送给用户。
要完成后端设置,您需要将@jwt_required()
装饰器添加到函数中my_profile
,以防止未经身份验证的用户向 API 端点发出请求。首先,请使用以下命令向以下 URL/profile
发出请求来测试 API 端点:GET
Postman
http://127.0.0.1:5000/profile
接下来,添加@jwt_required()
装饰器
@api.route('/profile')
@jwt_required() #new line
def my_profile():
response_body = {
"name": "Nagato",
"about" :"Hello! I'm a full stack developer that loves python and javascript"
}
return response_body
/profile
并尝试使用上述 URL向端点发出 API 请求。您将收到一个错误401 UNAUTHORIZED error
,因为发出请求时缺少令牌。
用户登录并获取分配的令牌后,每次用户调用后端的 API 端点时都需要以Authorization Header
以下格式发送令牌:
Authorization: Bearer <access_token>
在转到前端之前,您还可以通过在调用受保护的API 端点Postman
之前将用户的令牌添加到授权标头来对此进行测试。\profile
POST
向下面的端点发出请求以获取您的令牌并将其复制出来。
http://127.0.0.1:5000/token
接下来,添加authorization
以您的token
为值的标头键,然后发送GET
请求,您应该会得到一个包含您的姓名和 about_me 信息的字典的 json 响应。
恭喜您已成功向 API 端点添加身份验证。经过修改和添加后,base.py
脚本的最终外观应该是这样的。
import json
from flask import Flask, request, jsonify
from datetime import datetime, timedelta, timezone
from flask_jwt_extended import create_access_token,get_jwt,get_jwt_identity, \
unset_jwt_cookies, jwt_required, JWTManager
api = Flask(__name__)
api.config["JWT_SECRET_KEY"] = "please-remember-to-change-me"
api.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)
jwt = JWTManager(api)
@api.after_request
def refresh_expiring_jwts(response):
try:
exp_timestamp = get_jwt()["exp"]
now = datetime.now(timezone.utc)
target_timestamp = datetime.timestamp(now + timedelta(minutes=30))
if target_timestamp > exp_timestamp:
access_token = create_access_token(identity=get_jwt_identity())
data = response.get_json()
if type(data) is dict:
data["access_token"] = access_token
response.data = json.dumps(data)
return response
except (RuntimeError, KeyError):
# Case where there is not a valid JWT. Just return the original respone
return response
@api.route('/token', methods=["POST"])
def create_token():
email = request.json.get("email", None)
password = request.json.get("password", None)
if email != "test" or password != "test":
return {"msg": "Wrong email or password"}, 401
access_token = create_access_token(identity=email)
response = {"access_token":access_token}
return response
@api.route("/logout", methods=["POST"])
def logout():
response = jsonify({"msg": "logout successful"})
unset_jwt_cookies(response)
return response
@api.route('/profile')
@jwt_required()
def my_profile():
response_body = {
"name": "Nagato",
"about" :"Hello! I'm a full stack developer that loves python and javascript"
}
return response_body
现在您可以转到反应前端,在那里您将进行 API 端点调用。
React 前端
在上一篇文章中,你只需要对文件进行少量更改App.js
。但这次我们将进行重大更改,并创建新的组件。
在前端,Login
将创建一个用于承载登录页面的组件。每当它检测到未经身份验证的用户尝试访问包含受保护 API 端点的页面时,就会渲染此组件。这将确保对后端发出的任何请求都附加了令牌。
首先,在目录components
中创建一个新目录,src
并在其中创建四个新组件Login.js
、useToken.js
和。然后导航回基目录并在进入组件之前进行安装:Header.js
Profile.js
react-router-dom
npm install react-router-dom
前端存储token
登录后,后端生成的令牌需要存储在您的 Web 浏览器中。目前情况并非如此。每当用户刷新浏览器页面时,令牌都会被删除,并提示用户再次登录。
要解决此问题,您需要使用 Web 存储对象:localStorage
或。您可以在此处sessionStorage
阅读更多相关信息。
i)sessionStorage:用户的令牌存储在浏览器中当前打开的标签页中。如果用户刷新页面,令牌仍然会保留。但是,如果用户在 Web 浏览器中打开同一页面的新标签页,令牌将不会反映在该页面上,因为新标签页与之前的标签页不共享存储空间。因此,系统会提示用户重新登录。
要查看实际效果,请打开您选择的任意网站,然后在浏览器中的任意页面上右键单击,打开Developer tools
带有Inspect Element
或选项的菜单。您还可以在该部分下查看网络存储。Inspect
Application
打开您的控制台并使用 sessionStorage 函数将对象样本存储在网络存储中。
sessionStorage.setItem('test', 53)
53
然后要获取分配给上述键的值,test
请运行:
sessionStorage.getItem('test')
刷新页面并getItem
再次运行该函数,您仍然会从存储中获取值。
现在,在新选项卡中打开您刚刚使用的同一页面的链接,并尝试通过控制台访问存储的对象值:
sessionStorage.getItem('test')
您将获得一个null
值,因为当前选项卡无法访问前一个选项卡的存储。
注意:在执行上述所有测试时,请留意web storage
您上方部分发生的变化console
。
ii)localStorage:用户的令牌存储在通用存储中,所有标签页和浏览器窗口都可以访问。即使用户刷新或关闭页面、创建新的标签页或窗口,或者完全重启浏览器,令牌仍然会保留。
localStorage.setItem('test', 333)
然后获取分配的值333
:
localStorage.getItem('test')
尝试运行上面完成的重复测试,您会注意到可以从重复的页面访问该值。您还可以创建一个新的浏览器窗口,打开同一网站的任意页面,然后尝试访问上面设置的值。您会注意到您仍然可以访问它。这就是使用 的妙处localStorage
,它确保用户只需登录一次,就可以轻松导航到网站上的任何页面。
完成后,您可以使用以下命令从存储中删除该对象:
localStorage.removeItem("token")
useToken.js
现在,你需要在你的 React 代码中复制上面所做的操作。打开useToken
组件。
import { useState } from 'react';
function useToken() {
function getToken() {
const userToken = localStorage.getItem('token');
return userToken && userToken
}
const [token, setToken] = useState(getToken());
function saveToken(userToken) {
localStorage.setItem('token', userToken);
setToken(userToken);
};
function removeToken() {
localStorage.removeItem("token");
setToken(null);
}
return {
setToken: saveToken,
token,
removeToken
}
}
export default useToken;
通过您在控制台中执行的测试,useToken
组件中创建的功能应该很容易理解。
该getToken
函数用于检索token
存储在中的localStorage
,并且仅当存在时才返回标记,因此使用&&
条件运算符。
useState 钩子用于处理token
包含 token 值的变量的状态。这确保 React 应用程序在调用任何函数时始终重新加载。这样,当用户登录并存储 token 或用户注销时,应用程序也会感知到浏览器的 Web 存储发生了变化,并做出相应的反应,要么重定向到用户想要访问的页面,要么在用户注销后返回到登录页面。
该saveToken
函数处理用户登录时获得的令牌的存储,并且其中的函数使用作为参数传递给函数的变量来更新变量setToken
的状态。token
token
saveToken
该removeToken
函数从本地存储中删除令牌,并在每次调用时将令牌返回到空状态。
最后,saveToken
将函数作为值赋给setToken变量,函数token
本身的值以及removeToken
函数本身的值都作为调用函数的结果返回useToken
。
App.js
我告诉过你,你会做出重大改变,对吧?😜。清理App.js
;上次添加的所有代码都将移动到Profile
组件中。
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import Login from './components/Login'
import Profile from './components/Profile'
import Header from './components/Header'
import useToken from './components/useToken'
import './App.css'
function App() {
const { token, removeToken, setToken } = useToken();
return (
<BrowserRouter>
<div className="App">
<Header token={removeToken}/>
{!token && token!=="" &&token!== undefined?
<Login setToken={setToken} />
:(
<>
<Routes>
<Route exact path="/profile" element={<Profile token={token} setToken={setToken}/>}></Route>
</Routes>
</>
)}
</div>
</BrowserRouter>
);
}
export default App;
在文件的顶部,用于处理配置文件组件 URL 路由的、 、BrowserRouter
函数从已安装的包中导入。其他创建的组件也从该文件夹中导入。Route
Routes
react-router-dom
components
在函数中App
,将函数调用时返回的值对象useToken
解构,并将值分别赋给token
、removeToken
和setToken
变量。
const { token, removeToken, setToken } = useToken();
接下来,BrowserRouter
将该函数作为父组件,并在其中Header
放置该组件,并将removeToken
函数作为参数传递,该函数prop
在 react 中被调用。
<Header token={removeToken}/>
然后使用 JavaScript 条件三元运算符来确保用户必须拥有 token 才能访问profile
组件。如果用户没有 token,Login
则使用作为参数传递的函数渲染组件setToken
。如果用户已经拥有 token,则Profile
使用 URL 路径/profile
渲染组件并显示给用户。
您可以在此处阅读有关如何使用的更多信息React Router
Login
现在,您需要分别在、Header 和Profile
组件文件中创建 Login、Header 和 Profile 功能。
登录.js
import { useState } from 'react';
import axios from "axios";
function Login(props) {
const [loginForm, setloginForm] = useState({
email: "",
password: ""
})
function logMeIn(event) {
axios({
method: "POST",
url:"/token",
data:{
email: loginForm.email,
password: loginForm.password
}
})
.then((response) => {
props.setToken(response.data.access_token)
}).catch((error) => {
if (error.response) {
console.log(error.response)
console.log(error.response.status)
console.log(error.response.headers)
}
})
setloginForm(({
email: "",
password: ""}))
event.preventDefault()
}
function handleChange(event) {
const {value, name} = event.target
setloginForm(prevNote => ({
...prevNote, [name]: value})
)}
return (
<div>
<h1>Login</h1>
<form className="login">
<input onChange={handleChange}
type="email"
text={loginForm.email}
name="email"
placeholder="Email"
value={loginForm.email} />
<input onChange={handleChange}
type="password"
text={loginForm.password}
name="password"
placeholder="Password"
value={loginForm.password} />
<button onClick={logMeIn}>Submit</button>
</form>
</div>
);
}
export default Login;
上面的代码应该很容易理解,它的作用是使用用户提供的登录详细信息POST
向/token
后端的 API 端点发出请求,然后返回用户的令牌,并使用作为 prop 传递给 Login 函数的函数将令牌存储在本地 Web 存储中setToken
。
Header.js
import logo from '../logo.svg'
import axios from "axios";
function Header(props) {
function logMeOut() {
axios({
method: "POST",
url:"/logout",
})
.then((response) => {
props.token()
}).catch((error) => {
if (error.response) {
console.log(error.response)
console.log(error.response.status)
console.log(error.response.headers)
}
})}
return(
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<button onClick={logMeOut}>
Logout
</button>
</header>
)
}
export default Header;
一旦用户点击Logout
按钮,POST
就会向 API 端点发出请求/logout
,并在后端清除存储用户 JWToken 的 Cookie。Axios
响应函数用于调用removeToken
删除token
本地 Web 存储中存储的 Cookie 的函数。现在,如果用户尝试访问该/profile
页面,则会被重定向到登录页面。
Profile.js
import { useState } from 'react'
import axios from "axios";
function Profile(props) {
const [profileData, setProfileData] = useState(null)
function getData() {
axios({
method: "GET",
url:"/profile",
headers: {
Authorization: 'Bearer ' + props.token
}
})
.then((response) => {
const res =response.data
res.access_token && props.setToken(res.access_token)
setProfileData(({
profile_name: res.name,
about_me: res.about}))
}).catch((error) => {
if (error.response) {
console.log(error.response)
console.log(error.response.status)
console.log(error.response.headers)
}
})}
return (
<div className="Profile">
<p>To get your profile details: </p><button onClick={getData}>Click me</button>
{profileData && <div>
<p>Profile name: {profileData.profile_name}</p>
<p>About me: {profileData.about_me}</p>
</div>
}
</div>
);
}
export default Profile;
之前的代码片段App.js
已移至此处。这包含受保护的端点\profile
。GET
每当点击按钮时,都会向该端点发送一个请求方法Click me
,并以用户的详细信息作为响应。
为了让用户能够访问\profile
API 端点的数据,必须将包含令牌的授权标头添加到 axiosGET
请求中。
headers: {
Authorization: 'Bearer ' + props.token
}
如果响应包含access token
,则表示当前令牌即将过期,并且服务器已创建了新令牌。因此,本地存储中的令牌将使用新生成的令牌进行更新。
res.access_token && props.setToken(res.access_token)
应用程序.css
您还需要更改页眉的 CSS 样式。在第 16 行,您将看到页眉组件的样式.App-header
。注释掉或删除该/* min-height: 100vh; */
代码,您的应用程序最终会看起来像这样👇:
现在要测试您的应用程序,请通过运行以下脚本启动后端服务器
npm run start-backend
其次是 :
npm start
然后在您的 Web 浏览器中导航到该http://localhost:3000/profile
URL,由于该页面受到保护,系统会提示您登录。希望您还记得登录信息:email:test
和。您也可以在部分下password:test
打开,以监控令牌的存储和删除情况。localStorage
Application
Developer tools
一路走来,我们终于完成了本教程。我相信,凭借你所学的知识,你可以轻松地验证你的 Flask + React 应用程序。恭喜你获得了新的知识。
如果您有任何疑问,请随时以评论形式提交,或在领英或推特上给我留言,我会尽快回复。再见👋
鏂囩珷鏉ユ簮锛�https://dev.to/nagatodev/how-to-add-login-authentication-to-a-flask-and-react-application-23i7