用Strapi和Next.js实现用户注册和认证

2,690 阅读7分钟

本文将指导你使用Strapi CMS和Next.js来创建一个可以注册和验证用户并持续会话的应用程序。

设置Next.js应用程序

让我们从设置Next.js应用程序开始。设置的最简单方法是使用create-next-app 包。

在你的终端中,运行npx create-next-app next-app ,其中next-app 是你喜欢的任何项目名称。

这将生成一个名为next-app 的文件夹。然后,在你的终端屏幕上导航到该文件夹,运行npm run dev 。你将看到应用程序在本地主机3000端口运行。

设置Strapi

就像Next.js一样,Strapi也有一个npm包,可以方便地启动一个项目。

运行npx create-strapi-app strapi-app ,创建一个Strapi项目。当提示时,选择Quickstart 选项。

一旦安装完成,导航到strapi-app 文件夹并运行npm run dev 。当你访问localhost:1337 ,你将被要求创建一个管理员帐户。这样做之后,你应该被引导到Strapi控制面板。

在Strapi中注册一个用户

在Strapi的侧边栏,在集合类型下,你应该看到Users ,这是默认创建的。在这里你可以手动创建用户,并看到所有的现有用户。

默认情况下,Strapi的API提供了创建、获取和认证用户的端点。

/auth/local/register 是我们需要发送用户名、电子邮件和密码的端点,以便注册一个用户。

对于这个例子的应用程序,我们将有/register/profile 路径,在这里你可以分别注册一个用户,然后看到一个用户的资料。登录界面将出现在主页上。

让我们从<RegisterComponent/> 组件开始。

这个组件返回一个非常基本的表单,用户在其中输入他们的用户名、电子邮件和密码。

然后userData 对象持有这些信息并将其发送到/api/register 端点,这是我们将在Next应用程序的后台部分创建的东西。请注意,为了进行API调用,我使用了axios 包,所以请确保你把它作为一个依赖项来安装。

npm install --save axios

一旦对/api/register 的请求成功,我们就将应用程序路由到/profile ,以便显示用户信息。我们将在后面创建/profile 页面。

下面的文件是/components/registerComponent.jsx ,我们将在/pages/register.jsx 中使用它。

import { useState } from 'react';
import { useRouter } from 'next/router';
import axios from 'axios';

const RegisterComponent = () => {
  const router = useRouter();
  const [userData, setUserData] = useState({
    username: '',
    email: '',
    password: '',
  })

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      await axios.post('/api/register', userData);
      router.replace('/profile');
    } catch (err) {
      console.log(err.response.data);
    }
  }

  const handleChange = (e) => {
    const { name, value } = e.target;
    setUserData({...userData, [name]: value });
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Username:
        <input type="text" name="username" onChange={e => handleChange(e)} />
      </label>
      <br />
      <label>
        Email:
        <input type="text" name="email" onChange={e => handleChange(e)} />
      </label>
      <br />
      <label>
        Password:
        <input type="password" name="password" onChange={e => handleChange(e)} />
      </label>
      <br />
      <button>Register</button>
    </form>
  )
}

export default RegisterComponent;

现在在pages 文件夹下创建register.jsx ,只需返回<RegisterComponent/>

import RegisterComponent from "../components/registerComponent";
const Register = () => (
  <RegisterComponent />
)
export default Register;

我们需要做的下一件事是创建/api/register 端点。

/pages ,你会发现/api 文件夹。在该文件夹中,创建register.js 。这样,当我们在Next.js客户端向/api/register 发出请求时,将由这个文件来处理。

import axios from 'axios';
import { setCookie } from 'nookies'

export default async (req, res) => {
  const { username, password, email } = req.body;

  try {
    const response = await axios.post('http://localhost:1337/auth/local/register', {
      username,
      email,
      password,
    })

    setCookie({ res }, 'jwt', response.data.jwt, {
      httpOnly: true,
      secure: process.env.NODE_ENV !== 'development',
      maxAge: 30 * 24 * 60 * 60,
      path: '/',
    });

    res.status(200).end();
  } catch (e) {
    res.status(400).send(e.response.data.message[0].messages[0]);
  }
}

