如何使用 Nextjs 构建一个简单的登录?(以及 React Hooks)
这篇文章绝不是Zeit的推荐,但还是要向那些家伙们致敬,因为他们正在构建的东西太棒了。简直是你能想象到的魔法和超能力(至少用 JavaScript 来说)。
我发现 Next.JS 框架非常简单易学。它的文档本身就很棒,他们甚至还提供了一个学习网站。请务必查看。
您可以在我的 repo 中查看完整代码:
https://github.com/mgranados/simple-login
最终产品使用了此登录名并进行了一些改进,您可以在这里找到它:Booktalk.io,这是一个分享书评的页面,其灵感主要来源于 Hacker News。我将在接下来的文章中提供更多关于如何创建更多功能以及完整项目的信息。如果您感兴趣,请关注我!
设置
你需要安装 Node +10 以及 yarn 或 npm。我个人更喜欢使用 yarn 或 npm yarn
,并且会在本教程中使用它们,不过 npm 也完全没问题。只是命令略有不同,仅此而已。
创建 Nextjs 应用
根据 Next.js 团队的建议,首选的方法是:
yarn create next-app
使用 Nextjs 进行本地开发
就是这样!你搞定了。现在你可以运行测试应用了
yarn dev
这应该会启动下一个开发版本并在您的 上公开一个开发版本http://localhost:3000/
。
让我们构建 API!🏗
现在,为了开始在 NextJS 9.2 上构建 API,您可以添加一个文件夹/pages/api
,然后在其中构建的所有内容都将在Now等产品中进行生产构建时作为无服务器函数公开。这有多神奇啊!
这里相当有趣的是,您可以使用 ES6 和类似的东西,import
而不是像require
在 NodeJS 文件中那样使用CommonJS
让我们构建登录的相关端点:
POST /users
根据 REST 原则,这是创建user
资源的首选方式。这可以理解为:创建用户。也就是注册时发生的操作。POST /auth
这是我个人的偏好,用于命名用户登录时前端所访问的端点。GET /me
如果正确登录,这也是端点的个人偏好,端点将会受到打击并检索用户数据。
让我们开始吧
POST /用户
该文件的第一部分用于导入相关库并创建与数据库的连接。
/pages/api/users.js
const MongoClient = require('mongodb').MongoClient;
const assert = require('assert');
const bcrypt = require('bcrypt');
const v4 = require('uuid').v4;
const jwt = require('jsonwebtoken');
const jwtSecret = 'SUPERSECRETE20220';
const saltRounds = 10;
const url = 'mongodb://localhost:27017';
const dbName = 'simple-login-db';
const client = new MongoClient(url, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
MongoClient
显然是用来连接 MongoDB 并存储 API 需要的数据的。我喜欢用这个assert
模块作为请求体和端点所需数据的简单验证器。bcrypt
它可以用来哈希和验证密码,而无需实际将其存储为纯文本。(千万不要这么做)
该v4
函数是一种为用户创建随机 ID 的好方法,最终jwt
可以创建一个从前端安全并在后端验证的良好会话。
我强烈建议将其存储jwtSecret
为来自 a 的代码.env
,因为将其存储为提交给 github 或 gitlab 的代码的一部分是一个非常糟糕的主意,因为它会被公开。
最后,您需要设置 dbName 和 mongo 客户端以连接到数据库并从那里进行写入和读取。
操作数据库(获取用户并创建新用户)
function findUser(db, email, callback) {
const collection = db.collection('user');
collection.findOne({email}, callback);
}
function createUser(db, email, password, callback) {
const collection = db.collection('user');
bcrypt.hash(password, saltRounds, function(err, hash) {
// Store hash in your password DB.
collection.insertOne(
{
userId: v4(),
email,
password: hash,
},
function(err, userCreated) {
assert.equal(err, null);
callback(userCreated);
},
);
});
}
findUser
这是一个通过电子邮件发送的简单函数,它基本上包装了该collection.findOne()
函数并通过电子邮件进行查询并传递回调。
该createUser
函数更有趣一些,因为首先需要输入密码hashed
,然后insertOne()
使用散列密码而不是纯文本版本。
实际上将处理 api 请求的其余代码,NextJS 如下:
export default (req, res) => {
if (req.method === 'POST') {
// signup
try {
assert.notEqual(null, req.body.email, 'Email required');
assert.notEqual(null, req.body.password, 'Password required');
} catch (bodyError) {
res.status(403).json({error: true, message: bodyError.message});
}
// verify email does not exist already
client.connect(function(err) {
assert.equal(null, err);
console.log('Connected to MongoDB server =>');
const db = client.db(dbName);
const email = req.body.email;
const password = req.body.password;
findUser(db, email, function(err, user) {
if (err) {
res.status(500).json({error: true, message: 'Error finding User'});
return;
}
if (!user) {
// proceed to Create
createUser(db, email, password, function(creationResult) {
if (creationResult.ops.length === 1) {
const user = creationResult.ops[0];
const token = jwt.sign(
{userId: user.userId, email: user.email},
jwtSecret,
{
expiresIn: 3000, //50 minutes
},
);
res.status(200).json({token});
return;
}
});
} else {
// User exists
res.status(403).json({error: true, message: 'Email exists'});
return;
}
});
});
}
};
export default (req, res) => {}
神奇的事情就在这里发生,你可以像在 Express 应用中一样获取 req 和 res。如果你只想处理POST
发往端点的请求,那么这里唯一需要设置的事情之一就是:
if (req.method === 'POST') { }
其他 HTTP 方法可以通过附加条件进行处理。
该代码基本上验证请求主体是否有电子邮件和密码,否则就没有足够的用户信息来尝试创建。
try {
assert.notEqual(null, req.body.email, 'Email required');
assert.notEqual(null, req.body.password, 'Password required');
} catch (bodyError) {
res.status(403).json({error: true, message: bodyError.message});
}
基本上,我们验证了该邮箱地址是否存在用户,如果存在,我们会抛出错误,因为这样就没有必要再创建第二个用户了!至少应该在一个字段上强制执行唯一性,而邮箱地址非常适合这种情况。
findUser(db, email, function(err, user) {
if (err) {
res.status(500).json({error: true, message: 'Error finding User'});
return;
}
最后,如果没有该电子邮件的用户,我们就可以安全地继续创建它。
createUser(db, email, password, function(creationResult) {
if (creationResult.ops.length === 1) {
const user = creationResult.ops[0];
const token = jwt.sign(
{userId: user.userId, email: user.email},
jwtSecret,
{
expiresIn: 3000, //50 minutes
},
);
res.status(200).json({token});
return;
}
});
这里发生的另一件相关的事情是 JWT 签名正在进行。JWT 的详细信息可以在这里找到。如果一切顺利,我们会创建一个包含用户 ID 和电子邮件的令牌,并设置一段时间(在本例中为 50 分钟),然后将其作为响应发送。
我们将看看如何在前端处理这个问题。
让我们添加/pages
🎨
让我们构建一个index.js
始终显示一些内容的程序,以防访问者没有登录或账户。此外,我们还要添加一些逻辑,以便用户注册并登录后,能够看到略有不同的页面。
还添加login.js
和signup.js
/pages/signup
注册页面最相关的部分必须是提交功能,该功能在用户单击提交按钮时处理对 API 的请求。
function handleSubmit(e) {
e.preventDefault();
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
password,
}),
})
.then((r) => r.json())
.then((data) => {
if (data && data.error) {
setSignupError(data.message);
}
if (data && data.token) {
//set cookie
cookie.set('token', data.token, {expires: 2});
Router.push('/');
}
});
}
e.preventDefault()
阻止提交遵循标准程序并基本上重定向页面。
然后,API 的调用就随之发生fetch('/api/users')
。我们将主体以 JSON 格式发送,需要注意的是,这些值是从输入的 onChange 钩子中获取的。
其中最有趣的部分是
if (data && data.error) {
setSignupError(data.message);
}
if (data && data.token) {
//set cookie
cookie.set('token', data.token, {expires: 2});
Router.push('/');
}
我们使用该import cookie from 'js-cookie'
库根据获取到的 token 设置 cookie,并将其有效期设置为几天。这其中存在一些矛盾,或许最好将其设置为 1 天,而 JWT 的有效期则设置为更短一些。
设置 cookie 后,每当我们发出其他请求时,该 cookie 也会被发送到服务器,在那里我们可以解密并检查用户是否经过正确身份验证以及身份验证是否有效。
POST /授权
此端点与注册端点非常相似,主要区别和最有趣的部分是 Auth 方法,它基本上比较正文中输入的纯文本密码,并返回是否与存储在用户集合中的哈希匹配。
function authUser(db, email, password, hash, callback) {
const collection = db.collection('user');
bcrypt.compare(password, hash, callback);
}
我们不会创建用户,而是验证输入的信息是否与现有用户匹配,并返回相同的 jwt 令牌
if (match) {
const token = jwt.sign(
{userId: user.userId, email: user.email},
jwtSecret,
{
expiresIn: 3000, //50 minutes
},
);
res.status(200).json({token});
return;
}
/pages/login
登录页面的表单基本与此相同,只是signup.js
文本有所不同。这里我想稍微谈谈所使用的钩子。
const Login = () => {
const [loginError, setLoginError] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
//...
return (
<input
name="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
)
}
这里你可以看到 React Hook 的基本用法。你可以将定义的变量状态存储在组件顶部,并使用伴随函数进行设置。
每当有人更改电子邮件时onChange={(e) => setEmail(e.target.value)}
,都会启动并设置值并使其可通过所有组件使用。
POST /我
const jwt = require('jsonwebtoken');
const jwtSecret = 'SUPERSECRETE20220';
export default (req, res) => {
if (req.method === 'GET') {
if (!('token' in req.cookies)) {
res.status(401).json({message: 'Unable to auth'});
return;
}
let decoded;
const token = req.cookies.token;
if (token) {
try {
decoded = jwt.verify(token, jwtSecret);
} catch (e) {
console.error(e);
}
}
if (decoded) {
res.json(decoded);
return;
} else {
res.status(401).json({message: 'Unable to auth'});
}
}
};
这个端点非常简单,但功能非常强大。每当有人拨打电话时api/me
,服务器都会token
在(由Nextjs 中间件req.cookies
神奇地管理的)中查找密钥。如果该令牌存在并传递,则表示用户已通过有效身份验证,并返回解码后的信息(用户 ID 和电子邮件,还记得吗?),并告诉前端继续,否则返回 401 。jwt.verify
Unauthorized
/pages/index
现在让我们保护索引页的一部分,以便在您获得授权时进行更改。这样会有一些不同,您可以看到 Cookie 和api/me
端点的全部功能。
检查授权时会发生什么:
const {data, revalidate} = useSWR('/api/me', async function(args) {
const res = await fetch(args);
return res.json();
});
if (!data) return <h1>Loading...</h1>;
let loggedIn = false;
if (data.email) {
loggedIn = true;
}
我们调用api/me
端点(使用同样由 zeit 团队开发的优秀库 useSWR),如果响应为 ,data.email
我们就让变量loggedIn
等于,然后在渲染中显示已登录用户的邮箱地址,以及一个“注销”按钮!(其实就是从 cookie 中true
移除,是不是很简单!)token
{loggedIn && (
<>
<p>Welcome {data.email}!</p>
<button
onClick={() => {
cookie.remove('token');
revalidate();
}}>
Logout
</button>
</>
)}
{!loggedIn && (
<>
<Link href="/login">Login</Link>
<p>or</p>
<Link href="/signup">Sign Up</Link>
</>
)}
页面组件的完整代码:
import Head from 'next/head';
import fetch from 'isomorphic-unfetch';
import useSWR from 'swr';
import Link from 'next/link';
import cookie from 'js-cookie';
function Home() {
const {data, revalidate} = useSWR('/api/me', async function(args) {
const res = await fetch(args);
return res.json();
});
if (!data) return <h1>Loading...</h1>;
let loggedIn = false;
if (data.email) {
loggedIn = true;
}
return (
<div>
<Head>
<title>Welcome to landing page</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<h1>Simplest login</h1>
<h2>Proudly using Next.js, Mongodb and deployed with Now</h2>
{loggedIn && (
<>
<p>Welcome {data.email}!</p>
<button
onClick={() => {
cookie.remove('token');
revalidate();
}}>
Logout
</button>
</>
)}
{!loggedIn && (
<>
<Link href="/login">Login</Link>
<p>or</p>
<Link href="/signup">Sign Up</Link>
</>
)}
</div>
);
}
export default Home;
请记住,整个代码都可以在这里找到:
https://github.com/mgranados/simple-login供您查看!
就这样!感谢你读到这里!希望你已经掌握了如何使用 Next.JS 构建 API 和页面,也希望你能够有动力去构建自己的内容。
如果你喜欢或者有疑问,我可以帮你解决一些 JS 相关的问题,请在 Twitter 上联系我!@martingranadosg我很想知道你能用它做什么!:) 或者dev.to
也可以在这里联系我 😁