Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,119 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.springframework.stereotype.Component;

import org.springframework.stereotype.Component;
import org.springframework.data.redis.core.StringRedisTemplate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
//
// @Slf4j // ⭐️ SLF4j 로그 사용을 위해 추가
// @Component
// public class CertificationSessionManager {
// private final Map<Long, Set<Long>> sessionUserMap = new ConcurrentHashMap<>();
//
// public void openSession(Long sessionId) {
// sessionUserMap.put(sessionId, ConcurrentHashMap.newKeySet());
// // ⭐️ 로그 추가
// log.info("✅ New certification session opened. SessionID: {}", sessionId);
// }
//
// public void addUserToSession(Long sessionId, Long userId) {
// Set<Long> users = sessionUserMap.computeIfAbsent(sessionId, k -> {
// log.warn("Attempted to add user to a non-existent session. Creating new set for SessionID: {}", k);
// return ConcurrentHashMap.newKeySet();
// });
//
// boolean isAdded = users.add(userId);
//
// // ⭐️ 요청하신 멤버 추가 확인 로그
// if (isAdded) {
// log.info("👤 User added to session. SessionID: {}, UserID: {}. Current participants: {}",
// sessionId, userId, users.size());
// } else {
// log.info("👤 User already in session. SessionID: {}, UserID: {}. Current participants: {}",
// sessionId, userId, users.size());
// }
// }
//
// public int getCurrentUserCount(Long sessionId) {
// return sessionUserMap.getOrDefault(sessionId, Set.of()).size();
// }
//
// public boolean hasUser(Long sessionId, Long userId) {
// return sessionUserMap.getOrDefault(sessionId, Set.of()).contains(userId);
// }
//
// public List<Long> snapshotUserIds(Long sessionId) {
// return List.copyOf(sessionUserMap.getOrDefault(sessionId, Set.of()));
// }
//
//
//
// public void removeSession(Long sessionId) {
// sessionUserMap.remove(sessionId);
// // ⭐️ 로그 추가
// log.info("❌ Certification session removed. SessionID: {}", sessionId);
// }
// }
@Component
@RequiredArgsConstructor
public class CertificationSessionManager {
private final Map<Long, Set<Long>> sessionUserMap = new ConcurrentHashMap<>();

// RedisTemplate을 주입받습니다.
private final StringRedisTemplate redisTemplate;

// 세션 ID를 위한 KEY를 만드는 헬퍼 메서드
private String getKey(Long sessionId) {
return "certification:session:" + sessionId;
}

public void openSession(Long sessionId) {
sessionUserMap.put(sessionId, ConcurrentHashMap.newKeySet());
String key = getKey(sessionId);
// 세션을 연다는 것은 키를 만드는 것과 같습니다.
// addUserToSession에서 자동으로 키가 생성되므로 이 메서드는 비워두거나,
// 만료 시간 설정 등 초기화 로직을 넣을 수 있습니다.
// 예: 10분 후 만료
redisTemplate.expire(key, 10, TimeUnit.MINUTES);
}

public void addUserToSession(Long sessionId, Long userId) {
sessionUserMap.computeIfAbsent(sessionId, k -> ConcurrentHashMap.newKeySet()).add(userId);
String key = getKey(sessionId);
// Redis의 Set 자료구조에 userId를 추가합니다.
redisTemplate.opsForSet().add(key, String.valueOf(userId));
}

public int getCurrentUserCount(Long sessionId) {
return sessionUserMap.getOrDefault(sessionId, Set.of()).size();
String key = getKey(sessionId);
// Redis Set의 크기를 반환합니다.
Long size = redisTemplate.opsForSet().size(key);
return size != null ? size.intValue() : 0;
}

public boolean hasUser(Long sessionId, Long userId) {
return sessionUserMap.getOrDefault(sessionId, Set.of()).contains(userId);
String key = getKey(sessionId);
// Redis Set에 해당 멤버가 있는지 확인합니다.
return redisTemplate.opsForSet().isMember(key, String.valueOf(userId));
}

public List<Long> snapshotUserIds(Long sessionId) {
return List.copyOf(sessionUserMap.getOrDefault(sessionId, Set.of()));
String key = getKey(sessionId);
// Redis Set의 모든 멤버를 가져옵니다.
Set<String> members = redisTemplate.opsForSet().members(key);
if (members == null) {
return List.of();
}
return members.stream()
.map(Long::valueOf)
.collect(Collectors.toList());
}

public void removeSession(Long sessionId) {
sessionUserMap.remove(sessionId);
String key = getKey(sessionId);
// 세션 키 자체를 삭제합니다.
redisTemplate.delete(key);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,27 @@

import lombok.RequiredArgsConstructor;

@EnableWebSocketMessageBroker
@Configuration
@RequiredArgsConstructor
public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer {

private final StompAuthChannelInterceptor stompAuthChannelInterceptor;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
config.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOriginPatterns("*"); // 클라이언트 WebSocket 연결 주소
// .setAllowedOriginPatterns("http://10.0.2.2:8080", "ws://10.0.2.2:8080");// CORS 허용
}

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(stompAuthChannelInterceptor);
}

}
// @EnableWebSocketMessageBroker
// @Configuration
// @RequiredArgsConstructor
// public class CertifyWebSocketConfig implements WebSocketMessageBrokerConfigurer {
//
// private final StompAuthChannelInterceptor stompAuthChannelInterceptor;
// @Override
// public void configureMessageBroker(MessageBrokerRegistry config) {
// config.enableSimpleBroker("/certification"); // 인증현황을 받아보기 위한 구독 주소
// config.setApplicationDestinationPrefixes("/app"); // 클라이언트가 인증 요청을 보내는 주소
// }
//
// @Override
// public void registerStompEndpoints(StompEndpointRegistry registry) {
// registry.addEndpoint("/ws-certify").setAllowedOriginPatterns("*"); // 클라이언트 WebSocket 연결 주소
// // .setAllowedOriginPatterns("http://10.0.2.2:8080", "ws://10.0.2.2:8080");// CORS 허용
// }
//
// @Override
// public void configureClientInboundChannel(ChannelRegistration registration) {
// registration.interceptors(stompAuthChannelInterceptor);
// }
//
// }
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,61 @@
public class StompAuthChannelInterceptor implements ChannelInterceptor {

private final JwtUtil jwtUtil;

@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
log.info("StompCommand: {}", accessor.getCommand()); // StompCommand 로그 추가

if (StompCommand.CONNECT.equals(accessor.getCommand())) {
log.info("CONNECT command received.");
// 프론트에서 connect 시 Authorization 헤더 넣어야 함
String authHeader = accessor.getFirstNativeHeader("Authorization");
log.info("Authorization Header: {}", authHeader); // Authorization 헤더 로그 추가

if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = jwtUtil.getTokenFromHeader(authHeader);
log.info("Extracted Token: {}", token); // 추출된 토큰 로그 추가

// JwtUtil 이용해서 Authentication 복원
Authentication authentication = jwtUtil.getAuthentication(token);
log.info("Authentication restored: {}", authentication); // 복원된 인증 정보 로그 추가

// WebSocket 세션에 Authentication(UserPrincipal) 저장
// ⭐️ 이 부분을 수정
accessor.setUser(authentication);
log.info("User principal set on accessor.");
} else {
log.warn("Authorization header is missing or not in Bearer format.");

// ⭐️ 추가: 메시지 헤더에도 Authentication 정보 저장
accessor.setHeader(StompHeaderAccessor.USER_HEADER, authentication);

log.info("Authentication set: {}", authentication);
}
} else if (StompCommand.SEND.equals(accessor.getCommand())) {
// SEND 명령어에 대한 로그 추가 (메시지 전송 시)
Object payload = message.getPayload();
log.info("SEND command received. Destination: {}, Payload: {}", accessor.getDestination(), payload);
}

