iT邦幫忙

2023 iThome 鐵人賽

0
自我挑戰組

用ChatGPT詠唱來完成工作與點亮前後端技能樹系列 第 49

【Day49】ChatGPT請教教我:NestJS!(八)- SQL & Knex!SQL注入攻擊!?

  • 分享至 

  • xImage
  •  

這個章節要來討論另一個有關SQL連接的套件:Knex

有關MySQL基礎語法以及架設本地MySQL的服務請看
【Day38】ChatGPT請教教我:資料庫!MySQL!MySQL Workbench!
【Day39】ChatGPT請教教我:基礎SQL語法!NodeJS連結SQL!
【Day40】ChatGPT請教教我:進階SQL語法!
這幾篇

這邊要討論的是NodeJS、NestJS可以用來做資料庫查詢的工具: Kenx
有關Knex,直接請ChatGPT上介紹

Knex介紹

https://ithelp.ithome.com.tw/upload/images/20231102/20135750dBxP5Oi000.png

好的,讓我來為你詳細介紹一下 Knex。

關於 Knex

Knex 是一個 Node.js 的資料庫查詢構建器(query builder),它可以幫助你以 JavaScript 的方式來操作資料庫。透過 Knex,你可以使用簡單的語法來進行資料庫操作,而不必直接寫 SQL 語句。

特色

  1. 跨平台支援: Knex 支援多種主流資料庫(如 PostgreSQL、MySQL、SQLite、Oracle 等),這使得你可以在不同的專案中輕鬆切換資料庫系統。
  2. 語法糖: Knex 提供了一套簡潔的 API 來建構複雜的查詢,這使得你可以以更清晰、易讀的方式來編寫資料庫操作。
  3. 遷移(Migrations): Knex 內建了遷移管理功能,這使得你可以輕鬆地管理資料庫結構的變化,並且保持資料庫版本的一致性。
  4. Promise 支援: Knex 會返回 Promises,這使得你可以使用非同步的方式進行資料庫操作,這對於 Node.js 的事件驅動模型非常適合。
  5. 模型(Model): 雖然 Knex 本身並不提供像是 ORM(物件-關聯映射) 那樣的模型,但你可以使用它來建構你自己的模型層。

優缺點

優點:

  1. 易學易用: 尤其對於初學者來說,Knex 提供了一個相對簡單的介面,讓你可以快速上手。
  2. 支援多種資料庫: 可以使用相同的程式碼來操作不同的資料庫,這大大提升了開發效率。
  3. 遷移管理: 內建的遷移功能使得資料庫結構的管理變得更為方便,尤其在團隊開發中。

缺點:

  1. 複雜查詢可能較為繁瑣: 對於一些複雜的資料庫操作,可能需要較多的程式碼來描述,相較於直接使用 SQL 語句,可能會顯得繁瑣一些。
  2. 需要學習特定的 API: 需要花一些時間來學習 Knex 的 API,特別是對於初學者來說。

總的來說,Knex 是一個功能豐富且強大的資料庫查詢構建器,特別適合在 Node.js 環境下進行資料庫操作。它的跨平台支援和遷移管理功能使得它成為許多開發者的首選。當然,也要注意到在處理複雜查詢時可能會相對繁瑣一些,但這在大多數情況下都可以透過良好的程式設計來化解。

安裝 & 設置 & 示範

https://ithelp.ithome.com.tw/upload/images/20231102/20135750qhkQrXNekK.png

當你已經建立了 NestJS 專案並希望連接 MySQL 資料庫以及使用 Knex 進行查詢時,以下是一個詳細的步驟教學。請確保你已經安裝了 NestJS,並且有一個運行的 NestJS 專案。

  1. 安裝必要套件:

    首先,你需要安裝必要的套件,包括 knexmysql2(MySQL 驅動程式)以及 @nestjs/knex(用於 NestJS 集成 Knex)。

    npm install knex mysql2 @nestjs/knex
    
  2. 建立 Knex 設定檔:

    在你的 NestJS 專案中,創建一個名為 knexfile.js 的設定檔。這個設定檔將包含資料庫的連接設定,如下:

    module.exports = {
      client: 'mysql2', // 使用 MySQL
      connection: {
        host: 'your-host',
        user: 'your-username',
        password: 'your-password',
        database: 'your-database',
      },
      pool: { min: 2, max: 10 }, // 資料庫連接池設定
    };
    

    請將 'your-host''your-username''your-password''your-database' 替換為你的 MySQL 連接設定。

  3. 建立一個 Knex 服務:

    在你的 NestJS 專案中,建立一個名為 knex.service.ts 的服務,用於初始化 Knex 連接。

    import { Injectable } from '@nestjs/common';
    import * as knex from 'knex';
    import knexfile from './knexfile'; // 引入 Knex 設定檔
    
    @Injectable()
    export class KnexService {
      readonly db = knex(knexfile);
    }
    
  4. 將 KnexService 注入到模組:

    在你的 NestJS 模組中,將 KnexService 注入到 providers 陣列中,以便在其他地方使用。

    import { Module } from '@nestjs/common';
    import { KnexService } from './knex.service';
    
    @Module({
      providers: [KnexService],
      exports: [KnexService], // 將 KnexService 匯出以供其他模組使用
    })
    export class KnexModule {}
    
  5. 建立一個服務來執行查詢:

    在你的 NestJS 專案中,建立一個服務來執行資料庫查詢。在這個服務中,你可以使用 KnexService 進行資料庫查詢操作。

    import { Injectable } from '@nestjs/common';
    import { KnexService } from './knex.service';
    
    @Injectable()
    export class YourService {
      constructor(private readonly knexService: KnexService) {}
    
      async queryData() {
        // 使用 Knex 進行查詢
        const result = await this.knexService.db('your_table_name').select('*');
    
        return result;
      }
    }
    

    請將 'your_table_name' 替換為你要查詢的資料表名稱。

  6. 使用你的服務:

    現在,你可以在你的 NestJS 控制器或其他地方使用 YourService 來執行資料庫查詢。

    import { Controller, Get } from '@nestjs/common';
    import { YourService } from './your.service';
    
    @Controller('your')
    export class YourController {
      constructor(private readonly yourService: YourService) {}
    
      @Get('data')
      async getData() {
        const data = await this.yourService.queryData();
        return data;
      }
    }
    
  7. 啟動你的 NestJS 應用:

    最後,啟動你的 NestJS 應用以查看結果。

    npm run start
    

