How to add login authentication to a Flask and React application.

2025-06-11

如何向 Flask 和 React 应用程序添加登录身份验证。

在 Flask 扩展中flask使用装饰器可以轻松添加身份验证。我有一篇关于如何向 Flask 应用程序添加基本身份验证的文章,您可以在这里阅读。@login_requiredFlask-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
Enter fullscreen mode Exit fullscreen mode

注意:如果您克隆了 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
Enter fullscreen mode Exit fullscreen mode

让我们回顾一下上面的代码:

首先,从已安装的扩展中导入所需的功能flask_jwt_extended

from flask_jwt_extended import create_access_token,get_jwt,get_jwt_identity, \
                               unset_jwt_cookies, jwt_required, JWTManager
Enter fullscreen mode Exit fullscreen mode

接下来,使用密钥配置 flask 应用程序实例,JWT然后将其作为参数传递给JWTManager函数并分配给jwt变量。

api.config["JWT_SECRET_KEY"] = "please-remember-to-change-me"
jwt = JWTManager(api)
Enter fullscreen mode Exit fullscreen mode

APItoken端点将包含一个POST请求方法。每当用户提交登录请求时,都会提取电子邮件地址和密码,并将其与硬编码的电子邮件地址(测试)和密码(测试)进行比较。请注意,在理想情况下,您需要将提取的登录详细信息与数据库中的数据进行比较。

如果登录详细信息不正确,则会将Wrong email or password带有状态代码的错误消息发送回用户。401UNAUTHORIZED Error

return {"msg": "Wrong email or password"}, 401
Enter fullscreen mode Exit fullscreen mode

否则,如果确认登录信息正确,则通过将 赋值emailidentity变量,为该电子邮件地址创建一个访问令牌。最后,将令牌返回给用户。

access_token = create_access_token(identity=email)

response = {"access_token":access_token}
return response
Enter fullscreen mode Exit fullscreen mode

要测试这一点,请使用以下命令启动后端服务器

npm run start-backend
Enter fullscreen mode Exit fullscreen mode

注意,上面的命令是package.json在 React 前端的文件中指定的。这在本系列的上一篇中已经完成。如果您还没有查看过,请前往那里了解如何设置。但是,如果您已经克隆了代码库,请继续。

接下来,打开PostmanPOST并向此 API 端点发送请求:

http://127.0.0.1:5000/token
Enter fullscreen mode Exit fullscreen mode

您会收到一条500 internal server错误消息👇 检查您的终端,您也会看到该错误消息👇
500 内部服务器错误

终端非类型错误
AttributeError: 'NoneType' object has no attribute 'get'POST发生错误的原因是,当您向 API 端点发出请求时没有指定登录详细信息,因此None将值作为参数传递给request.json.get函数。

返回POSTMAN并将登录详细信息与POST请求一起传递。 请确保按照上图所示调整设置。
登录详细信息

发出请求后,您将以以下形式获取访问令牌:

"access_token":"your access token will be here"
Enter fullscreen mode Exit fullscreen mode

您可以尝试输入错误的电子邮件或密码来查看401 UNAUTHORIZED error
401 UNAUTHORIZED 错误

注销 API 端点

@api.route("/logout", methods=["POST"])
def logout():
    response = jsonify({"msg": "logout successful"})
    unset_jwt_cookies(response)
    return response
Enter fullscreen mode Exit fullscreen mode

logout调用 API 端点时,response会传递给unset_jwt_cookies函数,该函数删除包含用户访问令牌的 cookie,并最终将成功消息返回给用户。

Postman再次转到并向logoutAPI 端点发出 POST 请求:

http://127.0.0.1:5000/logout
Enter fullscreen mode Exit fullscreen mode

您应该会收到以下回复👇
注销 API 调用

刷新令牌

生成的令牌始终会有一个lifespan有效期,超过该有效期后就会失效。为了确保用户登录时不会发生这种情况,您必须创建一个函数,在令牌接近其有效期时刷新它。

首先,指定lifespan生成的令牌的 并将其添加为应用程序的新配置。
注意:您可以根据应用程序的需要更改时间。

api.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)
Enter fullscreen mode Exit fullscreen mode

然后,在函数上方👇下方创建函数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
Enter fullscreen mode Exit fullscreen mode

装饰器after_request确保refresh_expiring_jwts在向受保护的 API 端点发出请求后运行该函数/profile。该函数将 API 调用的响应作为参数/profile

