Contents

如何保護 GraphQL API:使用 JWT 在 Express.js 中實現使用者身份驗證

GraphQL 是傳統 RESTful API 架構的熱門替代方案,為 API 提供靈活且有效率的資料查詢和操作語言。隨著其日益普及,優先考慮 GraphQL API 的安全性以保護應用程式免受未經授權的存取和潛在的資料外洩變得越來越重要。

保護GraphQL API 的一種可行策略是利用JSON Web 令牌(JWT),它提供了一種有效的方法來授予對受限資源的存取權並執行特權操作,同時保持客戶端和API 之間的安全通信。

GraphQL API 中的身份驗證和授權

與 RESTful 應用程式介面 (API) 相比,GraphQL API 實作通常具有單獨的端點,使用戶端能夠透過查詢請求不同數量和類型的資訊。這種適應性既代表了優勢,也代表了安全風險的潛在來源,特別是在受損的存取控制機制方面。

/bc/images/altumcode-dc6pb2jdaqs-unsplash-1.jpg

為了有效管理上述威脅,建立全面的身份驗證和授權協議至關重要,其中包括精心劃分存取權限。透過這些措施,我們確保只有經過授權的個人才能存取敏感資產,從而最大限度地減少安全漏洞和資料外洩事件的可能性。

[在此處插入 GitHub 儲存庫連結]。

設定 Express.js Apollo 伺服器

Apollo Server 是一種廣泛使用的 GraphQL API 的 GraphQL 伺服器實作。您可以使用它輕鬆建立 GraphQL 架構、定義解析器以及管理 API 的不同資料來源。

要建立 Express.js Apollo 伺服器,需要建立並開啟專案目錄。

 mkdir graphql-API-jwt
cd graphql-API-jwt 

若要利用 Node Package Manager (npm) 啟動新的 Node.js 項目,請執行下列命令:

 npm init --yes 

現在,安裝這些軟體包。

 npm install apollo-server graphql mongoose jsonwebtokens dotenv 

事實上,必須在專案的主資料夾中產生 server.js 文件,並使用提供的腳本配置伺服器,如下所示:

 const { ApolloServer } = require('apollo-server');
const mongoose = require('mongoose');
require('dotenv').config();

const typeDefs = require("./graphql/typeDefs");
const resolvers = require("./graphql/resolvers");

const server = new ApolloServer({
     typeDefs,
     resolvers,
    context: ({ req }) => ({ req }),
 });

const MONGO_URI = process.env.MONGO_URI;

mongoose
  .connect(MONGO_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => {
    console.log("Connected to DB");
    return server.listen({ port: 5000 });
  })
  .then((res) => {
    console.log(`Server running at ${res.url}`);
  })
  .catch(err => {
    console.log(err.message);
  }); 

GraphQL 伺服器已使用 typeDefsresolvers 的組合進行配置,它們定義了 API 能夠處理的架構和操作。此外,使用 context 選項允許配置 req 物件以包含與每個單獨的解析器相關的特定上下文。這使得伺服器能夠存取特定於請求的信息,包括標頭值,以便於更有效地檢索資料。

建立 MongoDB 資料庫

可以在本地安裝的 MongoDB 中建立新資料庫,也可以透過設定叢集並取得必要的連接資訊來利用 MongoDB Atlas 提供的基於雲端的解決方案。獲得適當的連接 URI 字串後,您可以繼續建立新的“.env”檔案並以指定的格式輸入連接詳細資訊。

 MONGO_URI="<mongo_connection_uri>"

定義資料模型

當然,這裡有一個範例,說明如何在名為「models/user.js」的Mongoose 架構檔案中為使用者定義資料模型:javascriptconst mongoose=require(‘mongoose’);//定義使用者架構const userSchema=new mongoose.Schema ({name: { type: String, required: true },email: { type: String, unique: true, required: true },password: { type: String, required: true },});//編譯schema到模型中 const User=mongoose.model(‘User’, userSchema);module.exports=User;在此範例中,我們首先在檔案頂部匯入 Mongoose 函式庫。然後,我們建立一個新的 Schema 對象

 const {model, Schema} = require('mongoose');

const userSchema = new Schema({
    name: String,
    password: String,
    role: String
});

module.exports = model('user', userSchema); 

定義 GraphQL 架構

GraphQL API 的模式除了描述可以執行以透過所述介面與資料進行通訊的可能操作(查詢和變更)之外,還建立了可檢索資訊的排列。

為了為您的專案建立模式,您應該先在您的專案的主目錄中建立一個新資料夾,稱為「graphql」。這個特定的資料夾應充當一個 enclave,其中組織與 GraphQL 操作相關的所有元件。在“graphql”資料夾的範圍內,必須有兩個不同的文件-一個名為“typeDefs.js”,另一個名為“resolvers.js”。

typeDefs.js檔案中,合併以下程式碼:

 const { gql } = require("apollo-server");

const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    password: String!
    role: String!
  }
  input UserInput {
    name: String!
    password: String!
    role: String!
  }
  type TokenResult {
    message: String
    token: String
  }
  type Query {
     users: [User]
   }
  type Mutation {
    register(userInput: UserInput): User
    login(name: String!, password: String!, role: String!): TokenResult
  }
