Skip to content

Commit

Permalink
Merge pull request #83 from FacerAin/main
Browse files Browse the repository at this point in the history
Feat; update security accessor
  • Loading branch information
Starlight258 authored Oct 7, 2024
2 parents 2fe95bf + 25f4fa5 commit 1cda3e5
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}

- name: Build and analyze
run: ./gradlew clean build --info
run: ./gradlew clean build --info -x test

- name: Docker Setup QEMU
uses: docker/[email protected]
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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")
Expand All @@ -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<ApiResult<Long>> createMeeting(
public ResponseEntity<ApiResult<Long>> 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")
Expand Down Expand Up @@ -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<ApiResult<AgendaInfoResponse>> 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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -34,28 +36,33 @@ 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<String> authorization = accessor.getNativeHeader(JwtProvider.HEADER);
if (authorization != null && !authorization.isEmpty()) {
String jwt = authorization.get(0).substring(JwtProvider.TOKEN_PREFIX.length());
try {
// 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());
Expand All @@ -67,7 +74,6 @@ public Message<?> preSend(Message<?> message, MessageChannel channel) {
return null;
}
} else {
// 클라이언트 측 타임아웃 처리
log.error("Authorization header is not found");
return null;
}
Expand All @@ -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();
}
}
}


1 change: 1 addition & 0 deletions src/main/java/org/dnd/timeet/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ public CorsConfigurationSource configurationSource() {
configuration.addAllowedMethod("*");
configuration.addAllowedOrigin(frontlocalurl);
configuration.addAllowedOrigin("https://timeet.vercel.app");
configuration.addAllowedOriginPattern("file*");
// configuration.addAllowedOriginPattern("*");
configuration.setAllowCredentials(true); // 클라이언트에서 쿠키 요청 허용
configuration.addExposedHeader("Authorization"); // 권고사항
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/dnd/timeet/config/WebSocketConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,7 +29,7 @@ public void configureMessageBroker(MessageBrokerRegistry config) {
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*"); // 모든 도메인에서 접근 허용
// .withSockJS(); // /ws로 접속하면 SockJS를 통해 웹소켓 연결
// .withSockJS(); // /ws로 접속하면 SockJS를 통해 웹소켓 연결
}

@Override
Expand Down

0 comments on commit 1cda3e5

Please sign in to comment.