Contents

Cách bảo mật API GraphQL: Triển khai xác thực người dùng trong Express.js bằng JWT

GraphQL là giải pháp thay thế phổ biến cho kiến ​​trúc API RESTful truyền thống, cung cấp ngôn ngữ thao tác và truy vấn dữ liệu linh hoạt và hiệu quả cho API. Với việc áp dụng ngày càng tăng, việc ưu tiên bảo mật API GraphQL để bảo vệ các ứng dụng khỏi bị truy cập trái phép và các vi phạm dữ liệu tiềm ẩn ngày càng trở nên quan trọng.

Một chiến lược khả thi để bảo vệ API GraphQL liên quan đến việc sử dụng Mã thông báo Web JSON (JWT), cung cấp một phương tiện hiệu quả để cấp quyền truy cập vào các tài nguyên bị hạn chế và thực hiện các hoạt động đặc quyền trong khi vẫn duy trì liên lạc an toàn giữa khách hàng và API.

Xác thực và ủy quyền trong API GraphQL

Ngược lại với Giao diện lập trình ứng dụng (API) RESTful, việc triển khai API GraphQL thường có một điểm cuối duy nhất cho phép khách hàng yêu cầu số lượng và loại thông tin đa dạng thông qua các truy vấn của họ. Khả năng thích ứng này vừa là lợi thế vừa là nguồn rủi ro bảo mật tiềm ẩn, đặc biệt đối với các cơ chế kiểm soát truy cập bị xâm phạm.

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

Để quản lý hiệu quả mối đe dọa nói trên, điều quan trọng là phải thiết lập các giao thức xác thực và ủy quyền toàn diện, bao gồm các đặc quyền truy cập được phân định tỉ mỉ. Thông qua các biện pháp như vậy, chúng tôi đảm bảo rằng chỉ những cá nhân được ủy quyền mới được cấp quyền truy cập vào các tài sản nhạy cảm, từ đó giảm thiểu khả năng xảy ra lỗ hổng bảo mật và sự cố rò rỉ dữ liệu.

[chèn liên kết kho GitHub vào đây].

Thiết lập máy chủ Apollo Express.js

Máy chủ Apollo là một triển khai máy chủ GraphQL được sử dụng rộng rãi cho API GraphQL. Bạn có thể sử dụng nó để dễ dàng xây dựng các lược đồ GraphQL, xác định trình phân giải và quản lý các nguồn dữ liệu khác nhau cho API của mình.

Để thiết lập Máy chủ Apollo Express.js cần phải tạo và mở thư mục dự án.

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

Để bắt đầu một dự án Node.js mới bằng cách sử dụng Trình quản lý gói Node (npm), hãy thực hiện lệnh sau:

 npm init --yes 

Bây giờ hãy cài đặt các gói này.

 npm install apollo-server graphql mongoose jsonwebtokens dotenv 

Thật vậy, điều cần thiết là tạo tệp server.js trong thư mục chính của dự án và định cấu hình máy chủ bằng tập lệnh được cung cấp như sau:

 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);
  }); 

Máy chủ GraphQL đã được định cấu hình bằng cách sử dụng kết hợp typeDefsbộ giải quyết, xác định lược đồ và các hoạt động mà API có khả năng xử lý. Ngoài ra, việc sử dụng tùy chọn context cho phép cấu hình đối tượng req bao gồm bối cảnh cụ thể có liên quan đến từng trình phân giải riêng lẻ. Điều này cho phép máy chủ truy cập thông tin theo yêu cầu cụ thể, bao gồm các giá trị tiêu đề, để tạo điều kiện truy xuất dữ liệu hiệu quả hơn.

Tạo cơ sở dữ liệu MongoDB

tạo cơ sở dữ liệu mới trong bản cài đặt MongoDB cục bộ của bạn hoặc sử dụng giải pháp dựa trên đám mây do MongoDB Atlas cung cấp bằng cách thiết lập một cụm và lấy thông tin kết nối cần thiết. Khi bạn đã có được chuỗi URI kết nối thích hợp, bạn có thể tiến hành tạo tệp .env mới và nhập chi tiết kết nối theo định dạng đã chỉ định.

 MONGO_URI="<mongo_connection_uri>"