如前所述,注册用户的端点是/auth/local/register 。由于Strapi运行在localhost:1337 ,因此我们将一个请求发送到 [http://localhost:1337/auth/local/register](http://localhost:1337/auth/local/register)并附上我们从请求正文中检索到的用户名、电子邮件和密码数据。

请注意,如果你将你的Strapi应用程序部署到远程服务器上,那么你需要相应地替换基础URL。好的做法是将基本URL存储在环境变量中,然后使用它而不是硬编码URL。然而,为了简单起见,我将在本文中只使用localhost:1337

从POST请求返回的响应包含一个JWT令牌。这个令牌是特定于该用户的,应该被安全地存储,以便自动验证用户。

nookies 包是一个辅助函数的集合,用于处理Next.js应用程序中的cookie。

setCookie 函数将响应对象作为第一个参数。第二个参数是cookie的名称,它可以是你选择的任何东西。

第三个参数是cookie的值。在我们的例子中,这是从请求响应中返回的JWT token,而作为最后一个参数,我们可以传递一个带有选项的对象。

httpOnly 标志可以防止客户端--在浏览器上运行的javascript--访问cookie,这样你就可以保护cookie免受可能的跨站脚本(XSS)攻击。

secure 标志确保cookie只通过安全的https 连接传输。因为localhost不是一个https 的连接,如果我们运行应用程序的环境是development ,我们就把它设置为false

maxAge 决定cookie的有效期是多少秒。在这个例子中,它被设置为30天。

path 决定cookie应在哪个路径上有效。它被设置为/ ,以便使cookie对所有的路径都有效。

请注意,在请求失败的情况下,我们正在发送一条消息。从e.response.data.message[0].messages[0] ,这个消息包含关于请求失败原因的有用信息。这可能是一个无效的电子邮件,或一个已经在使用的用户名,等等。

因此,这些信息可以用来在注册失败的情况下显示适当的错误信息。

为了简单起见,我们不在客户端中处理这个错误信息。我们只是把它记录下来,显示在浏览器的控制台中。

在Strapi中创建用户配置文件页面

这是/profile 路径,用户数据,如用户名和电子邮件,在这里显示。这个路由可以直接访问,或者,用户在登录或注册时可以被引导到这里。

因为这个页面需要使用cookie来验证用户,而且我们已经在服务器端用httpsOnly 标志设置了cookie,所以我们也需要在服务器上读取cookie。

getServerSideProps 是一个Next.js函数,在每次请求时在服务器上运行。在这个函数中,我们可以解析cookie,以便访问JWT令牌,然后向Strapi发出请求,获取用户数据。然后我们可以从函数中返回这些数据,将其作为道具暴露在页面中。

/pages/profile.jsx 的内容如下。

import { useRouter } from 'next/router';
import axios from 'axios';
import nookies from 'nookies';

const Profile = (props) => {
  const router = useRouter();
  const { user: { email, username } } = props;

  const logout = async () => {
    try {
      await axios.get('/api/logout');
      router.push('/');
    } catch (e) {
      console.log(e);
    }
  }

  return (
    <div>
      <div>Username: {username}</div>
      <div>Email: {email}</div>
      <button onClick={logout}>Logout</button>
    </div>
  )
}

export const getServerSideProps = async (ctx) => {
  const cookies = nookies.get(ctx)
  let user = null;

  if (cookies?.jwt) {
    try {
      const { data } = await axios.get('http://localhost:1337/users/me', {
        headers: {
          Authorization:
            `Bearer ${cookies.jwt}`,
          },
      });
      user = data;
    } catch (e) {
      console.log(e);
    }
  }

  if (!user) {
    return {
      redirect: {
        permanent: false,
        destination: '/'
      }
    }
  }

  return {
    props: {
      user
    }
  }
}

export default Profile;

让我们先关注一下getServerSideProps 这个函数。我们再次使用nookies 包,从上下文对象(ctx)中获取cookie。我们设置的cookie的名称是jwt ,因此要检查是否存在cookies.jwt

如果这个特定的cookie存在,那么我们就向本地Strapi服务器的/users/me 端点发送一个请求,其中包含JWT令牌的Authorization 头,以获取用户信息。

如果cookie不存在或者JWT令牌无效,user 变量将保持在null ,我们将页面重定向到/ ,也就是带有登录界面的主页。否则,我们在props 对象中返回user ,然后在我们导出的Profile 函数中作为一个道具可用。

Profile 函数返回一个非常基本的标记,其中显示了用户名和电子邮件,还有一个注销按钮,它调用logout 函数。

这个函数向/api/logout 端点发送请求,以删除cookie,然后将页面路由到/

下面是/pages/api/logout.js

import { destroyCookie } from 'nookies'

export default async (req, res) => {
  destroyCookie({ res }, 'jwt', {
    path: '/',
  });

  res.status(200).end();
}

登录用户

下面是/pages/index.jsx 的内容,这是主页和<LoginComponent> 的位置,还有一个注册按钮,它将页面路由到/register 路由。

import { useRouter } from 'next/router';
import axios from 'axios';
import nookies from 'nookies';
import LoginComponent from '../components/loginComponent';

const Home = () => {
  const router = useRouter();
  const goToRegister = () => {
    router.push('/register');
  }

  return (
    <div>
      <LoginComponent />
      <button onClick={goToRegister}>Register</button>
    </div>
  )
}

export const getServerSideProps = async (ctx) => {
  const cookies = nookies.get(ctx)
  let user = null;

  if (cookies?.jwt) {
    try {
      const { data } = await axios.get('http://localhost:1337/users/me', {
        headers: {
          Authorization:
            `Bearer ${cookies.jwt}`,
          },
      });
      user = data;
    } catch (e) {
      console.log(e);
    }
  }

  if (user) {
    return {
      redirect: {
        permanent: false,
        destination: '/profile'
      }
    }
  }

  return {
    props: {}
  }
}

export default Home;

正如我们在/pages/profile.jsx ,我们再次使用getServerSideProps 函数来获取cookie,然后检查用户的存在。

如果用户确实存在,那么我们就重定向到/profile 路由。如果不存在,我们返回一个空的props 对象,并留在同一路径上渲染<LoginComponent/> ,其内容如下。

import { useState } from 'react';
import { useRouter } from 'next/router';
import axios from 'axios';

const LoginComponent = () => {
  const router = useRouter();
  const [userData, setUserData] = useState({
    identifier: '',
    password: '',
  });

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      await axios.post('/api/login', { ...userData });
      router.push('/profile');
    } catch (err) {
      console.log(err.response.data);
    }
  }

  const handleChange = (e) => {
    const { name, value } = e.target;
    setUserData({...userData, [name]: value })
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <label>
          Email:
          <input type="text" name="identifier" onChange={e => handleChange(e)} />
        </label>
        <br />
        <label>
          Password:
          <input type="password" name="password" onChange={e => handleChange(e)} />
        </label>
        <br />
        <button>Login</button>
      </form>
    </div>
  )
}

