본문 바로가기
개발/React

리액트, Node JS - Socket.IO Middleware로 중복 로그인 방지하기 (Limiting Connections using Socket.IO Middleware)

by 피로물든딸기 2024. 4. 5.
반응형

리액트 전체 링크

Node JS 전체 링크

 

참고

- https://socket.io/docs/v4/middlewares/

 

chat-ui-kit-react로 채팅창 UI 만들기
로그인 + 채팅방 UI 만들기
채팅방 변경하기
채팅방 들어오고 나갈 때 표시하기
Socket.IO로 채팅하기
Socket.IO Room으로 채팅방 관리하기
Socket.IO namespace로 채팅방 관리하기

- Socket.IO로 중복 로그인 제한하기

- Socket.IO Middleware로 중복 로그인 방지하기

 

이전 글에서 중복 로그인을 제한했던 내용을 Socket.IO의 Middleware로 구현해 보자.


Middleware

 

socket.io의 미들웨어use()를 이용해서 구현할 수 있다.

use의 코드가 실행되고, next()로 순차적으로 실행된다.

// middleware
io.use((socket, next) => {
  console.log("middleware : login check");
  
  if(error) { 
    return next(new Error("error message"));  
  }
  
  next();
})

io.use((socket, next) => {
  console.log("middleware : another check");
  next();
})

 

예를 들어 중복 로그인 체크는 다음과 같이 구현할 수 있다.

주로 인증과 관련된 내용은 client에서 auth 객체에 내용을 추가한다.

const loginIDSet = new Set();
const loginIDMap = new Map();

// middleware = login check
io.use((socket, next) => {
  console.log("middleware : login check");
  let loginID = socket.handshake.auth.loginID;  
  if(loginID === undefined) {
    console.log("Login Error!");
    return next(new Error("Login ID is undefined!"));
  }

  if(loginIDSet.has(loginID)) {
    // return next({ message : "Login ID already exists!", id : socket.id });
    console.log("Login Error!!");  
    return next(new Error("Login ID already exists!"));  
  }
  
  loginIDSet.add(loginID);
  loginIDMap.set(socket.id, loginID);

  next();
})

 