Xác định mô hình dữ liệu

Chắc chắn rồi, đây là ví dụ về cách xác định mô hình dữ liệu cho người dùng trong tệp lược đồ Mongoose có tên models/user.js:javascriptconst mongoose=require(‘mongoose’);//Xác định lược đồ người dùng userSchema=new mongoose.Schema ({name: { type: String, bắt buộc: true },email: { type: String, duy nhất: true, bắt buộc: true },password: { type: String, bắt buộc: true },});//Biên dịch lược đồ vào modelconst User=mongoose.model(‘User’, userSchema);module.exports=User;Trong ví dụ này, trước tiên chúng tôi nhập thư viện Mongoose ở đầu tệp. Sau đó, chúng ta tạo một đối tượng Schema mới

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

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

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

Xác định lược đồ GraphQL

Lược đồ của API GraphQL thiết lập cách sắp xếp thông tin có thể truy xuất được, ngoài việc mô tả các hoạt động có thể có (truy vấn và thay đổi) có thể được thực thi để giao tiếp với dữ liệu qua giao diện nói trên.

Để thiết lập lược đồ cho dự án của bạn, trước tiên bạn nên tạo một thư mục mới trong thư mục chính của dự án, thư mục này sẽ được gọi là “graphql”. Thư mục cụ thể này sẽ đóng vai trò như một khu vực trong đó tất cả các thành phần liên quan đến hoạt động GraphQL được sắp xếp. Trong giới hạn của thư mục “graphql”, phải tồn tại hai tài liệu riêng biệt-một tài liệu có tên “typeDefs.js” và một tài liệu khác có tên “resolvers.js”.

Trong tệp typeDefs.js, hãy thêm đoạn mã sau:

 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; 

Tạo bộ phân giải cho API GraphQL

Các hàm phân giải đóng vai trò thiết yếu trong việc xác định cách thu thập dữ liệu để đáp ứng các yêu cầu của khách hàng về thông tin và sửa đổi, bao gồm mọi chi tiết bổ sung được chỉ định trong lược đồ. Khi nhận được yêu cầu từ khách hàng, máy chủ GraphQL sẽ kích hoạt các trình phân giải có liên quan để lấy dữ liệu từ nhiều nguồn, bao gồm cơ sở dữ liệu và dịch vụ bên ngoài, rồi trả về dữ liệu tương ứng.

Để kích hoạt xác thực và kiểm soát quyền truy cập bằng cách sử dụng JSON Web Token (JWT) trong máy chủ GraphQL của chúng tôi, trước tiên chúng tôi phải thiết lập trình phân giải cho cả đột biến “đăng ký” và “đăng nhập”. Những người giải quyết này chịu trách nhiệm quản lý các thủ tục tạo và xác minh tài khoản. Sau đó, chúng tôi nên phát triển trình giải quyết truy vấn tìm nạp dữ liệu mà chỉ những người dùng đã đăng ký và xác minh đã được cấp quyền làm như vậy mới có thể truy cập được.

Bộ mã hóa và giải mã RSA JWE từ gói’node-jose’, cũng như mô-đun’crypto’do Node.js cung cấp nguyên bản. Điều này sẽ cho phép chúng tôi thực hiện các hoạt động mã hóa liên quan đến việc tạo và xác thực JWT một cách liền mạch. Bằng cách đó, chúng tôi có thể đảm bảo liên lạc an toàn giữa ứng dụng của mình và người dùng trong khi vẫn duy trì tính đơn giản và dễ sử dụng cho cả nhà phát triển cũng như người dùng cuối.

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

Hãy đảm bảo rằng bạn bao gồm khóa bí mật cần thiết để ký Mã thông báo Web JSON trong tệp.env của mình để sử dụng khóa đó đúng cách.

 SECRET_KEY = '<my_Secret_Key>'; 

Để tạo mã thông báo xác thực hợp lệ, điều cần thiết là phải sử dụng chức năng được cung cấp và chỉ định các đặc điểm tùy chỉnh, bao gồm cả thời gian hết hạn, trong mã thông báo JWT. Hơn nữa, các thuộc tính bổ sung như thời gian phát hành có thể được đưa vào dựa trên nhu cầu cụ thể của ứng dụng.

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

  return token;
} 

