[SpringBoot/WebSocket+SocketJS] CORS 설정 시 에러 (When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of or..
Tech/트러블슈팅2023. 5. 24. 06:00
728x90
728x90
에러 메세지
When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
원인
CORS설정 시 allowedCredentials true와, allowedOrigins "*"를 같이 사용할 수 없다.
해결
에러 메세지에 친절하게 나와있다.
allowedOrigins("*") 대신 allowedOriginPatterns("*")을 사용하여 해결.
크게 세가지로 분류되는데, Openeing HandShake, Data transfer, Closing HandShake로 나뉜다
웹 소켓 동작과정
HandShake
악수는 두 명의 사람이 만나서, 악수를 요청하고, 악수를 받는다. 총 세 가지의 과정을 거쳐 "악수" 라는 연결이 성립되는 것이다. 이를 통해 클라이언트와 서버간의 연결을 성립하는 과정이 필요하다는 것을 알 수 있다. 핸드쉐이크란 통신이 시작되기 전 두 지점이 확립된 연결을 가지도록 설정하는 것이다.
HandShake
접속 요청은 http로 진행되며, HandShake가 완료되면 웹소켓 프로토콜(WS)로 변경된다.
GET /path HTTP/1.1
Host: ip:port
<!-- 반드시 HTTP버전 1.1이상, GET을 사용 -->
<!-- ip:port/path으로의 접속 ex)localhost:8080/chat-->
Upgrade: websocket
<!-- 프로토콜을 전환하기 위해 사용하는 헤더 -->
<!-- websocket의 값이 없거나 다른 값이면 cross-protocol-attack으로 간주하여 접속 중지 -->
Connection: Upgrade
<!-- 전송이 완료된 후 네트워크 접속을 유지할 것인지에 대한 정보 -->
<!-- 웹소켓 요청 시 반드시 Upgrade -->
<!-- 위의 Upgrade 헤더와 마찬가지로 값이 없거나 다른 값이면 접속 중지 -->
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
<!-- Key : 유효한 요청인지 확인하기 위해 사용하는 키값 -->
<!-- Protocol : 사용하고자 하는 하나 이상의 웹 소켓 프로토콜 지정, 필요한 경우 사용 -->
<!-- Version : 버전 -->
Origin: http://ip:port
<!-- CORS 정책으로 만들어진 헤더 -->
<!-- CSWSH(Cross-Site Websocket Hijacking)과 같은 공격을 피하기 위한 헤더 -->
CORS(교차 출처 리소스 공유)
웹 페이지에서 실행되는 자바스크립트 XMLHttpRequest(XHR) 호출이 출처가 다른 도메인의 리소스와 상호작용할 수 있도록 허용하는 표준 메커니즘
CSWSH(Cross-Site Websocket Hijacking)
Cross domain간 사용 정책인 Origin 헤더에 대한 검증 미흡으로 발생하는 문제로 서버가 Origin을 제대로 검증하지 않는다면 어떤 도메인에서도 사용자의 세션을 통해 WebSocket을 사용할 수 있다.
afterConnectionEstablished - 연결 성공 시 afterConnectionClosed - 연결 종료 시 handleTextMessage - 메세지 송수신 시
답변 등록 시 ALARM 테이블에 아래와 같이 데이터가 추가된다.
등록 시 ALARM 테이블에 데이터가 입력되는 과정은 생략하도록 하겠다. 이제 이정도는 껌이다
IDX : PK USER_ID : 알림 받을 사용자의 PK CODE : 알림 정보 - 대댓글 알림 시 NewComment 등 CODE로 조건을 주어 VIEW를 다르게 할 수 있게 설계 CHECKED : default X이며 사용자가 알림 확인 시 자동으로 삭제처리된다. PREFIX : 알림이 어디서 왔는지. 단순히 DB에 텍스트 형태로 입력시켜 주었다.
EchoHandler
package com.test.test1.chat.util;
(...생략...)
//Dao DI를위해 사용. 없을 경우 Dao = null
// -> Bean으로 등록되어 있어서 따로 스프링의 객체라고 주입시켜줘야함.
@Component
@RequestMapping("/echo")
public class EchoHandler extends TextWebSocketHandler{
@Autowired
private AlarmDao alarmDao;
public void setAlarmDao(AlarmDao alarmDao) {
this.alarmDao = alarmDao;
}
private static final Logger logger = LoggerFactory.getLogger(WebSocketHandler.class);
//로그인 한 인원 전체
private List<WebSocketSession> sessions = new ArrayList<WebSocketSession>();
//클라이언트가 웹 소켓 생성
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
logger.info("Socket 연결");
//웹 소켓이 생성될 때마다 리스트에 넣어줌
sessions.add(session);
}
//JS에서 메세지 받을 때.
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 메시지
for(WebSocketSession single : sessions) {
// 대댓글 때 사용
// String msg = message.getPayload();
// String[] str = msg.split(",");
// //JS에서 원하는대로 send하여 해당 기능 별 알람 구현
// //질문에 답변 달렸을 때(질문자 ID와 제목 들고옴)
// if(str != null && str.length == 2) {
// String id = str[0];
// String q_subject = str[1];
// int count = alarmDao.selectAlarmCount(id); //알람이 존재할 때
// }
//세션아이디
String hsid = (String) single.getAttributes().get("user_id");
//세션값이 같을때, 알람보낼 것이 있을 때만 전송 -> 로그인 한 사용자가 처음으로 알람 받는 순간임
//해당 sendMsg에 DB정보 넣어서 체크 안된 알람 전부 전송하기
if(single.getAttributes().get("user_id").equals(session.getAttributes().get("user_id"))) { //체크 안된 알림들만 담아서 View
List<AlarmDto> dto = new ArrayList<>();
dto = alarmDao.selectAlarm(hsid);
for(AlarmDto alarm : dto) {
int idx = alarm.getIdx();
String prefix = alarm.getPrefix();
String code = alarm.getCode();
if(code.equals("NewPost")) {
code = "답변이 등록되었습니다.";
}
TextMessage sendMsg = new TextMessage("("+idx+")" + prefix + "에 " + code);
single.sendMessage(sendMsg);
}
}
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {//연결 해제
// TODO Auto-generated method stub
logger.info("Socket 끊음");
//웹 소켓이 종료될 때마다 리스트에서 뺀다.
sessions.remove(session);
}
}
alarm.js
/* 02.23 장재호 */
const alarmUL = document.querySelector("#alarmUL");
const alarmI = document.querySelector("#alarmI");
const alarmDiv = document.querySelector("#alarmDiv");
var sock = null;
$(document).ready(function(){
connectWs();
});
//소켓
function connectWs(){
var ws = new SockJS("http://localhost:8080/echo");
sock = ws;
ws.onopen = function() {
console.log("연결완료");
ws.send($('#socketuserID').val());
};
ws.onmessage = function(event) {
/* 받을 알람이 있을 때 */
console.log(event.data);
if(event.data.length>0){
let newAlarm = '';
newAlarm += '<li scope="col">' + event.data + "</li>"
$('#alarmUL').append(newAlarm);
alarmDiv.style.visibility = "visible";
}
};
ws.onclose = function() {
console.log('close');
};
};
/* 알람창 추가 */
alarmI.addEventListener('click', function(){
alarmUL.classList.toggle('visible');
$(this).stop(false, false);
});
alarmUL.addEventListener('click', function(e){
var endIdx = e.target.textContent.indexOf(")");
var idx = e.target.textContent.substr(1, endIdx-1);
$.ajax({
url : '/alarmDel',
data : {"idx" : idx},
type : 'post',
success : function(data){
console.log(data);
alert("성공");
}
})
$(e.target).remove();
if(alarmUL.children.length == 0){
alarmDiv.style.visibility = "hidden";
}
})
/* *************************** */
실행되는 순서이다.
1. alarm.jsp를 include받은 페이지 접속 시 alarm.js에 설정된 소켓이 생성되며 EchoHandler의 아래 연결
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
logger.info("Socket 연결");
//웹 소켓이 생성될 때마다 리스트에 넣어줌
sessions.add(session);
}
2. 소켓이 생성 될 때 아래의 send메서드를 통해 세션에 등록되어 있는 USER_ID가 전송된다.(input hidden)