固体启动验证 - 安全的方式(使用BCRYPT和PSQL)
#javascript #网络开发人员 #教程 #solidjs

这是关于什么?

本文涉及新的Meta框架可靠的启动和身份验证,主要是使其成为安全的应用程序。我已经写了一篇有关how to store data in a PostgreSQL database with Solid Start的文章。本教程以这些知识为基础,因此,如果您还没有阅读该教程,我建议您这样做。

入门

注意:如果模板以任何方式更改,我可能会建议您阅读较新的教程,或者从this代码开始。如果模板保持不变,请继续设置。
首先,我们需要创建一个将在其中进行设置的文件夹。我们可以使用mkdir secure-auth-with-solid-start做到这一点,然后直接使用cd secure-auth-with-solid-start进入该文件夹。为了初始化我们的项目,我们键入npm init solid,这将我们带入了设置向导。要有一个基础,我们选择“用auth”模板:

✔ Which template do you want to use? › with-auth
✔ Server Side Rendering? … yes
✔ Use TypeScript? … yes

接下来,我们必须通过键入yarn安装包裹。您也可以使用npm install,但我已经习惯了纱。

编写应用程序

在这一部分中,我们实际上将制定我们的应用程序并正确保护它。我已经有一篇有关如何正确保护密码的文章:"how (not) to store passwords"

让我们哈希! (下一章使用bcrypt)

重要:本章通过手动进行,因此读者可以更好地了解正在发生的事情。如果您在这里参加BCRypt部分,请跳过本章。您显然也可以阅读两者。
为了首先,我们需要找到负责登录和注册的功能。我们可以在文件中找到该数据库(src/db/index.ts)。
首先,我们将介绍创建功能:

async create({ data }) {
  let user = { ...data, id: users.length };
  users.push(user);
  return user;
},

要检查用户寄存器确切地拥有的数据,我们可以console.log(user)
Screenshot of console displaying that the server returns us a username and a password
请注意,您需要重新启动服务器以查看这些更改。
我们还需要什么?好吧,我们还需要盐。这是一种可能的方法:

async create({ data }) {
  let salt = Math.random().toString(32).slice(2);
  //...
}

接下来我们需要消化。与Ruby不同,JavaScript没有本机SHA256功能,这就是为什么我们将使用crypto-js。我添加了yarn add crypto-js
快速查看加密JS的文档表明它实际上是多么容易:

import sha256 from 'crypto-js/sha256';

const hashDigest = sha256(nonce + message);

我们将准确地将此代码实现到我们的/db/index.ts文件中:

import sha256 from "crypto-js/sha256";

export const db = {
  user: {
    async create({ data }) {
      //...
      const digestedPassword = sha256(data.password + salt)
                                  .toString();
      console.log('our digested password:', encryptedPassword);
      //...
    },
    //...
  },
};

,当我们注册新用户时,这应该给我们带来很长的随机字符串:
Screenshot of a console log showing a digested password
最后,我们需要修改用户,我们将保存并在下面打印它:

let user = {
  username: data.username,
  id: users.length,
  salt: salt,
  digested_password: digestedPassword
}
console.log(user)

现在在下一行(users.push(user))上,我们应该获得打字稿错误。那是因为我们的初始用户具有不同的属性,因此不要害怕。我们将稍后再解决。
我们仍然缺少的最后一件事是胡椒,所以我们将创建那个。我刚刚在数组的let users初始化上方制作了一个export const

export const pepper: string = "make_sure_the_pepper_is_long"
                        + "_and_secure_so_that_it_is"
                        + "_hard_to_guess";
let users = [{ id: 0, username: "kody", password: "twixrox" }];

我们导出它,因为我们将需要此胡椒 最后,还添加到sha256算法:

const digestedPassword = sha256(data.password + salt + pepper)
                                  .toString();

接下来,我们将更改登录功能以匹配我们与寄存器的系统。登录位于/src/db/session.ts中,这就是为什么我们出口胡椒。现在,如果我们进入session.ts,我们会在第三行中看到:

import { db } from ".";

我们更改为导入pepper,不要忘记在下一行中导入SHA265算法:

import { db, pepper } from ".";
import sha256 from "crypto-js/sha256";

最后,我们可以重写我们的登录功能。它的作用是消化用户输入,然后将其与数据库中的密码匹配,因此我们永远不必将纯文本密码返回(在session.ts的第15行):

