기본 포트를 사용하면 무차별 대입 공격에 당합니다.
프로젝트 발표 당일에 DB가 해킹당해서 당황했었답니다. 

대충 복구하고싶으면 돈 보내라는 짤 (실제)

[SSH 포트 변경]
기본 포트(22)를 수정하세요. 아래는 Ubuntu 20.04 기준 명령어 임다.
sudo nano /etc/ssh/sshd_config (편집기가 열리면 22포트 부분찾아서 수정하세요)
sudo service ssh restart
sudo ufw deny 22 & sudo ufw allow ‘변경 포트‘ 
출처: https://mebadong.tistory.com/34

 

[톰캣 포트 변경]

기본 포트(8080)을 수정하세요.아래는 Ubuntu 20.04 & 톰캣 9.0.31 기준임다.

우분투 톰캣 폴더가 3개라 유의하세요

1) var/lib/tomcat9/conf (O)

2) etc/tomcat9 (O)

3) usr/share/tomcat9 (X)

1번과 2번은 동일한 폴더입니다. 둘이 연결돼있더군요.  

sudo nano var/lib/tomcat9/conf/server.xml (편집기가 열리면 Connector Port쪽에 8080을 찾아서 수정하세요)

sudo ufw allow ‘변경 포트‘

sudo systemctl restart tomcat9 (톰캣 재시작해야 포트 수정한거 적용되요)

출처: https://geundung.dev/79

 

[MariaDB 기본 포트 변경]

기본 포트(3030)을 수정하세요. 아래는 MariaDB 10.3.38 기준임다.

sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf (편집기가 열리면 3030포트 찾아서 수정하세요)

sudo systemctl restart mariadb

sudo ufw allow ‘변경 포트‘

출처: https://velog.io/@shin6949/mariaDB-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95-Ubuntu

 
 
수정 다 했으면 클라우드 홈페이지에서 바뀐 포트 반영하세요! 
이상있으면 netstat -tnlp 명령어로 포트가 잘 수정됐는지 확인해보세요. 

뷰 소개

 

home.jsp 는 수정된 부분이 없습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<body>
<h1>대화방</h1>
<h2>사용중인 id는 "${my_id}" 입니다.</h2>
<div id="chatLog">
<%
    List<chatLog> list = (List<chatLog>) request.getAttribute("list");
        for(chatLog l : list){
        pageContext.setAttribute("l", l);%>
        <p>
        ${l.userId}: ${l.message}
        <c:if test="${l.unread != 0}"><span class="ids" id="${l.chatId}" style="color: green">${l.unread}</span></c:if>
        <c:if test="${l.unread == 0}"><span class="ids" id="${l.chatId}" style="display: none">${l.unread}</span></c:if>
        </p>    
<%} %>
    
</div>
<textarea id="textarea"></textarea>
<input type="button" value="전송" onclick="seqCnctingSaveSend()">
</body>
cs

room.jsp의 <body>태그 부분입니다.

room 컨트롤러에서 모델로 받은 list 변수를 사용합니다.

list 변수에는 접속한 방의 메시지가 저장돼있습니다.

 

메시지 구조는

<p>

메시지 작성자:  메시지

<span class="ids" id="메시지-id">안 읽은 사람의 수</span>

</p>

입니다.

 

class, id 속성은 비실시간 -> 실시간 대화를 위해 사용합니다.

 

유저가 메시지 읽는 상황은 3가지 입니다.

 

1) a 와 b 의 실시간 대화

2) a 와 b 의 비실시간 대화

3) a 와 b의 비실시간 -> 실시간 대화

 

1) a 와 b 의 실시간 대화 란?

둘이 오프라인에서 대화하듯, 온라인에서 동일한 시간에 메시지를 주고받는 상황

 

2) a 와 b 의 비실시간 대화

 

a가 메시지를 보내고, 방을 나간후, b가 방에 들어와서 메시지를 읽는 상황

혹은 b가 메시지를 보내고, 방을 나간후, a가 방에 들어와서 메시지를 읽는 상황

 

