socket.io身份验证系统与JWT
#node #socketio #backend #authentication

在验证socket.io连接时,很少有资源可以探索。我最近完成了一个编程项目,该项目涉及认证套接字连接。在项目期间,我尽力搜索解释如何为socket.io连接创建身份验证系统的资源,但无济于事。它们要么对我的项目的用例不起作用,要么不清楚。通过一些努力,我能够为服务器构建安全的身份验证系统,以便在建立连接之前将对每个连接客户端进行身份验证。

在本文中,我将带您完成使用JSON Web令牌(JWT)构建安全的socket.io身份验证系统的分步过程。这些知识也可以转移到其他身份验证库,例如passport.js或其他编程语言的身份验证库,因为我将使用Nodejs。

让我们潜水!

概述

我将解释示例代码的每个细节,让每个人都遵循,无论他们熟悉的技术堆栈如何。

但是,我们将使用MongoDB User Collection,http服务器,Express appsocket.io服务器实例构建身份验证系统。此外,我们将提供示例客户端代码以演示其用例。

User集合用于存储和检索用户的凭据。

http服务器用于收听HTTP和套接字连接请求。

Express app用于设置将处理注册和登录身份验证端点的功能处理程序。预计包含JSON Web令牌(JWT)的响应是成功的身份验证。

socket.io服务器实例负责管理套接字连接事件。中间件功能用于验证客户端发送的JWT,以确保只有身份验证的用户才能制作Web套接字连接请求。

客户端代码用于向身份验证端点提出HTTP请求并存储JWT响应,然后将其用于将Web套接字连接请求到socket.io Server实例。

身份验证系统

在本节中,我们构建身份验证系统。从数据库模型到socket.io服务器实例身份验证中间件设置。

数据库模型

让我们从创建User模型架构开始。

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
    username: String,
    email: String,
    password: String,
  });

const User = mongoose.model('User', userSchema);

module.exports = User;

我们导入mongoose模块,该模块提供了定义和与MongoDB模式和模型交互的功能。

使用mongoose.Schema构造函数创建userSchema,指定用户对象的结构和数据类型。该模式包括usernameemailpassword的字段,每个类型String

然后使用userSchema创建User模型。该模型允许在连接的MongoDB数据库中与“用户”集合进行交互。

最后,将User模型导出以使其可用于身份验证系统。

明确身份验证处理程序

在这里,我们将使用Express创建用于注册和登录端点的处理程序功能。

首先,让我们导入必要的模块。

const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require("models/user.model");

在上面的代码中,expressbcryptjsonwebtoken模块都被分配给其各自的变量。此外,导入User数据库模型以启用用户信息的存储和验证。

接下来,我们创建Express app

const app = express();
app.use(express.json());

// Register API endpoint
app.post('/auth/register', registerUser);

// Login API endpoint
app.post('/auth/login', loginUser);

我们首先调用Express模块​​提供的express()函数,该功能使我们能够注册端点及其各自的处理程序功能。此外,我们将express.json()中间件附加到解析JSON请求有效载荷。随后将两种POST方法添加到Express app中,使其能够在/auth/register/auth/login端点上处理客户端请求。这些请求分别由registerUserloginUser处理程序功能处理。

接下来,我们继续开发registerUser处理程序功能的逻辑。

// User Registration Handler Function
async function registerUser(req, res) {
  try {
    const { username, email, password } = req.body;

    // Check if the username or email already exists
    const existingUser = await User.findOne().or([{ username }, { email }]);
    if (existingUser) {
      return res.status(400).json({ message: 'Username or email already exists' });
    }

    // Hash the password
    const hashedPassword = await bcrypt.hash(password, 10);

    // Create a new user
    const newUser = new User({
      username,
      email,
      password: hashedPassword,
    });

    // Save the user to the database
    await newUser.save();
    res.status(201).json({ message: 'Registration successful' });
  } catch (error) {
    console.error('Registration error', error);
    res.status(500).json({ message: 'Registration error' });
  }
}

我们首先从req.body对象破坏usernameemailpassword,其中包含客户端发送的请求主体。接下来,我们通过在数据库中搜索emailusername的唯一性进行检查。如果找到了任何一个,则返回400不良请求响应。否则,该函数将使用BCRYPT模块提供的bcrypt.hash()功能进行哈希密码。哈希密码以及破坏的usernameemail属性,然后将其保存到数据库中以持久。在没有错误的情况下,返回了201创建的响应。但是,如果执行处理程序函数期间发生任何错误,则返回500内部服务器错误响应。