現在,你已經成功地將 Knex 整合到你的 NestJS 專案中,並可以使用它來執行 MySQL 資料庫查詢。請記得在實際專案中使用真實的資料庫連接資訊並根據需求自訂你的查詢操作。

這上面有個小問題,knexfile.js 使用的不是ES模組的寫法
NestJS在編譯解讀的時候似乎不會把原本就是.js的檔案搬到dist中處理

請ChatGPT把他轉換一下寫法,還有把module注入的程式碼也一起補上
https://ithelp.ithome.com.tw/upload/images/20231102/20135750qpYLIHhrkb.png

好的,讓我來幫你完善程式碼:

首先,我們將 knexfile.js 轉換成 knexfile.ts

const config = {
  client: 'mysql2',
  connection: {
    host: 'your-host',
    user: 'your-username',
    password: 'your-password',
    database: 'your-database',
  },
  pool: { min: 2, max: 10 },
};

export default config;

請將 'your-host''your-username''your-password''your-database' 替換為你的 MySQL 連接設定。

接著,我們來補上 KnexModule 注入到 YourModule 的程式碼:

在你的 your.module.ts 檔案中:

import { Module } from '@nestjs/common';
import { YourController } from './your.controller';
import { YourService } from './your.service';
import { KnexModule } from './knex.module'; // 引入 KnexModule

@Module({
  imports: [KnexModule], // 將 KnexModule 匯入
  controllers: [YourController],
  providers: [YourService],
})
export class YourModule {}

這樣,你的 YourModule 就能夠使用 KnexModule 提供的服務了。

記得在實際專案中,確保你的目錄結構和引入路徑正確。如果有任何其他問題或需要進一步的協助,請隨時告訴我!

這邊延續著 【Day40】ChatGPT請教教我:進階SQL語法! 其中一段的內容
我們有一個叫做 mydatabase 的DB ,並且有一個表格customers

-- 創建 customers 表格
CREATE TABLE customers (
    customer_id INT PRIMARY KEY,
    name VARCHAR(50)
);

-- 插入一些示例數據
INSERT INTO customers (customer_id, name) VALUES
(1, 'Alice'),
(2, 'Bob'),
(3, 'Carol');

資料庫中就有這些資料
https://ithelp.ithome.com.tw/upload/images/20231102/20135750uoqe6YD6p2.png

然後改一下service中的queryData 中的table名稱,把表格名字換成 customers

async queryData() {
    const result = await this.knexService.db('customers').select('*');
    return result;
}

再搭配 swagger來看看執行結果
https://ithelp.ithome.com.tw/upload/images/20231102/20135750rKUG5Yrmku.png
真的把資料庫中的資料給撈出來了!

Knex語法教學

https://ithelp.ithome.com.tw/upload/images/20231102/20135750guFhtdDdRs.png

當然,讓我來為你詳細解釋 Knex 的一些常用查詢語法,並提供原始的 SQL 語句作為對照。

1. 選擇(Select)

Knex 查詢語法:

const result = await this.knexService.db('customers').select('*');

原始 SQL 語句:

SELECT * FROM customers;

這個語句將從 customers 表格中選擇所有的列。

2. 條件選擇(Select with Where)

Knex 查詢語法:

const result = await this.knexService.db('customers').where({ name: 'Alice' }).select('*');

原始 SQL 語句:

SELECT * FROM customers WHERE name = 'Alice';

這個語句將從 customers 表格中選擇所有名為 'Alice' 的行。

3. 新增(Insert)

Knex 查詢語法:

await this.knexService.db('customers').insert({ customer_id: 4, name: 'David' });