3) a 와 b의 비실시간 -> 실시간 대화

 

a가 메시지를 보내고, 아직 방에서 나가지 않은 상태에서, b가 방에 들어와서 메시지를 읽는 상황

(상대방이 칼답, 즉답, 메시지를 보내자마자 바로 읽는 경우 )

 

 

1,2번 상황이 class, id 속성이 필요하지 않은 이유

 

메시지를 화면에 띄울때 표시되는 읽음 표시 숫자는

현재 방에 접속한 사람의 수를 계산해서 결정됩니다.

 

1번 상황의 경우는 모두 방에 접속하였기 때문에, 

현재 방에 접속한 사람의 수는 2입니다.

2를 0으로 바꾸어 화면에 띄웁니다.

 

2번 상황의 경우는 한명만 방에 접속하였기 때문에

현재 방에 접속한 사람의 수는 1입니다.

1은 그대로 화면에 띄웁니다.

 

3번 상황이 class, id 속성이 필요한 이유

1,2번 상황은 접속한 사람의 수를 계산해서 결정됩니다.

그리고 이후에 변동사항이 없습니다.

++) 2번의 경우 상대방이 메시지를 읽으면 읽음 처리 숫자를 -1을 합니다.

그러나, 내가 나중에 방에 들어가서 메시지를 읽을때는 db에서 -1이된 uread칼럼을 가저오기때문에

내가 방에 들어갔을때는 자동으로 변동사항이 반영되어있습니다.

 

하지만 3번의 경우는 a가 메시지를 보내면, a 혼자 방에 있기 때문에, 일단 읽음 처리 숫자가 1 입니다.

그리고 a가 나가지않은 상태에서 b가 방에들어와서 메시지를 읽습니다.

그러면 a는 b가 메시지를 읽은 것을

바로 확인 할 수 있어야 합니다.

 

 2번 상황처럼  db에서 -1이된 uread칼럼을 가저오려면 새로고침을 해야합니다.

하지만, 사용자가 새로고침없이 바로 확인 할수 있어야합니다.

이런 상황을 위해 b는 방에 들어올때 안읽은 메시지가 있는지 확인하고

안읽은 메시지를 특정해서 a에게 알려주어야합니다.

그러므로 id속성에 있는 채팅id를 sock.send로 a에게 보냅니다. 

그리고 a는 전달받은 채팅id로 b가 방금 읽은 메시지를 특정합니다.

그러면 내 화면에서도 그 메시지의 읽음 처리 숫자를 -1해서 0으로 바꿀 수 있습니다.

 

++)  중요★

웹소켓으로 표현된 메시지(chatlog 테이블에서 가져오지 않은)

와 db로 표현된 메시지 (chatlog 테이블에서 가져온)의 구조가 같아야합니다.

 

이제 js부분을 소개하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function seqCnctingSaveSend(){
    var size;
    var seq;
    var data = new FormData();
    data.append("roomId", roomId);
    
    fetch('getChatSeq', {     
        method: 'POST',
        headers: {},  
    }).then(res => res.json())
    .then(res => {seq = res;})
    .then(res =>
        fetch('getCncting', {     
            method: 'POST',
            headers: {},  
            body: data
        }).then(res => res.json())
          .then(res => {if(res == 2){size = 0}else(size = 1)} )
    )
    .then(res => saveMessage(size, seq))
    .then(res => sendMessage(size, seq));
        
}
cs

웹소켓 메시지와 db메시지 구조가 같아야하기 때문에,

saveMessage 함수와 sendMessage 함수는 같은 매개변수를 사용합니다.

if(res == 2){size = 0}은 1번 상황을 위해 사용됩니다.

else(size = 1)는 2번 상황을 위해 사용됩니다.

 

++) then이 잘 작동하려면 res => 를 꼭 써야합니다.

혹은 함수식으로 response인자를 넣어야 합니다.

res => 다음에 오는 중괄호{} 는 if절, 대입 연산자가 있으면

꼭 사용해야합니다.

then(res => res.json())후에 오는

