프로젝트 진행 중 멘토님의 추가 기능 제안을 받았다. 이에 따라, 기존의 여론조사 웹사이트에 websocket을 활용한 채팅봇 기능을 추가하고자 한다.
먼저, 실시간 채팅 구현을 위해 build.gradle에 다음 의존성을 포함시켜야 한다.
// websocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'
의존성 추가 후, WebSocketConfig 클래스를 생성하여 웹소켓 설정을 한다.
@Configuration // 스프링 프레임 워크 설정 클래스로 등록
@EnableWebSocketMessageBroker // 웹소켓 서버 활성화하는데 사용한다.
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// => 웹소켓 설정을 사용자 정의한다.
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/chatMessage");
//chatMessage 로 시작하는 목적지 메시지 관리
config.setApplicationDestinationPrefixes("/app");
//목적지가 /app 시작 메시지 라우팅
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket");
// URL을 웹소켓 엔드포인트로 등록한다. 클라이언트는 이 URL을 통해 웹소켓 연결을 초기화
}
}
다음으로, /chat 주소로의 GET 요청 시 해당 페이지를 반환하는 컨트롤러가 필요하다.
@Controller
public class ChatViewController {
@GetMapping("/chat")
public String chatView(){
return "chat";
}
}
채팅에 사용될 HTML 페이지를 작성한다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>채팅 페이지</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link href="/css/main.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.min.js"></script>
<script src="/js/app.js"></script>
</head>
<body>
<div id="main-content" class="container">
<h2 class="text-center">채팅 페이지</h2>
<div class="row">
<div class="col-md-6">
<form class="form-inline">
<div class="form-group">
<label for="chatConnect">채팅 연결:</label>
<button id="chatConnect" class="btn btn-primary" type="submit">연결</button>
<button id="chatDisconnect" class="btn btn-danger" type="submit" disabled="disabled">연결 끊기</button>
</div>
</form>
</div>
<div class="col-md-6">
<form class="form-inline">
<div class="form-group">
<label for="name">채팅 메시지:</label>
<input type="text" id="name" class="form-control" placeholder="메시지를 입력하세요...">
<button id="messageSend" class="btn btn-success" type="submit">전송</button>
</div>
</form>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<table id="conversation" class="table table-bordered">
<thead>
<tr>
<th class="text-center">채팅 시작</th>
</tr>
</thead>
<tbody id="message">
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
우선 기능은 연결을 하는 기능, 연결 끊는 기능, 메시지 처리하는 기능을 추가할려구 한다.
각 버튼을 클릭할때 js를 실행 하도록 설계 한다.
const client = new StompJs.Client({
brokerURL: 'ws://192.168.1.77:8080/gs-guide-websocket'
});
client.onConnect = (frame) => {
setConnected(true);
console.log('Connected: ' + frame);
client.subscribe('/chatMessage', (chating) => {
showChating(JSON.parse(chating.body).content);
});
};
client.onWebSocketError = (error) => {
console.error('Error with websocket', error);
};
client.onStompError = (frame) => {
console.error('Broker reported error: ' + frame.headers['message']);
console.error('Additional details: ' + frame.body);
};
function setConnected(connected) {
$("#chatConnect").prop("disabled", connected);
$("#chatDisconnect").prop("disabled", !connected);
if (connected) {
$("#conversation").show();
}
else {
$("#conversation").hide();
}
$("#message").html("");
}
function connect() {
client.activate();
}
function disconnect() {
client.deactivate();
setConnected(false);
console.log("Disconnected");
}
function sendMessage() {
client.publish({
destination: "/app/chat",
body: JSON.stringify({'name': $("#name").val()})
});
}
function showChating(message) {
$("#message").append("<tr><td>" + message + "</td></tr>");
}
$(function () {
$("form").on('submit', (e) => e.preventDefault());
$("#chatConnect").click(() => connect());
$("#chatDisconnect").click(() => disconnect());
$("#messageSend").click(() => sendMessage());
});
$(function () {
$("form").on('submit', (e) => e.preventDefault());
$("#chatConnect").click(() => connect());
$("#chatDisconnect").click(() => disconnect());
$("#messageSend").click(() => sendMessage());
});
해당 버튼에 대한 함수로 이동된다.
function connect() {
client.activate();
}
function disconnect() {
client.deactivate();
setConnected(false);
console.log("Disconnected");
}
connect()는 웹소켓 연결을 활성화 시킨다.
disconnect()는 웹소켓 연결을 비활성화 시킨다.
function sendMessage() {
client.publish({
destination: "/app/chat",
body: JSON.stringify({'name': $("#name").val()})
});
}
접속자가 입력한 메시지를 서버의 /app/chat으로 전송한다.
function setConnected(connected) {
$("#chatConnect").prop("disabled", connected);
$("#chatDisconnect").prop("disabled", !connected);
if (connected) {
$("#conversation").show();
}
else {
$("#conversation").hide();
}
$("#message").html("");
}
client.onConnect = (frame) => {
setConnected(true);
console.log('Connected: ' + frame);
client.subscribe('/chatMessage', (chating) => {
showChating(JSON.parse(chating.body).content);
});
};
setConnected(true)로 html의 websocket의 연결 상태를 업데이트한다.
서버에서 /chatMessage로 오는 메시지를 받기 위해 준비한다.
subscride 하면 /chatMessage로 오는 메시지를 실시간으로 받을 수 있다.
메시지 전송 처리를 위한 로직이 필요하다.
@Controller
public class ChatController {
@MessageMapping("/chat")
@SendTo("/chatMessage")
public ChatingContent greeting(ChatMessageDto message) throws Exception {
Thread.sleep(100);
return new ChatingContent( HtmlUtils.htmlEscape(message.getChatingMessage()) );
}
}
-> @MessageMapping("/chat")
@SendTo("/chatMessage")
/api/chat으로 오는 메시지를 /chatMessage 주소로 전송
message.getChatingMessage() -> 메시지의 내용을 가져온 다음에
htmlUtils.htmlEscape를 이용해서 html문자를 안전하게 변환 한다.
=> 스크립트 인젝션 공격으로 데이터 처리
위에 콜백함수에서 잡고 ShowChagin 함수에서 html의 message id에 추가해준다.