原始 SQL 語句:

INSERT INTO customers (customer_id, name) VALUES (4, 'David');

這個語句將在 customers 表格中插入一個新的行。

4. 更新(Update)

Knex 查詢語法:

await this.knexService.db('customers').where({ customer_id: 1 }).update({ name: 'Alex' });

原始 SQL 語句:

UPDATE customers SET name = 'Alex' WHERE customer_id = 1;

這個語句將更新 customers 表格中 customer_id 為 1 的行的名字為 'Alex'。

5. 刪除(Delete)

Knex 查詢語法:

await this.knexService.db('customers').where({ customer_id: 2 }).del();

原始 SQL 語句:

DELETE FROM customers WHERE customer_id = 2;

這個語句將刪除 customers 表格中 customer_id 為 2 的行。

6. 聚合函數(Aggregation)

Knex 查詢語法:

const result = await this.knexService.db('customers').count('customer_id as count');

原始 SQL 語句:

SELECT COUNT(customer_id) as count FROM customers;

這個語句將計算 customers 表格中的行數。

7. 連接(Join)

Knex 查詢語法:

const result = await this.knexService.db('customers')
  .join('orders', 'customers.customer_id', '=', 'orders.customer_id')
  .select('customers.name', 'orders.order_id');

原始 SQL 語句:

SELECT customers.name, orders.order_id
FROM customers
JOIN orders ON customers.customer_id = orders.customer_id;

這個語句將連接 customersorders 表格,並選擇 customers.nameorders.order_id

這是一些常用的 Knex 查詢語法,它們提供了一種更簡潔且易讀的方式來編寫資料庫操作,相對於直接使用 SQL 語句,這使得程式碼更具可維護性和可讀性。如果你有任何其他問題或需要進一步的協助,請隨時告訴我!

繼續看更多範例
https://ithelp.ithome.com.tw/upload/images/20231102/20135750Z4EFQGGmtN.png

當然,還有許多其他的 Knex 查詢語法可以使用。以下是一些常用的 Knex 查詢語法:

8. 限制(Limit)

Knex 查詢語法:

const result = await this.knexService.db('customers').select('*').limit(5);

原始 SQL 語句:

SELECT * FROM customers LIMIT 5;

這個語句將返回 customers 表格中的前 5 筆資料。

9. 排序(Order By)

Knex 查詢語法:

const result = await this.knexService.db('customers').select('*').orderBy('name', 'asc');

原始 SQL 語句:

SELECT * FROM customers ORDER BY name ASC;

這個語句將按照 name 欄位的升序(asc)排列返回的資料。

10. 分頁(Offset & Limit)

Knex 查詢語法:

const result = await this.knexService.db('customers').select('*').offset(5).limit(5);

原始 SQL 語句:

SELECT * FROM customers LIMIT 5 OFFSET 5;

這個語句將返回 customers 表格中的第 6 到 10 筆資料。

11. 子查詢(Subquery)

Knex 查詢語法:

const subquery = this.knexService.db('orders').where('total_amount', '>', 100);
const result = await this.knexService.db('customers').whereExists(subquery).select('*');

原始 SQL 語句:

SELECT * FROM customers WHERE EXISTS (SELECT * FROM orders WHERE total_amount > 100);

這個語句將選擇所有在 orders 表格中總金額超過 100 的顧客。

12. 聯集(Union)

Knex 查詢語法:

const query1 = this.knexService.db('customers').where('name', 'Alice').select('*');
const query2 = this.knexService.db('customers').where('name', 'Bob').select('*');
const result = await this.knexService.db.raw('?? UNION ??', [query1, query2]);

原始 SQL 語句:

SELECT * FROM customers WHERE name = 'Alice'
UNION
SELECT * FROM customers WHERE name = 'Bob';

這個語句將返回名字為 'Alice' 或 'Bob' 的顧客的資料。

這些是一些常用的 Knex 查詢語法,它們提供了多種方式來操作資料庫,使你能夠輕鬆地執行各種複雜的查詢操作。如果你有任何其他問題或需要進一步的協助,請隨時告訴我!

從上面這邊可以看到Knex的語法,是用方法鏈式呼叫(Method chaining) 的方式
.select () .where() .orderBy()
這樣一段一段的功能分開呼叫

而且還能夠在where、insert、update等時候放物件進去,
可以用key : value的方式指定欄位名稱與對應的數值

而且拿了那麼多語法,最常用到的仍然是select 、where、update、insert等
這邊請ChatGPT個別的做詳細的語法介紹

Knex的select()語法

https://ithelp.ithome.com.tw/upload/images/20231102/20135750apQiXEHMXk.png

當然,讓我更詳細地解釋 Knex 中的 select 語法以及一些相關的特色。

select 方法

在 Knex 中,select 方法用於指定你希望從資料庫中選擇哪些欄位。以下是一個簡單的例子:

const result = await this.knexService.db('customers').select('name', 'email');

在這個例子中,我們使用了 select 方法來指定我們希望選擇 customers 表格中的 nameemail 欄位。