return message;
}

// @Override
// public Message<?> preSend(Message<?> message, MessageChannel channel) {
// StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
// log.info("StompCommand: {}", accessor.getCommand()); // StompCommand 로그 추가
//
// if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// log.info("CONNECT command received.");
// // 프론트에서 connect 시 Authorization 헤더 넣어야 함
// String authHeader = accessor.getFirstNativeHeader("Authorization");
// log.info("Authorization Header: {}", authHeader); // Authorization 헤더 로그 추가
//
// if (authHeader != null && authHeader.startsWith("Bearer ")) {
// String token = jwtUtil.getTokenFromHeader(authHeader);
// log.info("Extracted Token: {}", token); // 추출된 토큰 로그 추가
//
// // JwtUtil 이용해서 Authentication 복원
// Authentication authentication = jwtUtil.getAuthentication(token);
// log.info("Authentication restored: {}", authentication); // 복원된 인증 정보 로그 추가
//
// // WebSocket 세션에 Authentication(UserPrincipal) 저장
// accessor.setUser(authentication);
// log.info("User principal set on accessor.");
// } else {
// log.warn("Authorization header is missing or not in Bearer format.");
// }
// } else if (StompCommand.SEND.equals(accessor.getCommand())) {
// // SEND 명령어에 대한 로그 추가 (메시지 전송 시)
// Object payload = message.getPayload();
// log.info("SEND command received. Destination: {}, Payload: {}", accessor.getDestination(), payload);
// }
//
// return message;
// }
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package com.assu.server.domain.certification.controller;