export async function login({ username, password }: LoginForm) {
  const user = await db.user.findUnique({ where: { username } });
  if (!user) return null;
  const digestedInput = sha256(password
                              + user.salt
                              + pepper)
                                .toString();
  const isCorrectPassword = digestedInput === user.digested_password;
  if (!isCorrectPassword) return null;
  return user;
}

在这里您会看到另一个打字稿错误,但是我们稍后会清除这些错误。现在,我们可以使用密码“ 123456”创建一个新用户,并检查其属性的日志(请确保重新启动服务器):
Screenshot of logs displaying a user with a salt and an encrypted password
我们可以将这些属性复制到创建新的基本用户,该用户可与我们的消化一起使用。为此,我们更改/src/db/index.ts/中的users数组:

let users = [{
  id: 0,
  username: "@aneshodza",
  salt: "qm9616pd3eg",
  digested_password: "a5c594cb0938b5d118f0c4d0e4fbf4a64838c2390da1334a85cef73955008fd1"
}]

现在,如果我们重新启动服务器并尝试登录我们的基本用户,我们应该快速看到:
Screenshot of a logged in user
它有效!
接下来,我们想使用BCRypt进行此操作,但是如果您想拥有源代码,则可以得到它here

现在与bcrypt

我们想将我们的网站纳入行业标准,这就是为什么我们将使用BCrypt(可能是最常用的加密和消化图书馆)。
第一步是再次陷入“设置”章节,因为我们将在单独的应用程序中执行此操作。
现在,我们想使用BCRYPT。如果您没有阅读第一部分:寄存器函数在/src/db/index.ts中,登录位于src/db/session.ts中。
让我们从安装包装开始。我使用纱线,所以我将其与yarn add bcrypt安装。
我们将与文档并行合作,因此我建议您在另一个选项卡上保持this打开。
我们从寄存器所在的/src/db/index.ts文件开始。第一步是require bcrypt对象,并为盐弹设置一个const

import bcrypt from 'bcrypt';
const saltRounds = 10;

盐弹是消化的“成本因素”。这告诉我们字符串被消化的频率,因此这个数字越大,蛮力也越难,甚至创建了哈希。有10轮绝对应该足够。
接下来,我们要使用消化密码创建用户。 BCrypt已经为我们提供了一种方法,我们将在创建功能的顶部使用它:

    create({ data }) {
      const user = {
        id: users.length,
        username: data.username,
        digested_password: bcrypt.hashSync(data.password, saltRounds),
      };
      console.log('user', user);
      users.push(user);
      return user;
    },

您应该看到打字稿错误。为了解决该问题,只需删除应用程序给我们的第一个用户即可。
现在我们仍然缺少胡椒。使用BCrypt并不是真正的必要,但是要保持良好的实践,我们仍然会使用它。为此,我们只是创建一个const并将其附加到我们的初始字符串:

export const pepper:string = "this_is_some_really_secure_pepper";
export const db = {
  //...
  async create({ data }) {
    const user = {
      //...
      digested_password: bcrypt.hashSync(data.password + pepper, saltRounds),
    }
    //... 
  },
  //...
};

我们导出胡椒粉,因为我们以后将需要它。
现在,如果您记录了创建的用户,您将看到一个消化的密码:
Image of our newly created user with a digested password
用额外的胡椒粉固定后,我们最终可以进行登录。为此,我们进入/src/db/session.ts中的login功能。首先,我们像在另一个文件中一样在顶部导入bcrypt,因此我们可以使用预设的bcrypt函数为我们做到这一点:

export async function login({ username, password }: LoginForm) {
  const user = await db.user.findUnique({ where: { username } });
  if (!user) return null;
  let result = bcrypt.compareSync(password + pepper, user.digested_password);
  if (!result) return null;
  return user;
}

现在您仍然应该看到一个错误,因为我们还没有导入胡椒。您导入db的位置,您只需在其旁边添加pepper即可起作用。

我们的盐在哪里?

bcrypt有自己的盐系统,在那里他们有一个盐,包括一根绳子的盐弹。接下来,他们还编码其中的许多其他信息,因此复杂库由单个字符串工作:

$2y$10$nOUIs5kJ7naTuTFkBy1veuK0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa
 |  |  |                     |
 |  |  |                     hash-value = K0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa
 |  |  |
 |  |  salt = nOUIs5kJ7naTuTFkBy1veu
 |  |
 |  cost-factor = 10 = 2^10 iterations
 |
 hash-algorithm = 2y = BCrypt