選擇所有欄位

如果你想選擇所有欄位,可以使用 * 代表所有欄位:

const result = await this.knexService.db('customers').select('*');

這將選擇 customers 表格中的所有欄位。

別名(Alias)

你可以使用 as 來為選擇的欄位取別名:

const result = await this.knexService.db('customers').select('name as customer_name', 'email as customer_email');

這將返回一個結果物件,其中包含 customer_namecustomer_email 這兩個欄位。

聚合函數(Aggregation)

你可以在 select 中使用聚合函數,例如 countsum 等:

const result = await this.knexService.db('orders').select().count('total_amount as total_orders');

這將返回一個結果物件,其中包含一個名為 total_orders 的欄位,表示訂單總數。

使用 distinct

如果你只想選擇唯一的值,可以使用 distinct

const result = await this.knexService.db('customers').select().distinct('email');

這將返回一個包含唯一電子郵件地址的結果集。

使用子查詢

你可以在 select 中使用子查詢:

const subquery = this.knexService.db('orders').where('total_amount', '>', 100);
const result = await this.knexService.db('customers').whereExists(subquery).select('*');

這將選擇所有在 orders 表格中總金額超過 100 的顧客。

這些是一些針對 select 方法的詳細解釋和示例。select 方法是 Knex 中非常常用和重要的一部分,因為它允許你選擇你要檢索的資料,並在處理大量資料時提供了更大的靈活性。如果你有任何其他問題或需要進一步的協助,請隨時告訴我!

可以看到在knex中,語法的組合是很彈性的
接下來看Where

Knex的where()語法

https://ithelp.ithome.com.tw/upload/images/20231102/20135750UJbiTbWHug.png

當然,讓我來詳細介紹 Knex 中的 where 方法。

where 方法

where 方法用於指定查詢的條件。它允許你在查詢中添加條件以過濾資料。

基本使用

以下是一個簡單的例子:

const result = await this.knexService.db('customers').where({ name: 'Alice' }).select('*');

這個例子中,我們在 customers 表格中選擇所有欄位,並添加了一個條件:名字為 'Alice'。

多個條件

你可以使用多個 where 條件來進一步篩選資料:

const result = await this.knexService.db('customers').where({ name: 'Alice', email: 'alice@example.com' }).select('*');

這將返回名字為 'Alice' 且電子郵件為 'alice@example.com' 的顧客。

條件運算符

你可以使用不同的條件運算符,例如 <, >, <=, >=, != 等:

const result = await this.knexService.db('customers').where('age', '>', 30).select('*');

這個例子中,我們選擇所有年齡大於 30 的顧客。

使用 orWhere

如果你想在條件之間進行 OR 運算,你可以使用 orWhere

const result = await this.knexService.db('customers')
  .where({ name: 'Alice' })
  .orWhere({ name: 'Bob' })
  .select('*');

這將返回名字為 'Alice' 或 'Bob' 的顧客。

使用原始 SQL

如果你需要一些比較複雜的條件,你可以使用原始 SQL:

const result = await this.knexService.db('customers')
  .whereRaw('age > ? AND email = ?', [30, 'alice@example.com'])
  .select('*');

這個例子中,我們使用原始 SQL 語句來指定條件。

選擇特定欄位

你可以在 where 條件之後繼續鏈接 select 方法以選擇特定的欄位:

const result = await this.knexService.db('customers').where({ name: 'Alice' }).select('name', 'email');

這將只返回名字和電子郵件地址。

這是一個針對 Knex 中的 where 方法的詳細介紹。where 方法是非常重要的,它允許你在查詢中添加條件以過濾資料,從而得到你想要的結果。如果你有任何其他問題或需要進一步的協助,請隨時告訴我!

https://ithelp.ithome.com.tw/upload/images/20231102/20135750pPomRKxJMC.png

當然,讓我來為你介紹如何在 Knex 中使用 where likewhere in 語法,並提供一些其他常用的 where 語法。

使用 where like

你可以使用 where 方法結合 like 來進行模糊查詢:

const result = await this.knexService.db('customers').where('name', 'like', '%ob%').select('*');

這個語句將返回名字中包含 'ob' 的顧客資料。

使用 where in

你可以使用 whereIn 來進行多個值的查詢:

const result = await this.knexService.db('customers').whereIn('age', [25, 30, 35]).select('*');

這個語句將返回年齡為 25、30 或 35 的顧客資料。

使用 whereNotIn

相對地,你也可以使用 whereNotIn 來排除特定值:

const result = await this.knexService.db('customers').whereNotIn('age', [25, 30, 35]).select('*');

這個語句將返回年齡不是 25、30 或 35 的顧客資料。

使用 whereBetween

你可以使用 whereBetween 來查詢介於兩個值之間的資料:

const result = await this.knexService.db('orders').whereBetween('total_amount', [100, 500]).select('*');

這個語句將返回總金額介於 100 到 500 之間的訂單資料。

使用 whereNotBetween

相對地,你也可以使用 whereNotBetween 來排除介於兩個值之間的資料:

