构建具有授权和身份验证的 React 应用程序

2025-05-24

构建具有授权和身份验证的 React 应用程序

2024 年 6 月 27 日:这篇博文使用 Amplify Gen 1,如果您要启动新的 Amplify 应用程序,我建议您尝试Gen 2

在本教程中,我们将讨论授权以及如何使用 AWS Amplify 的 DataStore 实现授权。首先,让我们了解一下授权和身份验证的含义:

授权- 不同的用户可以执行不同的操作。身份验证- 确保用户的身份与其声称的身份相符,例如通过要求其输入密码。

请注意,我是 AWS Amplify 团队的开发倡导者,如果您对此有任何反馈或疑问,请联系我或在我们的 discord 上提问 - discord.gg/amplify!

本教程将略过 React 和 AWS Amplify 的讲解。如果您对它们都不熟悉,可以查看React 教程Amplify Admin UI 教程。您还需要了解React Router

为了方便使用本教程的相关部分,我创建了一个包含一些入门代码的仓库。如果您想继续学习,请将其克隆下来。npm i在克隆的目录中运行即可安装所有需要的软件包。

我们将构建一个博客平台,该平台包含一个前端和后端身份验证系统,该系统包含管理员角色,并且某些操作仅限于内容创建者。我们首先会创建博客——类似于 Medium 出版物或 Blogger 博客。只有管理员用户可以创建新博客,但任何人都可以查看博客列表。博客中会包含任何人都可以查看的帖子,但只有博客创建者才能更新或删除博客。

使用管理界面创建博客

首先,我们需要为应用创建数据模型。您可以前往Amplify Sandbox开始使用。我们将创建两个模型:博客模型和帖子模型。博客模型将是一个包含帖子集合的出版物。博客模型只有一个名称,而帖子模型将包含标题和内容。所有字段都将是字符串,我还将名称和标题设置为必填字段。这两个模型之间也将是 1:n 的关系。

现在,按照管理界面提供的引导流程部署你的数据模型。部署完成后,进入管理界面,创建一些博客和一些帖子。

然后,我们将添加身份验证。在管理界面中,点击“身份验证”选项卡,然后配置身份验证。我使用默认选项进行部署。

身份验证部署完成后,添加授权规则。首先,点击“博客”模型,然后在右侧面板上配置授权。取消勾选“任何通过 API 密钥验证的用户都可以……”下的“创建、更新和删除”——我们将允许任何人查看博客,但只有管理员可以修改博客。然后,点击“添加授权规则”下拉菜单。点击“特定组”下的“新建”,并将您的组命名为“管理员”。允许管理员用户执行所有操作。

现在我们将配置帖子的授权。选择该模型,并再次将“任何使用 API 密钥验证的用户”的权限更改为“阅读”帖子。然后将“启用所有者授权”切换为开启状态。在“拒绝其他经过身份验证的用户对所有者的记录执行以下操作:”下,选择“更新”和“删除”——我们希望任何人都能阅读帖子,但只有帖子的所有者才能修改现有帖子。我们还需要允许某些人创建帖子!在“添加授权规则”下,选择“任何使用以下身份验证的登录用户”,然后选择“Cognito”。

返回代码目录,使用您的应用 ID 运行 Amplify pull 命令——您可以在管理界面的“本地设置说明”下找到此命令。如果您不使用上面克隆的存储库,请安装 Amplify JavaScript 和 React 库。

$ npm i aws-amplify @aws-amplify/ui-react
Enter fullscreen mode Exit fullscreen mode

您还需要在index.js文件中配置 Amplify,以便您的前端链接到 Amplify 配置。您还需要在此步骤中配置多重身份验证。

import Amplify, { AuthModeStrategyType } from 'aws-amplify'
import awsconfig from './aws-exports'

Amplify.configure({
  ...awsconfig,
  DataStore: {
    authModeStrategyType: AuthModeStrategyType.MULTI_AUTH
  }
})
Enter fullscreen mode Exit fullscreen mode

实施身份验证

首先,我们需要为网站实现身份验证,以便用户登录,并且不同的帐户可以执行不同的操作。我创建了一个<SignIn>带有路由的组件。然后,添加withAuthenticator高阶组件来实现用户身份验证流程!

// SignIn.js

import { withAuthenticator } from '@aws-amplify/ui-react'
import React from 'react'

import { Link } from 'react-router-dom'

function SignIn () {
  return (
    <div>
      <h1>Hello!</h1>
      <Link to='/'>home</Link>
    </div>
  )
}

+ export default withAuthenticator(SignIn)
Enter fullscreen mode Exit fullscreen mode

然后,我们将所有博客加载到应用的主页上。我将从以下代码开始,这些代码将为我的应用实现不同的路由。如果您使用的是克隆的样板,那么您的代码中已经包含了这些内容。您还需要为BlogPagePostPage和创建 React 组件BlogCreate——这些组件目前可以作为空组件。

import './App.css'