import java.security.Principal;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import com.assu.server.domain.certification.dto.GroupSessionRequest;
import com.assu.server.domain.certification.service.CertificationService;
Expand All @@ -17,32 +23,32 @@
@Slf4j
@Controller // STOMP 메시지 처리를 위한 컨트롤러
@RequiredArgsConstructor
@Component
@RequestMapping("/app")
public class GroupCertificationController {

private final CertificationService certificationService;

@MessageMapping("/certify")
public void certifyGroup(@Payload GroupSessionRequest dto, SimpMessageHeaderAccessor headerAccessor) {
try {
log.info("### SUCCESS ### 인증 요청 메시지 수신 - adminId: {}, sessionId: {}", dto.getAdminId(), dto.getSessionId());

// Authentication에서 Member 정보 추출
Authentication auth = (Authentication) headerAccessor.getUser();
if (auth != null && auth.getPrincipal() instanceof PrincipalDetails) {
PrincipalDetails principalDetails = (PrincipalDetails) auth.getPrincipal();
// 실제 비즈니스 로직 호출
certificationService.handleCertification(dto, principalDetails.getMember());
log.info("### SUCCESS ### 그룹 인증 처리 완료");
public void certifyGroup(@Payload GroupSessionRequest dto,
Principal principal) {
if (principal instanceof UsernamePasswordAuthenticationToken) {
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken)principal;
PrincipalDetails principalDetails = (PrincipalDetails)auth.getPrincipal();

try {
log.info("### SUCCESS ### 인증 요청 메시지 수신 - user: {}, adminId: {}, sessionId: {}",
principalDetails.getUsername(), dto.getAdminId(), dto.getSessionId());

// 헤더를 직접 다룰 필요 없이, 바로 principalDetails 객체를 사용
if (principalDetails != null) {
certificationService.handleCertification(dto, principalDetails.getMember());
log.info("### SUCCESS ### 그룹 인증 처리 완료");
}
} catch (Exception e) {
log.error("### ERROR ### 인증 처리 실패", e);
}
} catch (Exception e) {
log.error("### ERROR ### 인증 처리 실패", e);
}
}

// @MessageMapping("/certify")
// public void certifyGroup(SimpMessageHeaderAccessor headerAccessor) {
// log.info("### DEBUG ### 메서드 진입!");
// log.info("### DEBUG ### User: {}", headerAccessor.getUser());
// log.info("### DEBUG ### SessionId: {}", headerAccessor.getSessionId());
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,4 @@ public class CertificationProgressResponseDTO {
private Integer count;
private String message;
private List<Long> userIds;

// 생성자들
public static CertificationProgressResponseDTO progress(int count) {
return new CertificationProgressResponseDTO("progress", count, null, null);
}

public static CertificationProgressResponseDTO completed(String message, List<Long> userIds) {
return new CertificationProgressResponseDTO("completed", userIds.size(), message, userIds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ public void handleCertification(GroupSessionRequest dto, Member member) {
boolean isDoubledUser= sessionManager.hasUser(sessionId, userId);
if(isDoubledUser) {
messagingTemplate.convertAndSend("/certification/progress/"+sessionId,
new CertificationProgressResponseDTO("progress", 0,"doubled member", null));
new CertificationProgressResponseDTO("progress", null,
"doubled member", sessionManager.snapshotUserIds(sessionId)));
throw new GeneralException(ErrorStatus.DOUBLE_CERTIFIED_USER);
}

Expand All @@ -127,11 +128,8 @@ public void handleCertification(GroupSessionRequest dto, Member member) {
new CertificationProgressResponseDTO("completed", currentCertifiedNumber, "인증이 완료되었습니다.", sessionManager.snapshotUserIds(sessionId)));
} else {
messagingTemplate.convertAndSend("/certification/progress/" + sessionId,
new CertificationProgressResponseDTO("progress", currentCertifiedNumber, null, null));
new CertificationProgressResponseDTO("progress", currentCertifiedNumber, null, sessionManager.snapshotUserIds(sessionId)));
}



}

@Override
Expand Down
Loading