const result = await this.knexService.db('orders').whereNotBetween('total_amount', [100, 500]).select('*');

這個語句將返回總金額不在 100 到 500 之間的訂單資料。

使用 whereRaw

如果你需要使用原生的 SQL 條件,你可以使用 whereRaw

const result = await this.knexService.db('customers').whereRaw('age > ?', [30]).select('*');

這個語句將返回年齡大於 30 的顧客資料。

這些是一些針對 where 方法的進階用法,讓你可以進行更複雜的資料篩選和查詢操作。如果你有任何其他問題或需要進一步的協助,請隨時告訴我!

Knex的update()、insert()語法

https://ithelp.ithome.com.tw/upload/images/20231102/20135750yrzjiJe5iC.png

當然,讓我為你提供有關 Knex 中 insertupdate 方法的詳細介紹。

使用 insert 方法

insert 方法用於將新記錄插入到資料庫表格中。

await this.knexService.db('customers').insert({ name: 'Alice', age: 30 });

這個例子中,我們將一個新的顧客記錄插入到 customers 表格中,這個記錄包含 nameage 兩個欄位的資料。

插入多個記錄

你也可以使用 insert 方法一次插入多個記錄:

await this.knexService.db('customers').insert([
  { name: 'Bob', age: 25 },
  { name: 'Carol', age: 35 },
]);

這將一次性插入兩個新的顧客記錄。

使用 returning 方法

Knex 允許你在 insert 中使用 returning 方法,以返回插入的資料:

const inserted = await this.knexService.db('customers')
  .returning(['customer_id', 'name', 'age'])
  .insert({ name: 'David', age: 40 });

console.log(inserted);

這個語句將插入一條新記錄,並返回 customer_idnameage 這些欄位的資料。

使用 update 方法

update 方法用於更新現有的記錄。

await this.knexService.db('customers').where('name', 'Alice').update({ age: 31 });

在這個例子中,我們將 customers 表格中名字為 'Alice' 的顧客的年齡更新為 31 歲。

使用 returning 方法

你可以使用 returning 方法來返回更新的資料:

const updated = await this.knexService.db('customers')
  .where('name', 'Alice')
  .returning(['customer_id', 'name', 'age'])
  .update({ age: 32 });

console.log(updated);

這個語句將更新名字為 'Alice' 的顧客的年齡,並返回更新後的 customer_idnameage

使用 incrementdecrement

Knex 還提供了 incrementdecrement 方法,用於增加或減少數值欄位的值:

await this.knexService.db('customers').where('name', 'Alice').increment('age', 2);
await this.knexService.db('customers').where('name', 'Bob').decrement('age', 1);

這些方法可以讓你對數值欄位進行增量或減量操作。

這些是有關 insertupdate 方法的詳細介紹。這些方法讓你能夠在 Knex 中執行新增和更新記錄的操作,並具有許多靈活的選項,包括返回已插入或已更新的資料。如果你有任何其他問題或需要進一步的協助,請隨時告訴我!

那如果要用 NOW()、UPPER()等 SQL內建的函數呢?

.raw()

https://ithelp.ithome.com.tw/upload/images/20231102/20135750Itp47UaNxt.png

若要在 Knex 中使用 MySQL 內建函數,你可以使用 .raw() 方法來執行原始的 SQL 語句。這樣你就可以在 SQL 中使用 MySQL 提供的各種函數。

以下是一個例子,假設你想要使用 MySQL 的 UPPER() 函數將名字轉為大寫:

const result = await this.knexService.db.raw('SELECT UPPER(name) as upper_name FROM customers');

這個語句會執行一個原始的 SQL 查詢,使用 UPPER() 函數來將 name 欄位轉為大寫,並且將結果命名為 upper_name

在這個例子中,我們使用了 .raw() 方法來執行原始的 SQL 語句,並且可以在其中使用 MySQL 提供的內建函數。

請注意,在使用 .raw() 方法時,要小心避免 SQL 注入攻擊,應該確保使用參數化的方式來傳遞值。

如果你有特定的 MySQL 內建函數想要使用,請提供該函數的名稱和你想要實現的功能,我可以幫你撰寫相應的 Knex 查詢語句。

仔細一看,ChatGPT給我們的 聯集(Union)範例中,就有用到.raw 的語法
也在上面所提到的「比較複雜」的語句中使用到
來詳細了解一下用法
https://ithelp.ithome.com.tw/upload/images/20231102/20135750HB1HR8wghv.png

.raw() 方法是 Knex 中一個重要且強大的功能,它允許你直接在查詢中嵌入原生的 SQL 語句或表達式。這對於需要在 Knex 查詢中使用特定的資料庫特性或函數時非常有用。

以下是對 .raw() 方法的詳細介紹:

使用 .raw() 方法的基本語法:

this.knexService.raw('<原生 SQL 語句或表達式>');

簡單範例:

const result = await this.knexService.db('customers')
  .select(this.knexService.raw('UPPER(name) as upper_name'))
  .where('id', '=', 1);