接下来,我们继续开发loginUser处理程序功能的逻辑。

// ...

// User Login Handler Function
async function loginUser(req, res) {
  try {
    const { username, password } = req.body;

    // Check if the username exists
    const user = await User.findOne({ username });
    if (!user) {
      return res.status(400).json({ message: 'Invalid username or password' });
    }

    // Compare the password
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      return res.status(400).json({ message: 'Invalid username or password' });
    }

    // Generate a JWT
    const token = jwt.sign({ userId: user._id }, process.env.SECRET_KEY);

    res.json({ token, message: 'Login successful' });
  } catch (error) {
    console.error('Login error', error);
    res.status(500).json({ message: 'Login error' });
  }
}

// ...

我们首先验证数据库中是否存在破坏的username。如果不存在,则返回400不良请求响应。另外,如果破坏的password与数据库中的用户存储密码不匹配,则返回400不良响应。但是,如果密码匹配,我们将使用jsonwebtoken模块生成JWT令牌。通过将userId键设置为用户的_id属性的值来生成该令牌,MongoDB自动将其分配给每个保存的用户信息。假设未发生错误,返回包含生成令牌的响应。

接下来,我们导出Express app

// ...
module.exports = app;

可以在下面找到身份验证端点的完整代码。

const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require("models/user.model");

const app = express();
app.use(express.json());

// Register API endpoint
app.post('/auth/register', registerUser);

// Login API endpoint
app.post('/auth/login', loginUser);

// User Registration Handler Function
async function registerUser(req, res) {
  try {
    const { username, email, password } = req.body;

    // Check if the username or email already exists
    const existingUser = await User.findOne().or([{ username }, { email }]);
    if (existingUser) {
      return res.status(400).json({ message: 'Username or email already exists' });
    }

    // Hash the password
    const hashedPassword = await bcrypt.hash(password, 10);

    // Create a new user
    const newUser = new User({
      username,
      email,
      password: hashedPassword,
    });

    // Save the user to the database
    await newUser.save();
    res.json({ message: 'Registration successful' });
  } catch (error) {
    console.error('Registration error', error);
    res.status(500).json({ message: 'Registration error' });
  }
}

// User Login Handler Function
async function loginUser(req, res) {
  try {
    const { username, password } = req.body;

    // Check if the username exists
    const user = await User.findOne({ username });
    if (!user) {
      return res.status(400).json({ message: 'Invalid username or password' });
    }

    // Compare the password
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      return res.status(400).json({ message: 'Invalid username or password' });
    }

    // Generate a JWT
    const token = jwt.sign({ userId: user._id }, process.env.SECRET_KEY);

    res.json({ token, message: 'Login successful' });
  } catch (error) {
    console.error('Login error', error);
    res.status(500).json({ message: 'Login error' });
  }
}

module.exports = app;

我们的Express app现在可以使用。

插座服务器设置

现在,让我们设置httpsocket.io服务器实例以进行身份​​验证。

const http = require('http');
const ioServer = require('socket.io');
const app = require('./app');

我们首先导入httpsocket.io模块,以及我们的express app

接下来,我们继续创建httpsocket.io服务器的实例。

// Create server from express app
const server = http.createServer(app);

// Create the socket server instance
const io = ioServer(server);

我们使用http模块提供的createServer()功能来创建HTTP服务器实例。然后将Express app传递给该函数,以将请求处理到身份验证端点。然后使用http服务器来创建socket.io服务器实例。

接下来,我们继续添加身份验证中间件。