然后,获取用户令牌的当前到期时间戳,并将其与timestamp令牌的指定到期时间戳(设置为 30 分钟)进行比较。您也可以更改此设置。

如果用户令牌的到期时间戳恰好距离到期还有 30 分钟,则该用户的令牌将更改为一个有效期为 1 小时的新令牌,并将新令牌附加到返回给用户的响应中。但如果令牌尚未到期,则将原始响应发送给用户。

要完成后端设置,您需要将@jwt_required()装饰器添加到函数中my_profile,以防止未经身份验证的用户向 API 端点发出请求。首先,请使用以下命令向以下 URL/profile发出请求来测试 API 端点GETPostman

http://127.0.0.1:5000/profile
Enter fullscreen mode Exit fullscreen mode

您仍然应该获得上一篇文章中创建的字典的 json 形式。
配置文件 API 调用的 JSON 格式

接下来,添加@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
Enter fullscreen mode Exit fullscreen mode

/profile并尝试使用上述 URL向端点发出 API 请求。您将收到一个错误401 UNAUTHORIZED error,因为发出请求时缺少令牌。

未经授权的错误

用户登录并获取分配的令牌后,每次用户调用后端的 API 端点时都需要以Authorization Header以下格式发送令牌:

Authorization: Bearer <access_token>
Enter fullscreen mode Exit fullscreen mode

在转到前端之前,您还可以通过在调用受保护的API 端点Postman之前将用户的令牌添加到授权标头来对此进行测试。\profile

POST向下面的端点发出请求以获取您的令牌并将其复制出来。

http://127.0.0.1:5000/token
Enter fullscreen mode Exit fullscreen mode

接下来,添加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

Enter fullscreen mode Exit fullscreen mode

现在您可以转到反应前端,在那里您将进行 API 端点调用。

React 前端

在上一篇文章中,你只需要对文件进行少量更改App.js。但这次我们将进行重大更改,并创建新的组件。

在前端,Login将创建一个用于承载登录页面的组件。每当它检测到未经身份验证的用户尝试访问包含受保护 API 端点的页面时,就会渲染此组件。这将确保对后端发出的任何请求都附加了令牌。

首先,在目录components中创建一个新目录src并在其中创建四个新组件Login.jsuseToken.js。然后导航回基目录并在进入组件之前进行安装:Header.jsProfile.jsreact-router-dom

npm install react-router-dom
Enter fullscreen mode Exit fullscreen mode

前端存储token

登录后,后端生成的令牌需要存储在您的 Web 浏览器中。目前情况并非如此。每当用户刷新浏览器页面时,令牌都会被删除,并提示用户再次登录。

要解决此问题,您需要使用 Web 存储对象:localStorage或。您可以在此处sessionStorage阅读更多相关信息

i)sessionStorage:用户的令牌存储在浏览器中当前打开的标签页中。如果用户刷新页面,令牌仍然会保留。但是,如果用户在 Web 浏览器中打开同一页面的新标签页,令牌将不会反映在该页面上,因为新标签页与之前的标签页不共享存储空间。因此,系统会提示用户重新登录。

要查看实际效果,请打开您选择的任意网站,然后在浏览器中的任意页面上右键单击,打开Developer tools带有Inspect Element或选项的菜单。您还可以在该部分下查看网络存储。InspectApplication

打开您的控制台并使用 sessionStorage 函数将对象样本存储在网络存储中。

sessionStorage.setItem('test', 53)
Enter fullscreen mode Exit fullscreen mode

53然后要获取分配给上述键的值,test请运行:

sessionStorage.getItem('test')
Enter fullscreen mode Exit fullscreen mode

会话和本地存储测试
刷新页面并getItem再次运行该函数,您仍然会从存储中获取值。

现在,在新选项卡中打开您刚刚使用的同一页面的链接,并尝试通过控制台访问存储的对象值:

sessionStorage.getItem('test')
Enter fullscreen mode Exit fullscreen mode

您将获得一个null值,因为当前选项卡无法访问前一个选项卡的存储。

注意:在执行上述所有测试时,请留意web storage您上方部分发生的变化console

ii)localStorage:用户的令牌存储在通用存储中,所有标签页和浏览器窗口都可以访问。即使用户刷新或关闭页面、创建新的标签页或窗口,或者完全重启浏览器,令牌仍然会保留。