`;

module.exports = typeDefs; 

為 GraphQL API 建立解析器

解析器函數在決定如何獲取資料以回應客戶端對資訊和修改的請求(包括架構中指定的任何其他詳細資訊)方面發揮著重要作用。 GraphQL 伺服器收到客戶端的請求後,會啟動相關解析器從多個來源(包括資料庫和外部服務)取得數據,並相應地傳回。

為了在 GraphQL 伺服器中使用 JSON Web Token (JWT) 啟用身份驗證和存取控制,我們必須先為「註冊」和「登入」突變建立解析器。這些解析器負責管理帳戶建立和驗證程序。隨後,我們應該開發一個資料擷取查詢解析器,該解析器只能由已獲得許可的註冊和驗證使用者存取。

來自「node-jose」套件的 RSA JWE 編碼器和解碼器,以及 Node.js 原生提供的「crypto」模組。這將使我們能夠無縫執行與 JWT 生成和驗證相關的加密操作。透過這樣做,我們可以確保應用程式與其用戶之間的安全通信,同時保持開發人員和最終用戶的簡單性和易用性。

 const User = require("../models/user");
const jwt = require('jsonwebtoken');
const secretKey = process.env.SECRET_KEY; 

請確保在.env 檔案中包含簽署 JSON Web 令牌所需的金鑰,以便正確使用它。

 SECRET_KEY = '<my_Secret_Key>'; 

為了產生有效的身份驗證令牌,必須利用提供的功能並在 JWT 令牌中指定自訂特徵,包括到期時間。此外,根據應用程式的特定需求,還可以包括諸如發行時間之類的附加屬性。

 function generateToken(user) {
  const token = jwt.sign(
   { id: user.id, role: user.role },
   secretKey,
   { expiresIn: '1h', algorithm: 'HS256' }
 );

  return token;
} 

為了確保未來 HTTP 交換中使用的 JSON Web 令牌 (JWT) 的真實性,有必要對這些令牌進行驗證程序。這將透過實施一種身份驗證機制來實現,該機制檢查並確認在此類互動期間呈現的每個 JWT 的合法性。

 function verifyToken(token) {
  if (!token) {
    throw new Error('Token not provided');
  }

  try {
    const decoded = jwt.verify(token, secretKey, { algorithms: ['HS256'] });
    return decoded;
  } catch (err) {
    throw new Error('Invalid token');
  }
} 

給定的函數接受令牌作為輸入,利用指定的金鑰來驗證其真實性。如果令牌被證明是合法的,則該函數傳回解密後的令牌;但是,如果出現任何差異,它會引發異常,表示存在無效令牌。

定義 API 解析器

為了描述 GraphQL API 的解析過程,我們必須指定它將執行的特定任務或操作,例如管理使用者註冊和登入程序。首先,建立一個包含用於執行這些指定任務的解析器函數的物件。隨後,實施以下突變以促進這些操作:

 const resolvers = {
  Mutation: {
    register: async (_, { userInput: { name, password, role } }) => {
      if (!name || !password || !role) {
        throw new Error('Name password, and role required');
     }

      const newUser = new User({
        name: name,
        password: password,
        role: role,
      });

      try {
        const response = await newUser.save();

        return {
          id: response._id,
          ...response._doc,
        };
      } catch (error) {
        console.error(error);
        throw new Error('Failed to create user');
      }
    },
    login: async (_, { name, password }) => {
      try {
         const user = await User.findOne({ name: name });

        if (!user) {
          throw new Error('User not found');
       }

        if (password !== user.password) {
          throw new Error('Incorrect password');
        }

         const token = generateToken(user);

         if (!token) {
          throw new Error('Failed to generate token');
        }

        return {
          message: 'Login successful',
          token: token,
        };
      } catch (error) {
        console.error(error);
        throw new Error('Login failed');
      }
    }
  }, 

註冊突變負責透過向資料庫新增使用者資料來處理註冊過程。另一方面,登入突變處理使用者登錄,並在身份驗證成功後產生 JSON Web 令牌 (JWT),同時也在回應中傳回成功訊息。

為了將對檢索用戶資料的查詢的存取權限限制為僅具有「管理員」角色的經過身份驗證的特權用戶,必須在系統中合併授權邏輯。這確保未經授權的個人無法存取敏感資訊。

本質上,查詢將首先驗證代幣的合法性,然後評估用戶的角色。如果授權審查證明是有利的,則解決請求應從儲存庫中檢索並產生最終使用者的資訊。

   Query: {
    users: async (parent, args, context) => {
      try {
        const token = context.req.headers.authorization || '';
        const decodedToken = verifyToken(token);

        if (decodedToken.role !== 'Admin') {
          throw new ('Unauthorized. Only Admins can access this data.');
        }

        const users = await User.find({}, { name: 1, _id: 1, role:1 });
         return users;
      } catch (error) {
        console.error(error);
        throw new Error('Failed to fetch users');
      }
    },
  },
}; 

最後,啟動開發伺服器:

 node server.js 

事實上,強烈建議您透過在 Web 瀏覽器中使用 Apollo Server API 沙箱來驗證我們的 GraphQL API 的可操作性。為了說明此過程,請考慮利用「註冊」突變將新使用者資訊輸入資料庫,然後使用「登入」突變進行身份驗證。

/bc/images/login-mutation.jpg

最後,將 JWT 令牌合併到授權標頭參數中,然後透過執行查詢從資料庫中檢索使用者資訊。

/bc/images/graphql-api-query.jpg

保護 GraphQL API 的安全

身份驗證和授權在確保 GraphQL API 的安全性方面發揮關鍵作用;然而,僅實施這些措施並不能保證提供全面的保護。必須納入進一步的安全措施,例如強大的輸入驗證和敏感資訊加密,以實現徹底的安全。

實施全面的安全策略對於保護您的應用程式介面 (API) 免受可能出現的各種類型的威脅至關重要。