解决VITE问题

如果您检查控制台,您应该会看到很多红色:
Vite error messages caused by the BCrypt library
我们可以通过进入我们的/vite.config.js并告诉它不要优化此库来解决这个问题:

import solid from "solid-start/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [solid()],
  optimizeDeps: {
    exclude: ['bcrypt']
  }
});

现在,您的控制台应无错误,代码应运行:
Shows user @aneshodza being logged in

创建数据库连接

这是您应该阅读my first article的部分。我在那里解释了这是如何深入工作的。

配置

我们将使用PostgreSQL npm package。因为我使用纱线,所以我将使用yarn add postgres安装它。如果您使用任何其他软件包管理器,请使用其中需要的命令。

创建模式

现在我们需要创建我们的数据库和表。我们将首先使用psql -U <username> -d postgres连接到Postgres-CLI,这将使我们进入Postgres数据库。在那里,我们创建了一个具有之前使用属性的数据库和一个“ application_user”表:

postgres=# CREATE DATABASE solid_start_auth_made_secure;
CREATE DATABASE

postgres=# \c solid_start_auth_made_secure
You are now connected to database "solid_start_auth_made_secure".

solid_start_auth_made_secure=# CREATE TABLE application_users (
  id serial primary key,
  username varchar(255),
  digested_password varchar(255)
);
CREATE TABLE

现在,如果我们在桌子上打印所有内容,我们应该看到以下内容:

solid_start_auth_made_secure=# SELECT * FROM application_users;

 id | username | digested_password
----+----------+-------------------

(0 rows)

将客户端连接到我们的数据库

接下来,我们必须将数据库连接到后端。我们将使用在"Solid Start with PostgreSQL"中使用的相同库,因此我们必须再次使用yarn add postgres
安装它 然后我们进入src/db/index.ts,在其中创建一个对象,该对象包含文件顶部的数据库连接:

import postgres from "postgres";

const sql = postgres({
  host: "localhost",
  port: 5432,
  database: "solid_start_auth_made_secure",
  username: "<USERNAME>"
});

现在,您应该在终端中看到一些Big integer literals错误。该错误是由Vite引起的。我们通过告诉vite.config.ts文件以不优化以下方法来修复它:

import solid from "solid-start/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [solid()],
  optimizeDeps: {
    exclude: ['bcrypt', 'postgres'],
  }
});

重新启动服务器和voilã:错误已消失。

查询数据库

首先,我们将用户存储在数据库中。为此,我们进入/src/db/index.ts,在那里我们使用NPM库将其更改为存储在数据库中的用户:

async create({ data }) {
  return await sql`INSERT INTO application_users (username, digested_password) 
      VALUES (${data.username}, ${bcrypt.hashSync(data.password + pepper, saltRounds).toString()})
      RETURNING *;`;
},

现在,如果我们注册用户并查询我们的数据库:

solid_start_auth_made_secure=# SELECT * FROM application_users;

 id |  username  |                      digested_password
----+------------+--------------------------------------------------------------
  4 | @aneshodza | $2b$10$.IyUhM832d24cD.uWuQrUubuQLkGQWw76Ot5C/r0XGniS666L0hvO

(1 row)

现在,我们还需要在同一文件的内部重写我们的findUnique函数,以便为用户搜索DB:

async findUnique({ where: { username = undefined, id = undefined } }) {
  if (id !== undefined && id.toString() !== 'NaN') {
    // return users.find((user) => user.id === id);
    const result = await sql`SELECT * FROM application_users WHERE id = ${id};`;
    return result.at(0);
  } else if (username !== undefined) {
    // return users.find((user) => user.username === username);
    const result = await sql`SELECT * FROM application_users WHERE username = ${username} LIMIT 1;`;
    return result.at(0);
  }
  return null;
},

这将返回用户。现在,如果我们尝试登录应用程序:
Image of logged in user
它有效!
如果您想要此方法的源代码,here you go

结论

我会说和我在第一教程中一样。尽管稳定的开始有一个可靠的开端;),但它仍处于非常早期的发展状态,即使是坚实的开始团队也同意:不要在生产软件中使用它。我认为,有更多的本地支持,例如数据库我们可以做更多的事情。快乐黑客:)