localStorage.setItem('test', 333)
Enter fullscreen mode Exit fullscreen mode

然后获取分配的值333

localStorage.getItem('test')
Enter fullscreen mode Exit fullscreen mode

尝试运行上面完成的重复测试,您会注意到可以从重复的页面访问该值。您还可以创建一个新的浏览器窗口,打开同一网站的任意页面,然后尝试访问上面设置的值。您会注意到您仍然可以访问它。这就是使用 的妙处localStorage,它确保用户只需登录一次,就可以轻松导航到网站上的任何页面。

完成后,您可以使用以下命令从存储中删除该对象:

localStorage.removeItem("token")
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

通过您在控制台中执行的测试,useToken组件中创建的功能应该很容易理解。

getToken函数用于检索token存储在中的localStorage,并且仅当存在时才返回标记,因此使用&&条件运算符。

useState 钩子用于处理token包含 token 值的变量的状态。这确保 React 应用程序在调用任何函数时始终重新加载。这样,当用户登录并存储 token 或用户注销时,应用程序也会感知到浏览器的 Web 存储发生了变化,并做出相应的反应,要么重定向到用户想要访问的页面,要么在用户注销后返回到登录页面。

saveToken函数处理用户登录时获得的令牌的存储,并且其中的函数使用作为参数传递给函数的变量来更新变量setToken的状态tokentokensaveToken

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;

Enter fullscreen mode Exit fullscreen mode

在文件的顶部,用于处理配置文件组件 URL 路由的、 、BrowserRouter函数从已安装的包中导入。其他创建的组件也从该文件夹中导入。RouteRoutesreact-router-domcomponents

在函数中App,将函数调用时返回的值对象useToken解构,并将值分别赋给tokenremoveTokensetToken变量。

const { token, removeToken, setToken } = useToken();
Enter fullscreen mode Exit fullscreen mode

接下来,BrowserRouter将该函数作为父组件,并在其中Header放置该组件,并将removeToken函数作为参数传递,该函数prop在 react 中被调用。

<Header token={removeToken}/>
Enter fullscreen mode Exit fullscreen mode

然后使用 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;

Enter fullscreen mode Exit fullscreen mode

上面的代码应该很容易理解,它的作用是使用用户提供的登录详细信息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;

Enter fullscreen mode Exit fullscreen mode

一旦用户点击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;

Enter fullscreen mode Exit fullscreen mode

之前的代码片段App.js已移至此处。这包含受保护的端点\profileGET每当点击按钮时,都会向该端点发送一个请求方法Click me,并以用户的详细信息作为响应。

为了让用户能够访问\profileAPI 端点的数据,必须将包含令牌的授权标头添加到 axiosGET请求中。

headers: {
        Authorization: 'Bearer ' + props.token
      }
Enter fullscreen mode Exit fullscreen mode

如果响应包含access token,则表示当前令牌即将过期,并且服务器已创建了新令牌。因此,本地存储中的令牌将使用新生成的令牌进行更新。

res.access_token && props.setToken(res.access_token)
Enter fullscreen mode Exit fullscreen mode

应用程序.css

您还需要更改页眉的 CSS 样式。在第 16 行,您将看到页眉组件的样式.App-header。注释掉或删除该/* min-height: 100vh; */代码,您的应用程序最终会看起来像这样👇:

最终应用外观

现在要测试您的应用程序,请通过运行以下脚本启动后端服务器

npm run start-backend
Enter fullscreen mode Exit fullscreen mode

其次是 :

npm start
Enter fullscreen mode Exit fullscreen mode

然后在您的 Web 浏览器中导航到该http://localhost:3000/profileURL,由于该页面受到保护,系统会提示您登录。希望您还记得登录信息:email:test和。您也可以在部分password:test打开,以监控令牌的存储和删除情况。localStorageApplicationDeveloper tools

一路走来,我们终于完成了本教程。我相信,凭借你所学的知识,你可以轻松地验证你的 Flask + React 应用程序。恭喜你获得了新的知识。

如果您有任何疑问,请随时以评论形式提交,或在领英推特上给我留言,我会尽快回复。再见👋

鏂囩珷鏉ユ簮锛�https://dev.to/nagatodev/how-to-add-login-authentication-to-a-flask-and-react-application-23i7
PREV
每个开发人员都必须了解这 7 个 JavaScript 概念。
NEXT
JavaScript 中的多态性总结:结论: