해결된 질문
작성
·
224
0
헌재 상황은 NGINX로 HTTPS로 백엔드를 배포하고 있고 리액트는 로컬에서 작업 중인데 알람 기능을 구현할 때 OPTIONS가 CORS에러를 일으킵니다.
```
@Service
@RequiredArgsConstructor
@Log4j2
@Transactional
public class NotificationService {
// SSE 이벤트 타임아웃 시간
private static final Long DEFAULT_TIMEOUT = 24L * 60 * 60 * 1000; // SSE 연결 타임아웃 (1일)
private final CustomNotificationRepository customNotificationRepository;
private final MemberRepository memberRepository;
private final CommunityRepository communityRepository;
private final NotificationRepository notificationRepository;
// 메시지 알림
public SseEmitter subscribe(String memberEmail, String lastEventId) throws Exception {
// 회원 조회
MemberEntity findMember = memberRepository.findByMemberEmail(memberEmail);
// 매 연결마다 고유 이벤트 ID 부여
String eventId = makeTimeIncludeId(findMember);
log.info("eventId {} ", eventId);
// SseEmitter 생성후 Map에 저장
SseEmitter sseEmitter = customNotificationRepository.save(eventId, new SseEmitter(DEFAULT_TIMEOUT));
// 사용자에게 모든 데이터가 전송되었다면 emitter 삭제
sseEmitter.onCompletion(() -> {
log.info("onCompletion callback");
customNotificationRepository.deleteById(eventId);
});
// Emitter의 유효 시간이 만료되면 emitter 삭제
// 유효 시간이 만료되었다는 것은 클라이언트와 서버가 연결된 시간동안 아무런 이벤트가 발생하지 않은 것을 의미한다.
sseEmitter.onTimeout(() -> {
log.info("onTimeout callback");
customNotificationRepository.deleteById(eventId);
});
// 503 에러를 방지하기 위한 더미 이벤트 전송
sendToClient(eventId, sseEmitter, "알림 서버 연결 성공 [memberId = " + findMember.getMemberId() + "]");
// 클라이언트가 미수신한 Event 목록이 존재할 경우 전송하여 Event 유실을 예방
if (hasLostData(lastEventId)) {
sendLostData(lastEventId, findMember.getMemberId(), sseEmitter);
}
return sseEmitter;
}
private static @NotNull String makeTimeIncludeId(MemberEntity findMember) {
String eventId = findMember.getMemberId() + "_" + System.currentTimeMillis();
return eventId;
}
private void sendToClient(String eventId, SseEmitter sseEmitter, Object data) {
try {
sseEmitter.send(SseEmitter.event()
.name("connect")
.id(eventId)
.data(data));
} catch (IOException e) {
customNotificationRepository.deleteById(eventId);
throw new RuntimeException("알림 서버 연결 오류");
}
}
private boolean hasLostData(String lastEventId) {
return !lastEventId.isEmpty();
}
private void sendLostData(String lastEventId,
Long memberId,
SseEmitter sseEmitter) {
Map<String, Object> eventCaches = customNotificationRepository.findAllEventCacheStartWithByMemberId(memberId);
eventCaches.entrySet().stream()
.filter(entry -> lastEventId.compareTo(entry.getKey()) < 0)
.forEach(entry -> sendToClient(entry.getKey(), sseEmitter, entry.getValue()));
}
...
이렇게 서비스를 구현하고
@RestController
@RequestMapping("/api/v1/notify")
@RequiredArgsConstructor
@Log4j2
public class NotificationController implements NotificationControllerDocs {
private final NotificationService notificationService;
// 메시지 알림
// SSE 통신을 위해서는 produces로 반환할 데이터 타입을 "text/event-stream"으로 해주어야 함
@GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
public SseEmitter subscribe(@AuthenticationPrincipal UserDetails userDetails,
@RequestHeader(value = "last-event-id", required = false, defaultValue = "") final String lastEventId,
HttpServletResponse response) {
try {
if (userDetails == null) {
throw new IllegalArgumentException("인증 정보가 필요합니다.");
}
response.setHeader("Connection", "keep-alive");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("X-Accel-Buffering", "no");
String email = userDetails.getUsername();
SseEmitter responseEmitter = notificationService.subscribe(email, lastEventId);
log.info("Subscribed to email: " + email);
log.info("response: " + responseEmitter);
return responseEmitter;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
...
NGINX 설정
server {
listen 443 ssl;
server_name meettify.store;
ssl_certificate /etc/letsencrypt/live/meettify.store/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/meettify.store/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# SSE용 헤더 설정
location /api/v1/notify/subscribe {
# OPTIONS preflight 요청을 처리하도록 설정
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'http://localhost:5173' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
add_header 'Access-Control-Max-Age' 3600 always;
return 204;
}
proxy_pass https://meettify.store; # Spring Boot 서버로 요청 전달
proxy_http_version 1.1;
proxy_set_header Connection ''; # SSE 연결 유지
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding off;
# SSE 연결 타임아웃 방지
proxy_set_header Cache-Control no-cache;
proxy_read_timeout 86400s;
keepalive_timeout 86400s; # 필요에 따라 시간 조정
add_header Access-Control-Allow-Origin "http://localhost:5173";
add_header Access-Control-Allow-Credentials "true";
}
# 기본 프록시 설정 (백엔드 애플리케이션)
location / {
proxy_pass http://ubuntu-api-1:8080;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Connection '';
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding off;
}
이렇게 엔진엑스 설정까지 한다음
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:5173")
.allowedHeaders("Authorization", "Content-Type")
.allowCredentials(true)
.exposedHeaders("Cache-Control", "Content-Type", "X-Accel-Buffering")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS");
}
}
이렇게 설정했는데 계속 CORS에러가 발생합니다.
해당 문제가 지속적으로 반복중입니다. ㅠㅠ
답변 1
0
안녕하세요, 인프런 AI 인턴입니다.
유요한님께서 겪고 계신 CORS 문제는 보통 서버와 클라이언트 간의 도메인이 다를 때 발생합니다. 현 상황에서는 NGINX 설정과 Spring Boot의 CORS 설정에서 문제가 발생할 수 있습니다. 아래 몇 가지 확인 사항과 해결 방법을 제안드립니다.
NGINX 설정 확인:
location
블록 내의 if ($request_method = 'OPTIONS') { ... }
가 제대로 작동하고 있는지 확인하세요. 예를 들어, add_header
명령어가 제대로 동작하는지, return
등 다른 설정이 선행되지는 않는지 확인해 보세요.proxy_pass https://meettify.store;
에서 프로토콜이 http://
인지 https://
인지 재확인하시고 맞게 설정되어 있는지 확인하세요.Spring Boot CORS 설정:
WebMvcConfigurer
에서 설정한 allowedOrigins
는 http://localhost:5173
인데, NGINX에서 allow-origin
헤더도 http://localhost:5173
로 설정되어 있는지 확인해보세요.allowedOrigins
를 모두 허용하는 "*"
로 설정해 일시적으로 문제가 해결되는지 확인해 보세요.Spring Boot 로그 확인:
확장 옵션:
Network
탭에서 OPTIONS 요청과 응답의 헤더 정보를 자세히 확인하여 문제를 파악할 수 있습니다.추가로, 외부 URL로 요청을 보내지 않는 일부 경우에는 CORS 설정을 생략할 수도 있음을 유념하세요. 하지만 개발 환경에서는 항상 보안 설정을 적절히 유지하는 것이 중요합니다.
이와 관련된 다른 자료들을 참조하고 문제가 해결되는지 확인해주세요. 더 궁금한 점이 있으면 언제든지 문의하세요.