使用授权和认证构建一个React应用程序——手把手教程

400 阅读7分钟

在本教程中,我们将讨论授权以及如何用AWS Amplify的DataStore实现它。首先,让我们来了解一下什么是授权和认证。

授权--不同的用户有不同的操作,他们可以执行。认证- 确保某人是他们所说的人,例如通过让他们输入密码。

本教程将绕开React和AWS Amplify的教学--如果你是第一次接触React教程Amplify管理界面教程,请查看这个教程。你还需要了解React Router

我创建了一个带有一些启动代码的 repo,以便进入本教程的相关部分。如果你想跟着学的话,就去克隆它吧。在克隆的目录中运行npm i ,以获得所有需要安装的软件包。

我们将建立一个博客平台,有一个前台和后台的认证系统,有管理角色和限制内容创建者的某些操作。我们首先会有博客--类似于Medium出版物或Blogger博客。只有管理员用户能够创建新的博客,尽管任何人都可以查看博客的列表。博客内将有任何人都可以查看的帖子,但只有创建博客的人能够更新或删除博客。

使用管理界面创建一个博客

首先,我们需要为我们的应用程序创建数据模型。你可以去Amplify沙盒,以便开始。我们将创建两个模型,一个博客和一个帖子。博客将是一个出版物,它有一个附加的帖子集合。博客将只有一个名字,然后博客将有一个标题和内容。所有的字段都是字符串,我还把名字和标题作为必填字段。这两个模型之间也将有一个1:n的关系。

现在,按照管理界面提供的指导过程,继续部署你的数据模型。一旦部署完毕,进入管理界面,创建几个博客和几个帖子。

然后,我们将添加认证。在管理界面,点击 "认证 "标签,然后配置认证。我部署的是默认选项。

一旦你的认证部署完毕,添加授权规则。首先,点击博客模型,在右边的面板上,配置授权。在 "任何用API Key认证的人都可以...... "下取消对创建、更新和删除的检查。-- 我们将允许任何人查看博客,但只有管理员可以修改它们。然后,点击添加授权规则下拉菜单。从这里点击 "特定组 "下的 "创建新",并将你的组命名为 "管理员"。允许管理员用户执行所有行动。

现在我们将配置帖子的授权。选择该模型,并再次将 "任何用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)

然后,我们将所有的博客加载到应用程序的主页上。我从下面的代码开始,将为我的应用程序实现不同的路由。如果你使用的是克隆的模板,你的代码中已经有了这个。你还需要为BlogPagePostPageBlogCreate 创建React组件--现在这些可以只是空的组件。

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 multi-auth来实现基于用户的角色和内容所有权的不同权限。你可以继续用更多的表单、样式和数据渲染来扩展这个。我很想听听你对这个应用程序和这个新的Amplify功能的看法!