From 25f4fa594fcd3b21148abfba5a09f47b07c54dac Mon Sep 17 00:00:00 2001 From: FacerAin Date: Mon, 7 Oct 2024 00:24:35 +0900 Subject: [PATCH] feat: add security accessor --- build.gradle | 2 +- .../agenda/controller/AgendaController.java | 40 ++++++++++++++----- .../CustomHandshakeInterceptor.java | 36 +++++++++++++++++ .../interceptor/JwtChannelInterceptor.java | 28 ++++++++----- .../dnd/timeet/config/WebSocketConfig.java | 1 + 5 files changed, 85 insertions(+), 22 deletions(-) create mode 100644 src/main/java/org/dnd/timeet/common/interceptor/CustomHandshakeInterceptor.java diff --git a/build.gradle b/build.gradle index 845b851..323756d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.2.1' + id 'org.springframework.boot' version '3.3.2' id 'io.spring.dependency-management' version '1.1.4' id "org.sonarqube" version "4.0.0.2929" id 'checkstyle' diff --git a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java index 7b4e13c..05040bc 100644 --- a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java +++ b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java @@ -15,11 +15,15 @@ import org.dnd.timeet.common.security.CustomUserDetails; import org.dnd.timeet.common.utils.ApiUtils; import org.dnd.timeet.common.utils.ApiUtils.ApiResult; +import org.dnd.timeet.member.domain.Member; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.messaging.Message; import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; -import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; @@ -28,6 +32,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.security.Principal; + @Tag(name = "안건 컨트롤러", description = "Agenda API입니다.") @RestController @RequestMapping("/api/meetings") @@ -39,13 +45,22 @@ public class AgendaController { @Operation(summary = "안건(+쉬는시간) 생성", description = "안건(+쉬는시간)을 생성한다.") @MessageMapping("/meeting/{meeting-id}/agendas/create") @SendTo("/topic/meeting/{meeting-id}/agendas/create") - public ResponseEntity> createMeeting( + public ResponseEntity> createAgenda( @DestinationVariable("meeting-id") Long meetingId, - @RequestBody @Valid AgendaCreateRequest agendaCreateRequest, - @AuthenticationPrincipal CustomUserDetails userDetails) { - Long agendaId = agendaService.createAgenda(meetingId, agendaCreateRequest, userDetails.getMember()); + @Valid AgendaCreateRequest agendaCreateRequest, + Principal principal) { + + if (principal instanceof UsernamePasswordAuthenticationToken) { + UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) principal; + CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); - return ResponseEntity.ok(ApiUtils.success(agendaId)); + // userDetails 객체를 사용하여 작업 수행 + Long agendaId = agendaService.createAgenda(meetingId, agendaCreateRequest, userDetails.getMember()); + return ResponseEntity.ok(ApiUtils.success(agendaId)); + } + + // 인증 정보가 없을 때 + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } @GetMapping("/{meeting-id}/agendas") @@ -78,19 +93,22 @@ public AgendaActionResponse handleAgendaAction(@DestinationVariable("meeting-id" @DeleteMapping("/{meeting-id}/agendas/{agenda-id}") @Operation(summary = "안건 삭제", description = "지정된 ID에 해당하는 안건을 삭제한다.") + @MessageMapping("/meeting/{meeting-id}/agendas/{delete-id}") + @SendTo("/topic/meeting/{meeting-id}/delete/{delete-id}") public ResponseEntity deleteAgenda( - @PathVariable("meeting-id") Long meetingId, - @PathVariable("agenda-id") Long agendaId) { + @DestinationVariable("meeting-id") Long meetingId, + @DestinationVariable("agenda-id") Long agendaId) { agendaService.cancelAgenda(meetingId, agendaId); return ResponseEntity.noContent().build(); } - @PatchMapping("/{meeting-id}/agendas/order") @Operation(summary = "안건 순서 변경", description = "안건의 순서를 변경한다.") + @MessageMapping("/meeting/{meeting-id}/agendas/order") + @SendTo("/topic/meeting/{meeting-id}/agendas/order") public ResponseEntity> changeAgendaOrder( - @PathVariable("meeting-id") Long meetingId, - @RequestBody @Valid AgendaOrderRequest agendaOrderRequest) { + @DestinationVariable("meeting-id") Long meetingId, + @Valid AgendaOrderRequest agendaOrderRequest) { AgendaInfoResponse agendaInfoResponse = agendaService.changeAgendaOrder(meetingId, agendaOrderRequest.getAgendaIds()); return ResponseEntity.ok(ApiUtils.success(agendaInfoResponse)); diff --git a/src/main/java/org/dnd/timeet/common/interceptor/CustomHandshakeInterceptor.java b/src/main/java/org/dnd/timeet/common/interceptor/CustomHandshakeInterceptor.java new file mode 100644 index 0000000..f46d66e --- /dev/null +++ b/src/main/java/org/dnd/timeet/common/interceptor/CustomHandshakeInterceptor.java @@ -0,0 +1,36 @@ +package org.dnd.timeet.common.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.Map; + +@Component +public class CustomHandshakeInterceptor implements HandshakeInterceptor { + + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler wsHandler, Map attributes) throws Exception { + if (request instanceof ServletServerHttpRequest) { + HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication != null) { + attributes.put("user", authentication.getPrincipal()); + } + } + return true; + } + + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler wsHandler, Exception exception) { + } +} diff --git a/src/main/java/org/dnd/timeet/common/interceptor/JwtChannelInterceptor.java b/src/main/java/org/dnd/timeet/common/interceptor/JwtChannelInterceptor.java index 727529b..8f87ee3 100644 --- a/src/main/java/org/dnd/timeet/common/interceptor/JwtChannelInterceptor.java +++ b/src/main/java/org/dnd/timeet/common/interceptor/JwtChannelInterceptor.java @@ -16,7 +16,9 @@ import org.springframework.messaging.simp.stomp.StompCommand; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; @@ -34,10 +36,11 @@ public class JwtChannelInterceptor implements ChannelInterceptor { @Override public Message preSend(Message message, MessageChannel channel) { - StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); - // 연결 요청시 JWT 검증 + StompHeaderAccessor accessor = MessageHeaderAccessor + .getAccessor(message, StompHeaderAccessor.class); + + // 연결 요청 시 JWT 검증 및 인증 정보 설정 if (StompCommand.CONNECT.equals(accessor.getCommand())) { - // Authorization 헤더 추출 List authorization = accessor.getNativeHeader(JwtProvider.HEADER); if (authorization != null && !authorization.isEmpty()) { String jwt = authorization.get(0).substring(JwtProvider.TOKEN_PREFIX.length()); @@ -45,17 +48,21 @@ public Message preSend(Message message, MessageChannel channel) { // JWT 토큰 검증 DecodedJWT decodedJWT = JwtProvider.verify(jwt); Long memberId = decodedJWT.getClaim("id").asLong(); + // 사용자 정보 조회 Member member = userUtilityService.getUserById(memberId); - // 사용자 인증 정보 설정 + // 사용자 인증 정보 생성 CustomUserDetails userDetails = new CustomUserDetails(member); - UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken( - userDetails, null, userDetails.getAuthorities()); + Authentication authentication = + new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + + // 세션 매니저에 사용자 세션 추가 + + accessor.setUser(authentication); SecurityContextHolder.getContext().setAuthentication(authentication); - // 세션 추가 String sessionId = accessor.getSessionId(); sessionManager.addUserSession(sessionId, memberId); log.info("User Added. Active User Count: " + sessionManager.getActiveUserCount()); @@ -67,7 +74,6 @@ public Message preSend(Message message, MessageChannel channel) { return null; } } else { - // 클라이언트 측 타임아웃 처리 log.error("Authorization header is not found"); return null; } @@ -84,8 +90,10 @@ public void afterSendCompletion(Message message, MessageChannel channel, bool String sessionId = accessor.getSessionId(); sessionManager.removeUserSession(sessionId); log.info("User Disconnected. Active User Count: " + sessionManager.getActiveUserCount()); + + // SecurityContextHolder의 컨텍스트 제거 + SecurityContextHolder.clearContext(); } } } - diff --git a/src/main/java/org/dnd/timeet/config/WebSocketConfig.java b/src/main/java/org/dnd/timeet/config/WebSocketConfig.java index b54f7ab..19d1556 100644 --- a/src/main/java/org/dnd/timeet/config/WebSocketConfig.java +++ b/src/main/java/org/dnd/timeet/config/WebSocketConfig.java @@ -5,6 +5,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.security.config.annotation.web.socket.EnableWebSocketSecurity; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;