then(res => )에서 res는 res.json() 입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function saveMessage(size, seq){
    var data = new FormData();
    var textarea = document.getElementById("textarea");
    var myMessage = textarea.value;
    
    data.append("message", myMessage);
    data.append("roomId", roomId);
    data.append("chat_id", seq);
    data.append("size", size);
 
    fetch('saveMessage', {     
        method: 'POST',
        headers: {},
        body: data       
    });
}
cs

saveMessage 함수 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void saveMessage(String userid, String message, String roomId, int chatId, int size) {
        String sql = "insert into chatLog(user_id, message, room_id, unread, who_read, chat_id, regdate) values(?, ?, ?, ?, ?, ?, sysdate)";
        
        try {
            conn = dao.getConnection();
            PreparedStatement st = conn.prepareStatement(sql);
            st.setString(1, userid);
            st.setString(2, message);
            st.setString(3, roomId);
            st.setInt(4, size);
            st.setString(5, userid);
            st.setInt(6, chatId);
            st.executeUpdate();
        
            st.close();
            conn.close();
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
cs

service 클래스의 saveMessage 메소드 입니다.

중요한점은 who_read 칼럼에 작성자 이름을 남기면서, 메시지를 저장합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var chatLog = document.getElementById("chatLog");     
        var content = JSON.parse(e.data);
        var message = content.message;
        var type = content.type;
        var chatId = content.chatId;
        var writer = content.writer;
        var size = content.size;
        var data = new FormData();
        data.append("roomId", roomId);
        
        if(type == "JOIN"){
            var chat_ids = document.getElementsByClassName("ids");
            
            fetch('getWhoRead', {     
                method: 'POST',
                headers: {},
                body: data       
            }).then(res => res.json())
            .then(whos => {
                 for(var i=0; i<chat_ids.length; i++){        
                     if(!(whos[i].whoRead).includes(my_id)){  
                         if(parseInt(chat_ids[i].innerHTML) == 0)
                             continue;
                         chat_ids[i].innerHTML = parseInt(chat_ids[i].innerHTML) - 1;
                         chat_ids[i].innerHTML = "";
                        /*방에 접속해있는 모든 사람들(나 포함)에게 내가 이 채팅을 읽지 않았다는것을 보내준다. 그리고 if(type =="READ")에서 나를 제외한 사람들의 읽음 표시를 -1 한다  */
                        sock.send(JSON.stringify({chatRoomId: roomId, type: "READ", chatId: chat_ids[i].id, writer: my_id}));                  
                        decreaseUnread(chat_ids[i].id);                  
                        leaveId(chat_ids[i].id);
                    }
                 }                  
            })
            
        }
cs

 sock.onmessage 부분의  if(type == "JOIN") 부분입니다.

 

getWhoRead 클래스에서 chatlog테이블의 who_read 칼럼 리스트(접속해있는 방)를 가져옵니다.

getWhoRead 클래스는 Java파일에서는 List를 반환(return)합니다.

ajax로 이 List를 받게되면 문자열로 표현된 배열의 인덱스마다 객체(js object)가 넣어져서 옵니다.

현재 접속한 방의 첫번째 메시지부터 끝까지, who_read 칼럼에 내 이름이 포함되어있는지 확인합니다.

포함되어있지 않다면, 내가 메시지를 안 읽었던 것 이므로, 내화면에서 읽음 처리 숫자를 -1합니다.

내가 메시지를 읽었다는 것을 상대방에게 알리기위해

안읽은 메시지 수만큼 sock,send(type: "READ", chatId: chat_ids[i].id)를 합니다.

그리고 db에 unread 칼럼을 -1합니다.

who_read 칼럼에 내 id를 남깁니다.

 

++) 읽음 처리 숫자가 0이면, 메시지 안 읽음 검사를 건너뜁니다.

1
2
3
4
5
6
7
8
if(type == "READ"){
            if(!(writer == my_id)){
                 var chat_element = document.getElementById(chatId);
                   chat_element.innerHTML = parseInt(chat_element.innerHTML) - 1;
                if(parseInt(chat_element.innerHTML) == 0)
                    chat_element.innerHTML = "";
            }
        }    
cs

 sock.onmessage 부분의  if(type == "READ") 부분입니다.

sock,send(type: "READ")는 나를 포함해서 방에 접속해있는 모두에게 메시지를 전달합니다.

그러므로 메시지 전송자가 나면 코드가 실행되지 않게합니다.

코드가 실행되면, 전달받은 chatId로 상대방의 읽지않은 메시지를 특정해서 읽음처리 숫자를 -1합니다.

 

github.com/burnaby033/multiRoomV2

 

burnaby033/multiRoomV2

스프링 다중채팅방, 메신저 읽음 표시, 1 사라짐. Contribute to burnaby033/multiRoomV2 development by creating an account on GitHub.

github.com

프로젝트 다운 주소입니다.

자바 패키지 소개

 

 

자바 폴더의 패키지 구조입니다.

 

before 스프링부트 다중채팅방 코드
after 스프링부트 다중채팅방 코드

chat 패키지의 수정된 부분은 ChatRoomRepository 클래스입니다.

chatRoomMap 변수에 public static이 붙었습니다.

 

++) 이 포스팅은 스프링부트 다중 채팅방 프로젝트를 기반으로합니다.