io.use(async (socket, next) => {
        try {
            const token = socket.handshake.auth.token;

            // Verify and decode the JWT
            const decoded = jwt.verify(token, process.env.SECRET_KEY);

            // Get the user information from the database
            const user = await User.findById(decoded.userId);
            if (!user) {
                throw new Error('User not found');
            }

            // Attach the user object to the socket
            socket.user = user;
            next();
        } catch (error) {
            console.error('Authentication error', error);
            next(new Error('Authentication error'));
        }
    });

    io.on('connection', (socket) => {
        // Handle Events after authentication
    }

在上述代码中,当客户端连接到服务器时,调用了由socket.io库提供的中间件函数io.use()。在功能内部,我们首先从客户端在握手(连接)期间发送的socket.handshake.auth.token属性检索JWT令牌。然后,它使用存储在环境变量中的秘密密钥来验证和解码令牌。

如果令牌有效,则中间件会根据从令牌提取的用户ID从数据库中检索用户信息。如果找到用户,则将用户对象连接到套接字以供将来参考。

如果在身份验证过程中发生任何错误,例如无效令牌或找不到的用户,则会丢弃错误,并且中间件使用错误参数调用next()函数。

成功身份验证后,触发了io.on('connection')事件处理程序,允许与身份验证的用户进行进一步的事件处理和通信。

身份验证系统现已完成。但是要收听连接,可以将下面的代码添加或定制到您的喜好中。

server.listen(PORT, () => {
    console.log(` Server started running at ${PORT}`);
});

其中PORT是收听连接的首选端口。另外,请不要忘记在启动服务器之前连接到MongoDB系列。

可以在下面找到服务器设置的完整代码。

const http = require('http');
const ioServer = require('socket.io');
const app = require('./app');
// Create server from express app
const server = http.createServer(app);

// Create the socket server instance
const io = ioServer(server);

io.use(async (socket, next) => {
        try {
            const token = socket.handshake.auth.token;

            // Verify and decode the JWT
            const decoded = jwt.verify(token, process.env.SECRET_KEY);

            // Get the user information from the database
            const user = await User.findById(decoded.userId);
            if (!user) {
                throw new Error('User not found');
            }

            // Attach the user object to the socket
            socket.user = user;
            next();
        } catch (error) {
            console.error('Authentication error', error);
            next(new Error('Authentication error'));
        }
    });

    io.on('connection', (socket) => {
        // Handle Events after authentication
    }

    server.listen(PORT, () => {
        console.log(` Server started running at ${PORT}`);
    });

客户端连接

要演示如何设置客户端以使用身份验证系统,我们将使用AXIOS向身份验证端点提出请求,并使用检索到的令牌连接到socket.io Server实例。

让我们从注册用户开始。

const axios = require('axios');
const io = require('socket.io-client');

const username = 'HayatsCodes';
const email = 'hayatscodes@gmail.com';
const password = 123456;
let token;

try {
    const response = await axios.post(`http://localhost:${PORT}/auth/register`, {
      username,
      email,
      password,
    });
    console.log(response.data.message); // Registration successful
  } catch (error) {
      console.error(error);
  }

首先,axiossocket.io-client库是导入的。

然后,我们通过向服务器的/auth/register端点提出POST请求来注册用户。服务器URL是使用PORT变量构建的,该变量应包含端口号。

用户的usernameemailpassword作为请求有效负载提供。我们使用await关键字来提出请求并将响应存储在response变量中。

如果注册成功,则响应中的杂物记录到控制台。

如果在注册过程中发生错误,则执行catch块,并且使用console.error记录到控制台。

现在,让我们登录注册用户。

try {
      const response = await axios.post(`http://localhost:${PORT}/auth/login`, {
        username,
        password,
      });
      token = response.data.token;
      console.log(response.data.message); // login successful
    } catch (error) {
        console.error(error.response.data); 
    }
};

我们正在通过向服务器的/auth/login端点提出发布请求,同时将usernamepassword在“请求有效负载”中包括在内。

如果登录成功,则将令牌从response.data.token属性中提取,并且message被记录到控制台。

然后将令牌分配给token变量,以用于socket.io客户端连接。

如果在注册过程中发生错误,则执行catch块,并且使用console.error记录到控制台。

现在让我们连接到socket.io服务器。

const client = io(`http://localhost:${PORT}`, {
      auth: {
        token
      }
});
// handle events
client.on('connect', () => { console.log('connected!') });
// Additional event handling can follow

在上面的代码段中,我们使用socket.io-client库提供的io函数建立了与socket.io服务器的客户端连接。

使用套接字服务器URL调用io函数,将对象作为参数,其中包括auth属性。此属性指定将在握手过程中发送到服务器的身份验证令牌。从登录请求中提供了较早保存的token变量的值。

建立连接后,设置了client.on('connect')事件处理程序以聆听“连接”事件。当客户端成功连接到服务器时,执行回调函数,记录“连接!”到控制台。

可以在适当的事件处理程序中添加其他事件处理和与服务器的通信。

结论

由于可用的资源有限,为socket.io连接建立安全的身份验证系统可能是一项具有挑战性的任务。但是,通过利用JSON Web令牌(JWT),可以创建一个强大的身份验证系统。在本文中,我们介绍了构建此类系统的分步过程,包括设置数据库模型,使用Express创建身份验证端点,使用socket.io实现身份验证中间件,并使用axiossocket.io-client演示客户端连接图书馆。通过遵循提供的代码示例和说明,开发人员可以为socket.io连接构建自己的安全身份验证系统,仅允许身份验证的用户建立连接并与服务器进行交互。