import { Auth } from 'aws-amplify'
import { DataStore } from '@aws-amplify/datastore'
import { useEffect, useState } from 'react'
import { Switch, Route, Link } from 'react-router-dom'

import BlogPage from './BlogPage'
import PostPage from './PostPage'
import BlogCreate from './BlogCreate'
import SignIn from './SignIn'

import { Blog } from './models'

function App () {
  const [blogs, setBlogs] = useState([])

  return (
    <div className='App'>
      <Switch>
        <Route path='/sign-in'>
          <SignIn />
        </Route>
        <Route path='/blog/create'>
          <BlogCreate isAdmin={isAdmin} />
        </Route>
        <Route path='/blog/:name'>
          <BlogPage user={user} />
        </Route>
        <Route path='/post/:name'>
          <PostPage user={user} />
        </Route>
        <Route path='/' exact>
          <h1>Blogs</h1>
          {blogs.map(blog => (
            <Link to={`/blog/${blog.name}`} key={blog.id}>
              <h2>{blog.name}</h2>
            </Link>
          ))}
        </Route>
      </Switch>
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

<App>组件中,首先导入Blog模型。

import { Blog } from './models'
Enter fullscreen mode Exit fullscreen mode

然后,创建一个useEffect用于将数据拉到该组件的。

// create a state variable for the blogs to be stored in
const [blogs, setBlogs] = useState([])

useEffect(() => {
  const getData = async () => {
    try {
      // query for all blog posts, then store them in state
      const blogData = await DataStore.query(Blog)
      setBlogs(blogData)
    } catch (err) {
      console.error(err)
    }
  }
  getData()
}, [])
Enter fullscreen mode Exit fullscreen mode

然后,如果有当前用户,我们将获取该用户。我们还将检查该用户是否是管理员。

const [blogs, setBlogs] = useState([])
+ const [isAdmin, setIsAdmin] = useState(false)
+ const [user, setUser] = useState({})

useEffect(() => {w
  const getData = async () => {
    try {
      const blogData = await DataStore.query(Blog)
      setBlogs(blogData)
      // fetch the current signed in user
+ const user = await Auth.currentAuthenticatedUser()
      // check to see if they're a member of the admin user group
+ setIsAdmin(user.signInUserSession.accessToken.payload['cognito:groups'].includes('admin'))
+ setUser(user)
    } catch (err) {
      console.error(err)
    }
  }
  getData()
}, [])
Enter fullscreen mode Exit fullscreen mode

最后,我们需要根据用户是否登录来渲染不同的信息。首先,如果用户已登录,我们需要显示一个退出按钮。如果用户已退出,我们需要提供一个登录表单的链接。我们可以使用以下三元组来实现:

{user.attributes 
  ? <button onClick={async () => await Auth.signOut()}>Sign Out</button> 
  : <Link to='/sign-in'>Sign In</Link>}
Enter fullscreen mode Exit fullscreen mode

您还可以添加此代码片段,以便管理员用户拥有创建新博客的链接。

{isAdmin && <Link to='/blog/create'>Create a Blog</Link>}
Enter fullscreen mode Exit fullscreen mode

我将这两条线路都添加到了我网站的主页路线。

  <Route path='/' exact>
    <h1>Blogs</h1>
+ {user.attributes 
+ ? <button onClick={async () => await Auth.signOut()}>Sign Out</button> 
+ : <Link to='/sign-in'>Sign In</Link>}
+ {isAdmin && <Link to='/blog/create'>Create a Blog</Link>}
    {blogs.map(blog => (
      <Link to={`/blog/${blog.name}`} key={blog.id}>
        <h2>{blog.name}</h2>
      </Link>
    ))}
  </Route>
Enter fullscreen mode Exit fullscreen mode

这是App 组件的完整代码。

博客页面

现在,我们将实现显示单个博客的组件。首先,我们将查询博客的信息,然后获取附加到博客的帖子。在我的应用中,我使用 React Router 为每个遵循 url 模式的博客创建了博客详情页面/blog/:blogName。然后,我将使用:blogName获取该博客的所有信息。

我先创建一个页面来渲染每篇帖子。同时,我还会添加一个按钮来创建新帖子,但前提是用户登录后才能创建:

import { DataStore } from 'aws-amplify'
import { useEffect, useState } from 'react'
import { useParams, Link } from 'react-router-dom'

import { Post, Blog } from './models'

export default function BlogPage ({ user }) {
  const { name } = useParams()

  const createPost = async () => {
  }

  return (
    <div>
      <h1>{name}</h1>
      {user && <button onClick={createPost}>create new post</button>}
      {
        posts.map(post => (
          <h2 key={post.id}>
            <Link to={`/post/${post.title}`}>
              {post.title}
            </Link>
          </h2>)
        )
    }
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

然后,我将添加此内容useEffect以加载所有帖子。

// body of BlogPage component inside BlogPage.js
  const [blog, setBlog] = useState({})
  const [posts, setPosts] = useState([])
  useEffect(() => {
    const getData = async () => {
      // find the blog whose name equals the one in the url
      const data = await DataStore.query(Blog, p => p.name('eq', name))
      setBlog(data[0].id)
      // find all the posts whose blogID matches the above post's id
      const posts = await DataStore.query(Post, p => p.blogID('eq', data[0].id))
      setPosts(posts)
    }
    getData()
  }, [])
Enter fullscreen mode Exit fullscreen mode

我们还为“创建新帖子”按钮添加功能,让您一键即可创建新帖子!所有者字段将自动填充当前登录用户的信息。

const createPost = async () => {
   const title = window.prompt('title')
   const content = window.prompt('content')

   const newPost = await DataStore.save(new Post({
      title,
      content,
      blogID: blog.id
    }))
}
Enter fullscreen mode Exit fullscreen mode

BlogPage 组件的最终代码。

博客创建

我们来让它支持创建新博客。在<BlogCreate>组件内部。首先,创建一个标准的 React 表单,允许用户创建新博客。

import { DataStore } from 'aws-amplify'
import { useState } from 'react'

import { Blog } from './models'

export default function BlogCreate ({ isAdmin }) {
  const [name, setName] = useState('')

  const createBlog = async e => {
    e.preventDefault()
  }

    return (
      <form onSubmit={createBlog}>
        <h2>Create a Blog</h2>
        <label htmlFor='name'>Name</label>
        <input type='text' id='name' onChange={e => setName(e.target.value)} />
        <input type='submit' value='create' />
      </form>
    )
}
Enter fullscreen mode Exit fullscreen mode

现在,createBlog通过添加以下内容来实现该功能:

const createBlog = async e => {
  e.preventDefault()
  // create a new blog instance and save it to DataStore
  const newBlog = await DataStore.save(new Blog({
    name
  }))
  console.log(newBlog)
}
Enter fullscreen mode Exit fullscreen mode

最后,在表单周围添加一个条件 - 我们只想在用户是管理员时呈现它!

  if (!isAdmin) {
    return <h2>You aren't allowed on this page!</h2>
  } else {
    return (
      <form>
       ...
      </form>
    )
  }
Enter fullscreen mode Exit fullscreen mode

这是整个组件。

帖子页面

最后一个要实现的组件!这是帖子详情页。我们将实现一个编辑表单,以便内容所有者可以编辑他们的帖子。首先,为帖子创建一个 React 表单。我们再次使用 React Router 将帖子名称发送给该组件。

import { DataStore } from 'aws-amplify'
import { useEffect, useState } from 'react'
import { useParams, Link } from 'react-router-dom'

import { Post } from './models'

export default function PostPage ({ user }) {
  const { name } = useParams()

  const [post, setPost] = useState([])
  const [title, setTitle] = useState('')
  const [content, setContent] = useState('')

  const handleSubmit = async e => {
    e.preventDefault()
  }
  return (
    <div>
      <h1>{name}</h1>
      <form onSubmit={handleSubmit}>
        <label>Title</label>
        <input type='text' value={title} onChange={e => setTitle(e.target.value)} />
        <label>Content</label>
        <input type='text' value={content} onChange={e => setContent(e.target.value)} />
        <input type='submit' value='update' />
      </form>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

然后,我们将创建一个useEffect组件,用于从 DataStore 获取帖子信息并将其渲染到表单中。请注意,如果两个帖子同名,则此方法将无法正常工作!在更大规模的应用中,您需要在每个帖子的 URL 中添加一些区分符。

useEffect(() => {
  const getData = async () => {
    const posts = await DataStore.query(Post, p => p.title('eq', name))
    setPost(posts[0])
    setTitle(posts[0].title)
    setContent(posts[0].content)
  }
  getData()
}, [])
Enter fullscreen mode Exit fullscreen mode

然后,我们需要实现 handleSubmit 函数。我们需要复制原始帖子,更新所需的属性并将其保存到 DataStore。

const handleSubmit = async e => {
  e.preventDefault()
  await DataStore.save(Post.copyOf(post, updated => {
    updated.title = title
    updated.content = content
  }))
}
Enter fullscreen mode Exit fullscreen mode

最后,在 中return,我们只希望当用户拥有该帖子时才渲染表单。在表单外部,添加以下条件,以便仅当帖子所有者是该用户时才渲染表单!Amplify 会自动为我们创建所有者字段。每次您创建新帖子时,它都会自动填充!

 {user.attributes && (post.owner === user.attributes.email) && (
   <form onSubmit={handleSubmit}>
   ...
   </form>
 )}
Enter fullscreen mode Exit fullscreen mode

这是组件的最终代码

结论

在本文中,我们将使用 Amplify 的 DataStore 多重授权功能,根据用户的角色和内容所有权实现不同的权限。您可以继续扩展此功能,添加更多表单、样式和数据渲染功能。我非常期待听到您对这款应用和 Amplify 新功能的看法!

文章来源:https://dev.to/aws/build-a-react-app-with-authorization-and-authentication-1mha
PREV
使用 AWS Amplify 构建全栈应用程序:初学者指南
NEXT
完整的 Web 开发初学者指南