export default LoginComponent;

这个组件渲染了一个表单,以获得电子邮件和密码。当表单被提交后,向/api/login 发出POST请求,页面被重定向到/profile 路由。

下面你可以看到的内容是/api/login.js

import axios from 'axios';
import { setCookie } from 'nookies'

export default async (req, res) => {
  const { password, identifier } = req.body;

  try {
    const postRes = await axios.post('http://localhost:1337/auth/local', {
      identifier,
      password,
    })

    setCookie({ res }, 'jwt', postRes.data.jwt, {
      httpOnly: true,
      secure: process.env.NODE_ENV !== 'development',
      maxAge: 30 * 24 * 60 * 60,
      path: '/',
    });

    res.status(200).end();
  } catch (e) {
    res.status(400).send(e.response.data.message[0].messages[0]);
  }
}

/auth/local 是一个用于登录用户的Strapi端点。发送到这个端点的对象应该有identifierpassword 键。

正如我们在/api/register.js 中所做的那样,我们也设置了一个具有相同名称和选项的cookie来保持用户会话。

总结

在这篇文章中,我们演示了如何将Next.js作为一个全栈应用程序来创建一个具有多个路由的用户界面,并创建API端点来与Strapi的API通信,以便注册和获取用户信息。还演示了在Next.js的服务器端设置和获取cookie,以保持用户会话。

The postImplementing user registration and authentication with Strapi and Next.jsappeared first onLogRocket Blog.