在這個示例中,我們使用了 .raw() 方法在 select 語句中嵌入了 UPPER() 函數,將 name 欄位的值轉換成大寫。

帶有參數的 .raw()

const result = await this.knexService.db('customers')
  .select(this.knexService.raw('TIMESTAMPDIFF(?, ?, ?) as time_difference', ['SECOND', 'start_time', 'end_time']))
  .where('id', '=', 1);

在這個示例中,我們使用了 .raw() 方法傳遞了一些參數,這些參數將被插入到原生 SQL 語句的問號位置。

注意事項:

  • 請謹慎使用 .raw(),因為它使得你可以直接在查詢中嵌入原生 SQL,這可能會產生安全風險(例如 SQL 注入)。
  • 盡量避免使用 .raw(),除非你確定你需要使用原生 SQL,而 Knex 提供的其他方法無法滿足你的需求。

.raw() 方法是 Knex 提供的一個強大工具,它允許你在需要的情況下使用原生 SQL 語句或表達式,這樣你可以更靈活地操作資料庫。如果你有任何其他問題或需要進一步的協助,請隨時告訴我!

什麼是「SQL 注入」?
這邊既然ChatGPT提到了,那我們就必須一起來了解一下

SQL injection (SQL注入攻擊)

https://ithelp.ithome.com.tw/upload/images/20231102/20135750GfvRrskR9R.png

SQL 注入是一種常見的資料庫攻擊技術,它允許攻擊者在應用程式的資料輸入中插入惡意的 SQL 語句,從而對資料庫進行非法操作或取得敏感資訊。

如何發生 SQL 注入:

SQL 注入通常發生在應用程式未能適當處理或驗證用戶輸入的情況下。當應用程式直接將用戶輸入的資料插入到 SQL 查詢中而未經適當處理時,攻擊者可以利用這一點來注入惡意的 SQL 語句。

SQL 注入的危害:

  1. 資料庫破壞:攻擊者可以修改、刪除或破壞資料庫中的資料。
  2. 資料洩漏:攻擊者可以取得應用程式未授權的敏感資料,如用戶名、密碼等。
  3. 提升權限:攻擊者可能會成功提升其在應用程式中的權限,使其能夠執行不被授權的操作。

簡單範例:

假設有一個登入頁面,用戶可以輸入用戶名和密碼來登入:

SELECT * FROM users WHERE username='$username' AND password='$password';

如果應用程式未對用戶輸入進行適當處理,攻擊者可以輸入以下值來進行攻擊:

用戶名: ' OR '1'='1
密碼: ' OR '1'='1

這樣原來的 SQL 查詢就變成:

SELECT * FROM users WHERE username='' OR '1'='1' AND password='' OR '1'='1';

由於 1=1 條件永遠為真,這個查詢將返回所有用戶的資料,並允許攻擊者非法登入。

防範 SQL 注入:

要防範 SQL 注入攻擊,開發者應該:

  1. 使用參數化查詢或預處理語句:這是最有效的方法之一,它可以防止攻擊者將惡意 SQL 語句插入到查詢中。
  2. 避免直接拼接用戶輸入:不要直接將用戶輸入的值插入到 SQL 查詢中,而應使用參數或綁定變數的方式。
  3. 進行輸入驗證:對用戶輸入的資料進行驗證和篩選,只接受符合預期格式的輸入。
  4. 最小化賦予權限:應用程式的資料庫使用者應僅具有必要的權限,以最小化潛在的損害。
  5. 監控和紀錄:監控應用程式的輸入和輸出,並紀錄異常或異常的活動。

以上是有關 SQL 注入攻擊的詳細說明和範例。防範 SQL 注入是開發安全應用程式的重要一環,開發者應該時刻保持警惕並使用適當的安全措施來保護應用程式和資料庫。

哇喔,真是太酷了。喔不,我是說,太可怕了。
實際上,早期的許多網頁都沒有想到並防範這種攻擊
因此「SQL injection」這樣的注入攻擊是非常盛行且常見的

而且這也是我們盡量不要洩漏太多資料庫欄位結構的原因之一
必須避免在發生例外時,直接把執行失敗的sql語句給使用者看

對方如果推敲出後端的SQL語法,再加上沒有防範此攻擊,就會發生上述這些慘事

好的我們為了研究用途,來了解更多SQL injection 的範例
https://ithelp.ithome.com.tw/upload/images/20231102/20135750VPz6Ej4BTu.png

當然,以下是一些更多的 SQL 注入範例,以便更清楚地理解這種攻擊的危害性:

1. 刪除資料表

假設有一個應用程式允許用戶輸入他們的名字,然後將其插入到資料庫中:

INSERT INTO users (name) VALUES ('$name');

如果應用程式未對用戶輸入進行適當處理,攻擊者可以輸入以下值:

'); DROP TABLE users; --

這樣原來的 SQL 查詢就變成:

INSERT INTO users (name) VALUES (''); DROP TABLE users; -- ');

這會導致 users 表格被刪除。

2. 繞過登入

假設有一個登入頁面,用戶可以輸入用戶名和密碼來登入:

SELECT * FROM users WHERE username='$username' AND password='$password';

如果應用程式未對用戶輸入進行適當處理,攻擊者可以輸入以下值:

用戶名: ' OR '1'='1
密碼: ' OR '1'='1

這樣原來的 SQL 查詢就變成:

SELECT * FROM users WHERE username='' OR '1'='1' AND password='' OR '1'='1';

由於 1=1 條件永遠為真,這個查詢將返回所有用戶的資料,並允許攻擊者非法登入。

3. 獲取敏感資訊

假設有一個應用程式顯示特定用戶的個人資料:

SELECT * FROM users WHERE id='$id';

如果應用程式未對用戶輸入進行適當處理,攻擊者可以輸入以下值:

' OR '1'='1

這樣原來的 SQL 查詢就變成:

SELECT * FROM users WHERE id='' OR '1'='1';

這會返回所有用戶的資料,而不僅僅是特定用戶的資料。

這些範例突顯了 SQL 注入攻擊的危害性,並強調了開發者需要確保應用程式對用戶輸入進行適當處理以防範這種攻擊。

也就是如果是字串組合再進行Query,因為有一個空間是給使用者自行輸入的
那麼使用者可能就可以加入語法特有的符號與語法,讓組合完畢的query脫離原本的語意
就向上面所示範的一樣

所以使用raw() 不但要確定所使用的語法是正確的,還要將語法的變數部分,先用 ? 代表
再補上參數陣列,去避免被注入攻擊

而使用參數化的方式,我個人覺得有個小缺點就是語法看起來並不是那麼直觀
若有多個參數,就要一個一個的去數他的順序,然後才確定整體的參數帶入對應

而且這邊學到了注入攻擊之後,原本ChatGPT所介紹的kenx的語法
像是 this.knexService.db('customers').where({ name: 'Alice' }).select('*');

這個地方如果是.where({ name: userName }) ,也不會有注入攻擊嗎?
https://ithelp.ithome.com.tw/upload/images/20231102/201357503BCJvDRR0C.png

正確使用 Knex 的話,可以避免 SQL 注入攻擊。以下是為什麼以及如何避免:

  1. 參數化查詢或綁定變數:在 Knex 中,你應該使用參數化查詢或綁定變數的方式來傳遞用戶輸入,而不是直接將其插入 SQL 語句中。這樣可以防止攻擊者注入惡意的 SQL 語句。

    範例

    const name = 'Alice';
    const result = await this.knexService.db('customers').where({ name }).select('*');
    

    在這個範例中,我們使用變數 name 來代表要查詢的名字,而不是直接將其插入 SQL 查詢中。

  2. 使用 Knex 提供的方法:Knex 提供了許多用於構建查詢的方法,這些方法已經經過嚴謹的實現,可以防止 SQL 注入攻擊。

    範例

    const result = await this.knexService.db('customers').where({ name: 'Alice' }).select('*');
    

    在這個範例中,我們使用了 Knex 提供的 whereselect 方法,這些方法已經經過了防範 SQL 注入的處理。

  3. 避免使用 .raw():除非絕對必要,不要使用 knex.raw() 方法來插入原生的 SQL 語句,因為這可能會為攻擊者提供注入點。

綜上所述,正確使用 Knex 提供的方法,並避免直接將用戶輸入插入 SQL 語句中,可以有效地防範 SQL 注入攻擊。

耶~這是我很喜歡knex的其中一個原因
如果使用knex的方法處理,Knex會轉為參數化的語法再拿去丟資料庫query

當然還是要小心 where中使用到 like 的,後方的內容如果是讓使用者輸入 變成like %
模糊查詢的效力一樣會有!

進階用法

接下來了解一下,還有哪些進階議題
說白了就是為下一篇鋪路,也舉列出我們還可以更了解knex的方向
https://ithelp.ithome.com.tw/upload/images/20231102/20135750dHmbwbDc50.png

