Skip to content

通过前面的努力,我们已经实现了一个基本且完整的前后端全栈博客项目。

那么接下来呢,我们对这个项目敲敲打打,看看哪些地方有隐患,哪些地方需要改善,哪些地方需要调整。

js
// 点击删除文章按钮
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)。

json
{
    "alg": "HS256",
    "typ": "JWT"
}

负载 (Payload)

包含声明 (claims),即关于实体 (通常是用户) 及其他数据的陈述。

json
{
    "sub": "1234567890",
    "name": "John Doe",
    "admin": true
}

签名 (Signature)

通过将编码后的头部、负载和一个密钥结合起来生成,用于验证消息在传输过程中未被更改。

text
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

JWT 的工作原理

  1. 用户通过身份验证后,服务器生成一个 JWT 并返回给用户。
  2. 用户在后续请求中将 JWT 作为凭证发送,通常放在 Authorization 头中。
  3. 服务器验证 JWT 的有效性,如果有效,则允许访问受保护的资源。

关于 JWT 的介绍大概就是这样,可能部分读者还是一脸懵逼,没关系,接下来我们动手实践。

为了实现 JWT 验证,我们将会用到 fast-jwt 这个 npm 依赖包。

那么我们需要在什么地方用到这个包呢?那就是在登录的时候,生成 jwt 签名,然后连同用户信息,一起发送给浏览器。

js
// /config.js 文件
export const config = {
    jwt: {
        key: 'coolApi666', // 密钥
        algorithm: 'HS256', // 算法
        expiresIn: '7d' // 过期时间
    }
};

先在根目录下创建一个 config.js 文件,内容如上,配置了 jwt 的密钥,算法和过期时间。

js
// /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

这是登录接口的返回效果。

token解密数据

我们把 token 复制,随便在浏览器搜索 jwt 在线解密,就能得到图中的数据。

可以看到,这一串字符串经过 jwt 解密后,就能得到我们的用户 id 和用户名。

那么我们试想一下,每次请求接口,把这串字符上传,后端解密后,是不是就能得到用户 ID 了呢?这样我们就不用手动上传作者的 id 了。

那么一个重要的问题来了,这个 token 字符串可以被篡改吗?别急,我们后面验证。

会员内容

会员隐藏内容,共 [3642] 字。全文阅读地址👉https://sourl.cn/NM5H5m

这样,我们就能保证,用户张三登录成功后,只能对其自己的文章进行添加、删除、修改,是无法通过修改自己的 token 来操作其他人的文章的。

这样我们就实现了,jwt 的登录和文章管理,也进一步让我们的整个系统更加安全。

何以解忧,唯有代码。不忘初心,方得始终。