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) 和所使用的签名算法 (如 HMACSHA256RSA)。

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

在返回用户信息之前,将用户的 idusername 通过 signSync 来进行加密,生成 token,连同用户信息一起返回给客户端。

登录返回token

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

token解密数据

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

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

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

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

会员内容

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

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

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

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