构建具有授权和身份验证的 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
您还需要在index.js
文件中配置 Amplify,以便您的前端链接到 Amplify 配置。您还需要在此步骤中配置多重身份验证。
import Amplify, { AuthModeStrategyType } from 'aws-amplify'
import awsconfig from './aws-exports'
Amplify.configure({
...awsconfig,
DataStore: {
authModeStrategyType: AuthModeStrategyType.MULTI_AUTH
}
})
实施身份验证
首先,我们需要为网站实现身份验证,以便用户登录,并且不同的帐户可以执行不同的操作。我创建了一个<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)
然后,我们将所有博客加载到应用的主页上。我将从以下代码开始,这些代码将为我的应用实现不同的路由。如果您使用的是克隆的样板,那么您的代码中已经包含了这些内容。您还需要为BlogPage
、PostPage
和创建 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
在<App>
组件中,首先导入Blog
模型。
import { Blog } from './models'
然后,创建一个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()
}, [])
然后,如果有当前用户,我们将获取该用户。我们还将检查该用户是否是管理员。
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()
}, [])
最后,我们需要根据用户是否登录来渲染不同的信息。首先,如果用户已登录,我们需要显示一个退出按钮。如果用户已退出,我们需要提供一个登录表单的链接。我们可以使用以下三元组来实现:
{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>}
我将这两条线路都添加到了我网站的主页路线。
<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>
这是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>
)
}
然后,我将添加此内容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()
}, [])
我们还为“创建新帖子”按钮添加功能,让您一键即可创建新帖子!所有者字段将自动填充当前登录用户的信息。
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
}))
}
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>
)
}
现在,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)
}
最后,在表单周围添加一个条件 - 我们只想在用户是管理员时呈现它!
if (!isAdmin) {
return <h2>You aren't allowed on this page!</h2>
} else {
return (
<form>
...
</form>
)
}
这是整个组件。
帖子页面
最后一个要实现的组件!这是帖子详情页。我们将实现一个编辑表单,以便内容所有者可以编辑他们的帖子。首先,为帖子创建一个 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>
)
}
然后,我们将创建一个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()
}, [])
然后,我们需要实现 handleSubmit 函数。我们需要复制原始帖子,更新所需的属性并将其保存到 DataStore。
const handleSubmit = async e => {
e.preventDefault()
await DataStore.save(Post.copyOf(post, updated => {
updated.title = title
updated.content = content
}))
}
最后,在 中return
,我们只希望当用户拥有该帖子时才渲染表单。在表单外部,添加以下条件,以便仅当帖子所有者是该用户时才渲染表单!Amplify 会自动为我们创建所有者字段。每次您创建新帖子时,它都会自动填充!
{user.attributes && (post.owner === user.attributes.email) && (
<form onSubmit={handleSubmit}>
...
</form>
)}
这是组件的最终代码。
结论
在本文中,我们将使用 Amplify 的 DataStore 多重授权功能,根据用户的角色和内容所有权实现不同的权限。您可以继续扩展此功能,添加更多表单、样式和数据渲染功能。我非常期待听到您对这款应用和 Amplify 新功能的看法!
文章来源:https://dev.to/aws/build-a-react-app-with-authorization-and-authentication-1mha