Knex 是一個功能豐富的查詢構建庫,除了基本的查詢操作外,還提供了許多進階用法,以幫助開發者更靈活地操作資料庫。以下是一些進階 Knex 用法,這些用法可以幫助你進行更複雜的操作:

  1. 交易(Transactions):Knex 允許你執行資料庫交易,這是一系列的查詢操作,要麼全部成功,要麼全部回滾。這在需要保證資料完整性的情況下非常有用。

    await knex.transaction(async (trx) => {
      // 在這裡執行一系列的查詢操作
      await trx.insert({ name: 'Alice' }).into('customers');
      await trx.update({ age: 30 }).where('name', 'Bob').into('customers');
      // 如果所有操作成功,事務會自動提交;否則,會自動回滾。
    });
    
  2. 子查詢(Subqueries):你可以在 Knex 查詢中嵌套子查詢,以執行更複雜的操作。

    const subquery = knex('orders').select('customer_id').where('total_amount', '>', 1000);
    const result = await knex('customers').whereIn('customer_id', subquery);
    
  3. 分組與聚合(Grouping and Aggregation):Knex 允許你使用 groupByhaving 來進行分組和聚合操作。

    const result = await knex('orders')
      .select('customer_id')
      .count('* as order_count')
      .groupBy('customer_id')
      .having('order_count', '>', 5);
    
  4. 子選擇(Subselects):你可以在 .select 中使用子選擇,以從其他表格中擷取資料。

    const result = await knex('customers')
      .select('name')
      .whereIn('customer_id', function () {
        this.select('customer_id').from('orders').where('total_amount', '>', 1000);
      });
    
  5. 模組化查詢:你可以將不同的查詢片段封裝為函數或模組,以實現更容易維護和重用的查詢邏輯。

  6. 跨資料庫操作:Knex 支援多種資料庫,可以輕鬆地在不同資料庫間切換,而不需要改變查詢語法。

  7. 自訂資料庫函數:你可以使用 .with 方法定義自訂資料庫函數,並在查詢中使用它們。

    knex.with('my_function', (qb) => {
      qb.select('*').from('my_table').where('column', '>', 10);
    })
    .select('*')
    .from('other_table')
    .where('id', '=', 5)
    .whereRaw('id IN (SELECT id FROM my_function)');
    

這些是一些 Knex 的進階用法,它們可以幫助你處理更複雜的資料庫操作。根據你的具體需求,你可以根據 Knex 的文檔和資源來探索更多功能和用法。

統整重點

https://ithelp.ithome.com.tw/upload/images/20231102/20135750SlG5OKLA7u.png

綜合以上的對話內容,以下是關於 Knex(資料庫查詢構建庫)的主要重點:

  1. Knex 簡介:Knex 是一個 Node.js 資料庫查詢構建庫,用於簡化資料庫操作,支援多種資料庫系統。
  2. 基本查詢操作:Knex 提供了基本的查詢操作,如 selectinsertupdatedelete,以便於操作資料庫。
  3. 使用 TypeScript:如果你正在使用 TypeScript,Knex 也支援 TypeScript,可以提供類型安全的查詢。
  4. 防範 SQL 注入:正確使用 Knex 提供的方法,如 whereselect,以及參數化查詢或綁定變數,可以有效防範 SQL 注入攻擊。
  5. 進階用法:Knex 還提供了許多進階用法,包括交易、子查詢、分組與聚合、子選擇、模組化查詢、跨資料庫操作和自訂資料庫函數,這些功能可以幫助你處理更複雜的資料庫操作。

總結來說,Knex 是一個強大的工具,可以幫助你更輕鬆地操作資料庫,並提供了豐富的功能和靈活性。請記住,為了確保安全性,請總是正確使用 Knex 的方法和防範 SQL 注入的最佳實踐。如果你有任何關於 Knex 或資料庫操作的問題,請隨時向我查詢,我將樂意協助你。祝你在前端開發和資料庫操作中取得成功!

結語

本次的對話記錄在這邊:
https://chat.openai.com/share/5eb9475c-d20f-44df-a1a1-e97d99d2d7f5

下一篇會更詳細介紹Kenx的進階內容,包含上述提到的transaction(交易)、模組化查詢
以及一開始提到的migration(遷移)來做討論
另外也會再比較其他有關NodeJS進行資料庫查詢工具的優缺點

Knex是我自己在工作上的團隊有使用到,學習了語法之後覺得很喜歡
但並不表示說Knex這個工具就是首選

如果以討論度以及詢問ChatGPT哪個推薦的話,ChatGPT是推薦TypeORM的

說實話,從這個系列開始到現在為止,我以這些內容來當作主題的前提
大多建立在「我有一定的熟悉度」這件事情上
我不熟悉的工具、語言、技能,我打算放在比較後面的章節
像是Python、CICD、Docker,或是現在提到的TypeORM

有一定的熟悉度,所問出來的內容才有一定邏輯和完整度變成系列文章
雖然順序好像是跟著ChatGPT說什麼,我們就追問什麼
但這背後確實是有「某個程度的熟悉」,才知道這個主題還能探討些什麼、該探討什麼
再藉由ChatGPT的回答,額外了解或複習了很多原本不太清楚的知識與用法

因此!有關TypeORM也許會放在後面的章節再慢慢討論!
以上請見諒~也謝謝大家的訂閱,希望我們可以一起點亮這些技能樹

最後再預告這個系列文快要結束了!NestJS的主題在下一篇就會結束
Day51 開始會先進行數篇「ChatGPT幫我完成工作」的主題

會介紹我平常在工作時,除了點亮技能樹、解決特定Bug或問題之外
還讓這個免費版的GPT-3.5幫我完成了什麼樣的內容,來幫我完成工作

以上!

我是喜歡Knex語法的前端工程師一宵三筵,我們明天見!


上一篇
【Day48】ChatGPT請教教我:NestJS!(七)- Service & 例外處理 & HTTP請求
下一篇
【Day50】ChatGPT請教教我:NestJS!(九)- Knex!transaction!migration!
系列文
用ChatGPT詠唱來完成工作與點亮前後端技能樹56
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言