Để đảm bảo tính xác thực của Mã thông báo Web JSON (JWT) được sử dụng trong các trao đổi HTTP trong tương lai, cần phải kết hợp quy trình xác thực cho các mã thông báo này. Điều này sẽ đạt được bằng cách triển khai cơ chế xác thực để kiểm tra và xác nhận tính hợp pháp của từng JWT được trình bày trong các tương tác như vậy.

 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');
  }
} 

Hàm đã cho chấp nhận mã thông báo làm đầu vào, sử dụng khóa bí mật được chỉ định để xác thực tính xác thực của nó. Nếu mã thông báo được chứng minh là hợp pháp, hàm sẽ trả về mã thông báo đã được giải mã; tuy nhiên, trong trường hợp có bất kỳ sự khác biệt nào, nó sẽ đưa ra một ngoại lệ biểu thị sự hiện diện của mã thông báo không hợp lệ.

Xác định Trình phân giải API

Để mô tả quy trình giải quyết cho API GraphQL, chúng tôi phải chỉ định các nhiệm vụ hoặc hành động cụ thể mà nó sẽ thực hiện, chẳng hạn như quản lý quy trình đăng ký và đăng nhập của người dùng. Ban đầu, hãy thiết lập một đối tượng chứa các hàm phân giải dùng để thực hiện các tác vụ được chỉ định này. Sau đó, thực hiện các đột biến sau để tạo điều kiện thuận lợi cho các hoạt động này:

 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');
      }
    }
  }, 

Đột biến đăng ký chịu trách nhiệm xử lý quá trình đăng ký thông qua việc bổ sung dữ liệu người dùng mới vào cơ sở dữ liệu. Mặt khác, đột biến đăng nhập xử lý thông tin đăng nhập của người dùng và sau khi xác thực thành công, sẽ tạo ra Mã thông báo Web JSON (JWT) đồng thời trả về thông báo thành công trong phản hồi.

Để giới hạn quyền truy cập vào truy vấn truy xuất dữ liệu người dùng dành riêng cho người dùng được xác thực và có đặc quyền sở hữu vai trò “Quản trị viên”, điều cần thiết là phải kết hợp logic ủy quyền trong hệ thống. Điều này đảm bảo rằng những cá nhân không được phép sẽ không thể truy cập vào thông tin nhạy cảm.

Về bản chất, cuộc điều tra ban đầu sẽ xác thực tính hợp pháp của mã thông báo, sau đó là đánh giá vai trò của người dùng. Trong trường hợp việc kiểm tra ủy quyền tỏ ra thuận lợi, yêu cầu giải quyết sẽ truy xuất và cung cấp thông tin của người dùng cuối từ kho lưu trữ.

   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');
      }
    },
  },
}; 

Cuối cùng, khởi động máy chủ phát triển:

 node server.js 

Thật vậy, bạn nên xác thực khả năng hoạt động của API GraphQL của chúng tôi bằng cách sử dụng hộp cát API máy chủ Apollo trong trình duyệt web của bạn. Để minh họa quá trình này, hãy xem xét việc sử dụng đột biến “đăng ký” để nhập thông tin người dùng mới vào cơ sở dữ liệu, sau đó là đột biến “đăng nhập” cho mục đích xác thực.

/vi/images/login-mutation.jpg

Cuối cùng, kết hợp mã thông báo JWT trong tham số tiêu đề ủy quyền và sau đó truy xuất thông tin người dùng từ cơ sở dữ liệu bằng cách thực hiện truy vấn.

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

Bảo mật API GraphQL

Xác thực và ủy quyền đóng vai trò then chốt trong việc đảm bảo tính bảo mật của API GraphQL; tuy nhiên, việc thực hiện riêng chúng không đảm bảo sự bảo vệ hoàn toàn. Điều cần thiết là phải kết hợp các biện pháp an toàn hơn nữa như xác thực đầu vào mạnh mẽ và mã hóa thông tin nhạy cảm để đạt được bảo mật triệt để.

Việc triển khai chiến lược bảo mật toàn diện là điều cần thiết để bảo vệ giao diện lập trình ứng dụng (API) của bạn khỏi nhiều loại mối đe dọa khác nhau có thể phát sinh.