socket 클라이언트는 authloginID를 담아서 서버로 보낸다.

  const init = () => {
    socketIO.connect();
    
    setLoginID(location.state.loginID);
    
    // socketIO.emit("login", location.state.loginID);
    socketIO.auth = { loginID : location.state.loginID };
	
    ...

 

그리고 connect_error(예약된 이벤트)middleware에서 전송된 error message를 보고 필요한 구현하면 된다.

  useEffect(() => {
    socketIO.on("connect_error", (err) => {
      console.error(err);
      if (err.message === "Login ID is undefined!") {
        console.log("비정상 로그인!");
      } else if (err.message === "Login ID already exists!") {
        window.alert(err.message);
        window.location.href = "/";
      }
    });
  }, []);

전체 코드는 다음과 같다.

 

socketIOServer.js

const { Server } = require("socket.io");

const loginIDSet = new Set();
const loginIDMap = new Map();

const io = new Server("3333", {
  cors: {
    origin: "http://localhost:3000",
  },
});

// middleware
io.use((socket, next) => {
  console.log("middleware : login check");
  let loginID = socket.handshake.auth.loginID;  
  if(loginID === undefined) {
    console.log("Login Error!");
    return next(new Error("Login ID is undefined!"));
  }

  if(loginIDSet.has(loginID)) {
    // return next({ message : "Login ID already exists!", id : socket.id });
    console.log("Login Error!!");  
    return next(new Error("Login ID already exists!"));  
  }
  
  loginIDSet.add(loginID);
  loginIDMap.set(socket.id, loginID);

  next();
})

io.use((socket, next) => {
  console.log("middleware : another check");
  next();
})

io.sockets.on("connection", (socket) => {
  console.log("Login ID", socket.handshake.auth.loginID);

  socket.on("enter", (roomID) => {
    socket.join(roomID);
    console.log("enter", socket.rooms);              
  });

  socket.on("leave", (roomID) => {
    socket.leave(roomID);
    console.log("leave", socket.rooms);            
  });

  socket.on("sendMessage", (data) => {
    let { message, roomID } = data;
    
    console.log(socket.rooms);        

    socket.broadcast.in(roomID).emit("respondMessage", { message, roomID });

    // console.log(loginIDMap); console.log(loginIDSet);    
  });

  socket.on("disconnect", () => {
    let loginID = loginIDMap.get(socket.id);

    console.log("disconnect", socket.id, loginID);
    
    loginIDMap.delete(loginID);
    loginIDSet.delete(loginID);
  });
});

 

ChatUI.js (Client)

import React, { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { io } from "socket.io-client";

import styles from "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";

import {
  MainContainer,
  ChatContainer,
  MessageList,
  Message,
  MessageInput,
  Avatar,
  Sidebar,
  ConversationList,
  Conversation,
  ConversationHeader,
  VoiceCallButton,
  VideoCallButton,
  InfoButton,
  MessageSeparator,
} from "@chatscope/chat-ui-kit-react";

let socketIO = io("http://localhost:3333", { autoConnect: false });

const AVATAR_IMAGE =
  "https://img1.daumcdn.net/thumb/C428x428/?scode=mtistory2&fname=https%3A%2F%2Ftistory3.daumcdn.net%2Ftistory%2F4431109%2Fattach%2F3af65be1d8b64ece859b8f6d07fafadc";

const AVATAR_MAP = {
  Lilly: "https://chatscope.io/storybook/react/assets/lilly-aj6lnGPk.svg",
  Joe: "https://chatscope.io/storybook/react/assets/joe-v8Vy3KOS.svg",
  Emily: "https://chatscope.io/storybook/react/assets/emily-xzL8sDL2.svg",
  Akane: "https://chatscope.io/storybook/react/assets/akane-MXhWvx63.svg",
  Eliot: "https://chatscope.io/storybook/react/assets/eliot-JNkqSAth.svg",
  Zoe: "https://chatscope.io/storybook/react/assets/zoe-E7ZdmXF0.svg",
};

const defaultMessage = [];
const defaultMessagePatrik = [];
const totalMessages = [defaultMessage, defaultMessagePatrik];

const defaultConversation = [
  {
    info: "",
    lastSenderName: "bloodstrawberry",
    name: "bloodstrawberry",
    src: AVATAR_IMAGE,
    status: "available",
  },
  {
    info: "",
    lastSenderName: "Patrik",
    name: "Patrik",
    src: "https://chatscope.io/storybook/react/assets/patrik-yC7svbAR.svg",
    status: "invisible",
  },
];

const ChatUI = () => {
  const location = useLocation();
  const [loginID, setLoginID] = useState("");

  const [activeID, setActiveID] = useState(0);
  const [messages, setMessages] = useState(totalMessages);

  const getMessageComponent = (totalMessages) => {
    let data = totalMessages[activeID];

    return data.map((item, index) => {
      if(item.type !== "separator") {
        item.model.direction = item.avatar.name === loginID ? "outgoing" : "incoming";
      }
  
      return item.type === "separator" ? (
        <MessageSeparator key={index} content={item.content} />
      ) : (
        <Message key={index} model={item.model}>
          {item.avatar && item.model.direction === "incoming" ? (
            <Avatar src={item.avatar.src} name={item.avatar.name} />
          ) : null}
        </Message>
      );
    });
  };

  const changeRoom = (index) => {
    if(activeID === index) return;
    
    let leftSeparator = {
      type : "separator",
      content : `${loginID} has left the chatroom.`,
    }

    let enterSeparator = {
      type : "separator",
      content : `${loginID} has entered the chatroom.`,
    }

    let temp = [...totalMessages];
    temp[activeID].push(leftSeparator);
    temp[index].push(enterSeparator);

    socketIO.emit("sendMessage", { message: leftSeparator, roomID: activeID });
    socketIO.emit("leave", activeID);
    socketIO.emit("enter", index);
    socketIO.emit("sendMessage", { message: enterSeparator, roomID: index });

    setMessages(temp);
    setActiveID(index);
  }

  const getConversationComponent = (data) => {
    return data.map((item, index) => {
      return (
        <Conversation
          key={index}
          active={index === activeID}
          info={item.info}
          lastSenderName={item.lastSenderName}
          name={item.name}
          onClick={() => changeRoom(index)}
        >
          <Avatar name={item.name} src={item.src} status={item.status} />
        </Conversation>
      );
    });
  };

  const handleSend = (input) => {
    let newMessage = {
      model: {
        message: input,
        // direction: "outgoing",
      },
      avatar: {
        src: AVATAR_MAP[loginID],
        name: loginID,
      },
    };

    let temp = [...totalMessages];
    temp[activeID].push(newMessage);
    setMessages(temp);

    socketIO.emit("sendMessage", { message: newMessage, roomID: activeID });
  };

  const init = () => {
    socketIO.connect();
    
    setLoginID(location.state.loginID);
    // socketIO.emit("login", location.state.loginID);
    socketIO.auth = { loginID : location.state.loginID };

    let enterSeparator = {
      type : "separator",
      content : `${location.state.loginID} has entered the chatroom.`,
    }

    let temp = [...totalMessages];
    temp[activeID].push(enterSeparator);

    setMessages(temp);    
    socketIO.emit("enter", activeID);
    socketIO.emit("sendMessage", { message: enterSeparator, roomID: activeID });
  };

  const respondMessageCallback = (data) => {
    let { message, roomID } = data;

    let temp = [...totalMessages];
    temp[roomID].push(message);
    setMessages(temp);   
  }

  const logout = () => {
    window.alert("로그아웃 되었습니다.");
    window.location.href = "/";
  }

  useEffect(() => {
    if (!socketIO) return;
    socketIO.on("disconnect", logout);
    socketIO.on("respondMessage", respondMessageCallback);  
    return () => {
      socketIO.off("respondMessage", respondMessageCallback);            
    };
  }, []);

  useEffect(init, []);

  useEffect(() => {
    socketIO.on("connect_error", (err) => {
      console.error(err);
      if (err.message === "Login ID is undefined!") {
        console.log("비정상 로그인!");
      } else if (err.message === "Login ID already exists!") {
        window.alert(err.message);
        window.location.href = "/";
      }
    });
  }, []);

  return (
    <div>
      <Conversation
        info="I'm fine, thank you, and you?"
        lastSenderName={loginID}
        name={loginID}
      >
        <Avatar name={loginID} src={AVATAR_MAP[loginID]} status="available" />
      </Conversation>
      <MessageSeparator style={{ marginTop: 5, marginBottom: 5 }} />
      <MainContainer
        responsive
        style={{
          height: "300px",
        }}
      >
        <Sidebar position="left">
          <ConversationList>
            {getConversationComponent(defaultConversation)}
          </ConversationList>
        </Sidebar>
        <ChatContainer>
          <ConversationHeader>
            <ConversationHeader.Back />
            <Avatar
              name={defaultConversation[activeID].name}
              src={defaultConversation[activeID].src}
            />
            {activeID === 0 ? (
              <ConversationHeader.Content
                info="Active 10 mins ago"
                userName="bloodstrawberry"
              />
            ) : (
              <ConversationHeader.Content
                info="Active 7 hours ago"
                userName="Patrik"
              />
            )}
            <ConversationHeader.Actions>
              <VoiceCallButton />
              <VideoCallButton />
              <InfoButton />
            </ConversationHeader.Actions>
          </ConversationHeader>
          <MessageList>
            {getMessageComponent(messages)}
          </MessageList>

          <MessageInput placeholder="Type message here" onSend={handleSend} />
        </ChatContainer>
      </MainContainer>
    </div>
  );
};

export default ChatUI;
반응형

댓글