스프링부트 다중 채팅방 프로젝트 다운 및 설명 주소입니다.

kidmeokgu0.tistory.com/7?category=0

 

스프링부트 다중 채팅방 만들기 (2)

뷰 관점에서 본 코드 흐름 컨트롤러 패키지 구조입니다. 뷰 폴더 구조입니다. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.dms.controller; import java.util.Collection..

kidmeokgu0.tistory.com

controller 패키지는

1)  login 컨트롤러(클래스) 추가되었습니다.

2) room 컨트롤러(클래스)가 변경되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.dms.controller;
 
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
 
@Controller
@RequestMapping("/multiRoom")
public class login {
 
    @GetMapping("/login")
    public String homeController(Model model, HttpServletRequest request) {
 
        return "login";
    }
}
 
cs

controller 패키지의 login 클래스 입니다.

++)login 뷰(jsp)로 이동합니다.

로그인 화면(로그인 뷰 실행화면)입니다.

 

 완료버튼을 누르면 loginPcs 클래스로 입력한 아이디가 전달됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.dms.process;
 
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
@RequestMapping("/multiRoom")
public class loginPcs {
 
    @GetMapping("/loginPcs")
    public void loginPcsRc(HttpServletRequest request, HttpServletResponse response) throws IOException {
        HttpSession session = request.getSession();
        String my_id = request.getParameter("my_id");
        session.setAttribute("my_id", my_id);
        response.sendRedirect("/multiRoom/home");
    }
}
 
cs

process패키지의 loginPcs 클래스입니다.

 

HttpSession에 전달받은 id를 등록합니다.

그리고 home 컨트롤러로 이동합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.dms.controller;
 
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.dms.entity.chatLog;
import com.dms.service.service;
 
@Controller
@RequestMapping("/multiRoom")
public class room {
 
    @GetMapping("/room")
    public String roomController(Model model, HttpServletRequest request) {
        service service = new service();
        HttpSession session = request.getSession();
        String roomId = request.getParameter("id");
        String my_id = (String) session.getAttribute("my_id");
        List<chatLog> list = service.getChatlog(roomId);
        
        model.addAttribute("roomId", roomId);
        model.addAttribute("my_id", my_id);
        model.addAttribute("list", list);
 
        return "room";
    }
}
 
cs

my_id 변수에 HttpSession에 등록된 id를 저장합니다.

my_id 변수를 모델에 등록합니다.

roomId 변수를 통해 service 클래스에 있는 getChatlog 메소드를 실행합니다.

그리고 실행결과를 list 변수에 넣습니다.

list 변수를 모델에 등록합니다.

 

ajax 패키지 구조
룸 뷰 화면

