通过前面的努力,我们已经实现了一个基本且完整的前后端全栈博客项目。
那么接下来呢,我们对这个项目敲敲打打,看看哪些地方有隐患,哪些地方需要改善,哪些地方需要调整。
// 点击删除文章按钮
document.querySelector('.panel').addEventListener('click', async (evt) => {
if (evt.target.className === 'del') {
const user = JSON.parse(localStorage.getItem('user'));
const id = Number(evt.target.dataset.id);
// 把拿到的数据变成JSON结构
const { code, data, msg } = await utilHttp('/api/article/delete', {
id: id,
author: user.id
});
utilShowMsg(msg);
if (code === 0) {
apiArticleSelect();
}
}
});
请看以上请求,有 2 个参数,一个是文章 id,一个是作者 id。
这里会有个问题,作者 id 是手动上传的,那么我们可以随意更改这个作者的 id,来伪造成其他作者发布和操作文章。
这样显然是不行的,我们可以用什么来解决这个问题呢?那就是 JWT
。
先来了解一下 JWT
相关信息:
JWT (JSON Web Token
) 是一种开放标准 (RFC 7519
),用于在各方之间以紧凑和自包含的方式安全地传输信息。
JWT 通常用于身份验证和授权。
JWT 由三部分组成,分别用点 (.
) 分隔:
头部 (Header)
通常包含两部分:令牌的类型 (JWT
) 和所使用的签名算法 (如 HMAC
,SHA256
或 RSA
)。
{
"alg": "HS256",
"typ": "JWT"
}
负载 (Payload)
包含声明 (claims
),即关于实体 (通常是用户
) 及其他数据的陈述。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
签名 (Signature)
通过将编码后的头部、负载和一个密钥结合起来生成,用于验证消息在传输过程中未被更改。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
JWT 的工作原理
- 用户通过身份验证后,服务器生成一个
JWT
并返回给用户。 - 用户在后续请求中将
JWT
作为凭证发送,通常放在Authorization
头中。 - 服务器验证
JWT
的有效性,如果有效,则允许访问受保护的资源。
关于 JWT
的介绍大概就是这样,可能部分读者还是一脸懵逼,没关系,接下来我们动手实践。
为了实现 JWT
验证,我们将会用到 fast-jwt
这个 npm
依赖包。
那么我们需要在什么地方用到这个包呢?那就是在登录的时候,生成 jwt
签名,然后连同用户信息,一起发送给浏览器。
// /config.js 文件
export const config = {
jwt: {
key: 'coolApi666', // 密钥
algorithm: 'HS256', // 算法
expiresIn: '7d' // 过期时间
}
};
先在根目录下创建一个 config.js
文件,内容如上,配置了 jwt
的密钥,算法和过期时间。
// /apis/admin/login.js 文件
import { createSigner } from 'fast-jwt';
import { mysqlPool } from '../../mysql.js';
import { ajvValid } from '../../utils.js';
import { config } from '../../config.js';
const signSync = createSigner(config.jwt);
export const apiSchema = {
// 元数据
meta: {
tags: ['用户'],
summary: '用户登录'
},
// 请求协议
request: {
type: 'object',
properties: {
username: {
type: 'string',
title: '用户名',
minLength: 2,
maxLength: 20
},
password: {
type: 'string',
title: '密码',
minLength: 6,
maxLength: 50
}
},
required: ['username', 'password'],
additionalProperties: false
},
// 返回协议
response: {
type: 'object',
properties: {
token: {
type: 'string',
description: '登录令牌'
}
}
}
};
// 接口逻辑
export default async (req) => {
try {
await ajvValid(apiSchema.request, req.body);
// ------------------------------------------------------
// 从连接池中获取一个数据库连接
const db = await mysqlPool.getConnection();
// 查询数据库是否有对应的用户数据
const [rows] = await db.query({
sql: 'SELECT * FROM `user` WHERE `username`=:username LIMIT 1',
values: {
username: req.body.username
}
});
// ------------------------------------------------------
// 如果查到了用户数据,说明该用户名已注册
if (rows.length <= 0) {
return {
code: 1,
msg: '用户未注册'
};
}
// ------------------------------------------------------
// 判断密码是否匹配
const [user] = rows; // rows是一个数组,我们这里用user变量去取数组的第一个值
if (user.password !== req.body.password) {
return {
code: 1,
msg: '密码错误'
};
}
const token = signSync({
id: user.id,
username: user.username
});
// 释放数据库连接
db.release();
// 返回成功信息
return {
code: 0,
msg: '登录成功',
token: token,
data: user
};
} catch (err) {
if (err?.from === 'ajv') {
return {
code: 1,
msg: err.data
};
} else {
return {
code: 1,
msg: '未知错误'
};
}
}
};
然后来到 /apis/admin/login.js
文件,我们导入 fast-jwt
中的 createSigner
,导入 config.js
文件的配置并创建 jwt 的生成函数 signSync
。
在返回用户信息之前,将用户的 id
和 username
通过 signSync
来进行加密,生成 token
,连同用户信息一起返回给客户端。
这是登录接口的返回效果。
我们把 token 复制,随便在浏览器搜索 jwt
在线解密,就能得到图中的数据。
可以看到,这一串字符串经过 jwt 解密后,就能得到我们的用户 id 和用户名。
那么我们试想一下,每次请求接口,把这串字符上传,后端解密后,是不是就能得到用户 ID 了呢?这样我们就不用手动上传作者的 id 了。
那么一个重要的问题来了,这个 token 字符串可以被篡改吗?别急,我们后面验证。
会员内容
会员隐藏内容,共 [3642] 字。全文阅读地址👉https://sourl.cn/NM5H5m
这样,我们就能保证,用户张三登录成功后,只能对其自己的文章进行添加、删除、修改,是无法通过修改自己的 token 来操作其他人的文章的。
这样我们就实现了,jwt 的登录和文章管理,也进一步让我们的整个系统更加安全。