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 @@ -9,7 +9,6 @@
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -29,7 +28,6 @@ public class ChatMessageController implements ChatMessageApi {
* Destination Queue: /pub/chat.message.{chatRoomId}를 통해 호출 후 처리 되는 로직
*/
@Override
@PreAuthorize("#chatRoomAccessChecker.hasPermission(#chatRoomId, principal)")
@MessageMapping("chat.room.{chatRoomId}/message")
public void sendMessage(UserPrincipal principal,
@DestinationVariable Long chatRoomId,
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/rabbitmqprac/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
Expand All @@ -39,6 +40,7 @@
@Configuration
@EnableWebSecurity
@ConditionalOnDefaultWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.rabbitmqprac.domain.persistence.usersession.entity.UserStatus;
import com.rabbitmqprac.global.helper.RabbitPublisher;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -34,6 +35,7 @@ public class ChatMessageService {
private final ChatMessageRepository chatMessageRepository;
private final RabbitPublisher rabbitPublisher;

@PreAuthorize("@chatRoomMemberAuthorityValidator.isMember(#chatRoomId, #userId)")
@Transactional
public void sendMessage(Long userId, Long chatRoomId, ChatMessageReq req) {
User user = entityFacade.readUser(userId);
Expand All @@ -45,6 +47,7 @@ public void sendMessage(Long userId, Long chatRoomId, ChatMessageReq req) {
sendMessage(chatMessage, unreadMemberCnt, chatRoom);
}

@PreAuthorize("@chatRoomMemberAuthorityValidator.isMember(#chatRoomId)")
@Transactional(readOnly = true)
public List<ChatMessageDetailRes> readChatMessagesBefore(Long chatRoomId, Long lastChatMessageId, int size) {
List<ChatMessage> chatMessages = chatMessageRepository.findByChatRoomIdBefore(
Expand Down Expand Up @@ -137,16 +140,19 @@ private void sendMessage(ChatMessage chatMessage, int unreadMemberCnt, ChatRoom
rabbitPublisher.publish(chatRoom.getId(), chatMessageRes);
}

@PreAuthorize("@chatRoomMemberAuthorityValidator.isMember(#chatRoomId)")
@Transactional(readOnly = true)
public Optional<ChatMessage> readLastChatMessage(Long chatRoomId) {
return chatMessageRepository.findTopByChatRoomIdOrderByCreatedAtDesc(chatRoomId);
}

@PreAuthorize("@chatRoomMemberAuthorityValidator.isMember(#chatRoomId)")
@Transactional(readOnly = true)
public int countUnreadMessages(Long chatRoomId, Long lastReadMessageId) {
return chatMessageRepository.countByChatRoomIdAndIdGreaterThan(chatRoomId, lastReadMessageId);
}

@PreAuthorize("@chatRoomMemberAuthorityValidator.isMember(#chatRoomId)")
@Transactional(readOnly = true)
public List<ChatMessageDetailRes> readChatMessagesBetween(Long userId, Long chatRoomId, Long from, Long to) {
List<ChatMessage> chatMessages = chatMessageRepository.findByChatRoomIdAndIdBetween(chatRoomId, from, to);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

@RequiredArgsConstructor
public enum UserErrorCode implements BaseErrorCode {
/* 403 FORBIDDEN */
FORBIDDEN(StatusCode.FORBIDDEN, ReasonCode.ACCESS_TO_THE_REQUESTED_RESOURCE_IS_FORBIDDEN, "사용자에게 권한이 없습니다."),

/* 404 NOT FOUND */
NOT_FOUND(StatusCode.NOT_FOUND, ReasonCode.REQUESTED_RESOURCE_NOT_FOUND, "회원을 찾을 수 없습니다."),
CONFLICT_USERNAME(StatusCode.CONFLICT, ReasonCode.RESOURCE_ALREADY_EXISTS, "이미 존재하는 유저 아이디입니다");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.rabbitmqprac.global.advice;

import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.rabbitmqprac.domain.context.user.exception.UserErrorCode;
import com.rabbitmqprac.global.exception.CustomValidationException;
import com.rabbitmqprac.global.exception.GlobalErrorException;
import com.rabbitmqprac.global.exception.payload.CausedBy;
Expand All @@ -14,6 +15,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
Expand Down Expand Up @@ -126,6 +128,19 @@ protected ErrorResponse handleAccessDeniedException(AccessDeniedException e) {
return ErrorResponse.of(causedBy.getCode(), causedBy.getReason(), e.getMessage());
}

/**
* @PreAuthorize 에서 검증 실패 시 발생하는 AuthorizationDeniedException 처리
*/
@ResponseStatus(HttpStatus.FORBIDDEN)
@ExceptionHandler(AuthorizationDeniedException.class)
protected ErrorResponse handleAuthorizationDeniedException(AuthorizationDeniedException e) {
log.warn("handleAuthorizationDeniedException : {}", e.getMessage());
UserErrorCode errorCode = UserErrorCode.FORBIDDEN;
CausedBy causedBy = errorCode.causedBy();

return ErrorResponse.of(causedBy.getCode(), causedBy.getReason(), errorCode.getExplainError());
}

/**
* API 호출 시 객체 혹은 파라미터 데이터 값이 유효하지 않은 경우
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.rabbitmqprac.infra.security.authorityvalidator;

import com.rabbitmqprac.domain.context.user.exception.UserErrorCode;
import com.rabbitmqprac.domain.context.user.exception.UserErrorException;
import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails;
import org.springframework.security.core.Authentication;

public abstract class AuthorityValidator {
protected SecurityUserDetails isAuthenticated(Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
throw new UserErrorException(UserErrorCode.FORBIDDEN);
}

return (SecurityUserDetails) authentication.getPrincipal();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.rabbitmqprac.infra.security.authorityvalidator;

import com.rabbitmqprac.domain.context.chatroommember.service.ChatRoomMemberService;
import com.rabbitmqprac.infra.security.authentication.SecurityUserDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Component("chatRoomMemberAuthorityValidator")
@RequiredArgsConstructor
public class ChatRoomMemberAuthorityValidator extends AuthorityValidator {
private final ChatRoomMemberService chatRoomMemberService;

public boolean isMember(Long chatRoomId) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SecurityUserDetails userDetails = super.isAuthenticated(authentication);
Long currentUserId = userDetails.getUserId();

return chatRoomMemberService.isExists(chatRoomId, currentUserId);
}

/**
* STOMP는 HTTP와 달리 SecurityContextHolder에서 인증 정보를 가져올 수 없으므로, userId를 직접 받아서 검증한다.
*/
public boolean isMember(Long chatRoomId, Long userId) {
return chatRoomMemberService.isExists(chatRoomId, userId);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.rabbitmqprac.infra.security.common.registry;
package com.rabbitmqprac.infra.security.registry;

import com.rabbitmqprac.infra.stomp.exception.StompErrorCode;
import com.rabbitmqprac.infra.stomp.exception.StompErrorException;
import com.rabbitmqprac.infra.security.registry.checker.ChatRoomAccessChecker;
import com.rabbitmqprac.infra.security.registry.checker.StompAuthorityChecker;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import javax.swing.text.html.Option;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
Expand All @@ -18,16 +17,16 @@
*/
@RequiredArgsConstructor
@Component
public class ResourceAccessRegistry {
private final Map<Pattern, ResourceAccessChecker> checkers = new HashMap<>();
public class ResourceCheckerRegistry {
private final Map<Pattern, StompAuthorityChecker> checkers = new HashMap<>();
private final ChatRoomAccessChecker chatRoomAccessChecker;

@PostConstruct
public void setCheckers() {
registerChecker("^/exchange/chat\\.exchange/room\\.\\d+$", chatRoomAccessChecker);
}

public void registerChecker(final String pathPattern, final ResourceAccessChecker checker) {
public void registerChecker(final String pathPattern, final StompAuthorityChecker checker) {
checkers.put(Pattern.compile(pathPattern), checker);
}

Expand All @@ -38,7 +37,7 @@ public void registerChecker(final String pathPattern, final ResourceAccessChecke
* @return ResourceAccessChecker : path에 대한 체커
* @throws IllegalArgumentException : 해당 경로에 대한 체커가 없는 경우
*/
public Optional<ResourceAccessChecker> getChecker(final String path) {
public Optional<StompAuthorityChecker> getChecker(final String path) {
return checkers.entrySet().stream()
.filter(entry -> entry.getKey().matcher(path).matches())
.map(Map.Entry::getValue)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.rabbitmqprac.infra.security.common.registry;
package com.rabbitmqprac.infra.security.registry.checker;

import com.rabbitmqprac.domain.context.chatroommember.service.ChatRoomMemberService;
import lombok.RequiredArgsConstructor;
Expand All @@ -10,7 +10,7 @@
@Slf4j
@Component("chatRoomAccessChecker")
@RequiredArgsConstructor
public class ChatRoomAccessChecker implements ResourceAccessChecker {
public class ChatRoomAccessChecker implements StompAuthorityChecker {
private final ChatRoomMemberService chatRoomMemberService;

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.rabbitmqprac.infra.security.common.registry;
package com.rabbitmqprac.infra.security.registry.checker;

import java.security.Principal;

/**
* 리소스 접근 권한을 확인하는 인터페이스
*/
public interface ResourceAccessChecker {
public interface StompAuthorityChecker {
/**
* 리소스에 대한 접근 권한을 확인한다.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.rabbitmqprac.infra.stomp.handler.command.subscribe;

import com.rabbitmqprac.domain.context.usersession.service.UserSessionService;
import com.rabbitmqprac.infra.security.common.registry.ResourceAccessRegistry;
import com.rabbitmqprac.infra.security.registry.ResourceCheckerRegistry;
import com.rabbitmqprac.infra.stomp.exception.StompErrorCode;
import com.rabbitmqprac.infra.stomp.exception.StompErrorException;
import lombok.RequiredArgsConstructor;
Expand All @@ -14,7 +14,7 @@
@Component
@RequiredArgsConstructor
public class ChatRoomAuthorizeHandler implements SubscribeCommandHandler {
private final ResourceAccessRegistry resourceAccessRegistry;
private final ResourceCheckerRegistry resourceCheckerRegistry;
private final UserSessionService userSessionService;

private static final String USER_EXCHANGE_PREFIX = "/user";
Expand All @@ -29,7 +29,7 @@ public void handle(Message<?> message, StompHeaderAccessor accessor) {
}

Long chatRoomId = extractChatRoomId(destination);
resourceAccessRegistry.getChecker(destination).ifPresent(checker -> {
resourceCheckerRegistry.getChecker(destination).ifPresent(checker -> {
if (checker.hasPermission(chatRoomId, accessor.getUser())) {
Long userId = Long.parseLong(accessor.getUser().getName());
log.info("[Exchange 권한 검사] userId={}에 대한 {} 권한 검사 통과", userId, destination);
Expand Down
Loading