룸에서 메시지를 전송하면 saveMessage 클래스가 실행됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.dms.ajax;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.dms.service.service;
 
@RestController
@RequestMapping("/multiRoom")
public class saveMessage {
 
    @PostMapping("/saveMessage")
    public void saveMessageAjax(Model model, HttpServletRequest request) {
        
        service service = new service();
        HttpSession session = request.getSession();
        
        String my_id = (String) session.getAttribute("my_id");
        String message = request.getParameter("message");
        String roomId = request.getParameter("roomId");
        int chatId = Integer.parseInt(request.getParameter("chat_id"));
        int size = Integer.parseInt(request.getParameter("size"));
 
        service.saveMessage(my_id, message, roomId, chatId, size);                    
        
    }
}
 
cs

saveMessage 클래스 입니다.

db에 메시지를 저장합니다.

 

chat_id 변수는 채팅 id입니다.

룸 뷰에서 getChatSeq 클래스를 연결하여 가져옵니다.(ajax)

그리고 saveMessage 클래스로 보냅니다.

 

size 변수는 방에 접속해있는 사람의 수입니다.

룸 뷰에서 getCncting 클래스를 연결하여 가져옵니다.(ajax)

그리고 saveMessage 클래스로 보냅니다.

 

my_id 변수는 누가 메시지를 보냈는지를 구분하기 위해 사용합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.dms.ajax;
 
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.dms.service.service;
 
@RestController
@RequestMapping("/multiRoom")
public class getChatSeq {
    
    @PostMapping("/getChatSeq")
    public int getChatSeqAjax(HttpServletResponse response) throws IOException {
        service service = new service(); 
        int chatSeq = service.getChatSeq();
        return chatSeq;
    }
}
 
cs

getChatSeq 클래스입니다.

채팅 id를 가져옵니다.

채팅 id는 오라클 db의 시퀀스입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.dms.ajax;
 
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.dms.chat.ChatRoomRepository;
 
@RestController
@RequestMapping("/multiRoom")
public class getCncting {
 
    @PostMapping("/getCncting")
    public int getLiveCntAjax(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String roomId = request.getParameter("roomId");
        int size = ChatRoomRepository.chatRoomMap.get(roomId).getSessions().size();
        return size;
    }
    
}
 
cs

getCncting 클래스입니다.

룸 뷰에서 방 번호(roomId)를 보낸걸 받습니다.

 이 방 번호(roomId)를 사용하여,

ChatRoomRepository 클래스의 chatRoomMap 변수에 저장된 chatRoom 클래스를 찾습니다.

 

++) chatRoomMap의 key는 roomId입니다.

그리고 value는 chatRoom 클래스입니다.

그러므로 map의 메소드인 get(key)을 사용하면 chatRoom 클래스를 찾을 수 있습니다.

그리고 찾은 chatRoom 클래스의 sessions 변수에 등록된 유저들의 수를 구합니다.

 

ChatRoomRepository, chatRoom 클래스의 관계는 스프링 부트 다중 채팅방 만들기

포스팅에 소개해놓았습니다.

 

db 구조

chatlog 테이블의 구조와 데이터 타입

unread 는 안읽은 사람의 수를 저장하는 칼럼입니다.

 

who_read는 메시지를 읽은 사람의 id를 저장하는 칼럼입니다.

 

 

 

gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
plugins {
    id 'org.springframework.boot' version '2.3.4.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}
 
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
 
repositories {
    mavenCentral()
    mavenLocal()
}
 
 dependencies {
 
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation('org.springframework.boot:spring-boot-starter-test'
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-websocket')
    compile('org.webjars:sockjs-client:1.1.2')
    compile('org.webjars:webjars-locator:0.30')
    runtime('org.springframework.boot:spring-boot-devtools')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    compile('org.springframework.boot:spring-boot-starter-test')
    compile('org.apache.tomcat.embed:tomcat-embed-jasper')
 
 
}
 
test {
    useJUnitPlatform()
}
 
cs

 

다음 포스팅은 뷰 소개 입니다.

 

+ Recent posts