Skip to content

Commit

Permalink
웹소켓 채팅 리팩터링 (#160)
Browse files Browse the repository at this point in the history
* feat: 웹소켓 채팅 리팩터링

* feat: 채팅 롤백 Path 롤백 및 인터셉터 수정

* refactor: job skill 수정

* refactor: auth 인증부분 분리

* fix: WebsocketPlayerArgumentResolver 패키지 위치 수

---------

Co-authored-by: waterricecake <[email protected]>
  • Loading branch information
thdwoqor and waterricecake authored Dec 26, 2024
1 parent 0cb1952 commit 501f1d4
Show file tree
Hide file tree
Showing 12 changed files with 210 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package mafia.mafiatogether.chat.ui;

import java.util.List;
import lombok.RequiredArgsConstructor;
import mafia.mafiatogether.chat.annotation.SendToChatWithRedis;
import mafia.mafiatogether.chat.application.ChatService;
Expand All @@ -15,8 +16,6 @@
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@Controller
@RequiredArgsConstructor
public class ChatController {
Expand Down Expand Up @@ -56,5 +55,4 @@ public Message createChat(
return chatService.chat(name, code, request.content());
}


}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package mafia.mafiatogether.common.config;

import lombok.RequiredArgsConstructor;
import mafia.mafiatogether.common.resolver.PlayerArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
Expand All @@ -10,11 +11,14 @@
import java.util.List;

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

private final PlayerArgumentResolver playerArgumentResolver;

@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new PlayerArgumentResolver());
resolvers.add(playerArgumentResolver);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package mafia.mafiatogether.common.config;


import java.util.List;
import lombok.RequiredArgsConstructor;
import mafia.mafiatogether.common.interceptor.StompChannelInterceptor;
import mafia.mafiatogether.common.interceptor.ChatInterceptor;
import mafia.mafiatogether.common.interceptor.PathMatcherInterceptor;
import mafia.mafiatogether.common.resolver.WebsocketPlayerArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
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 @@ -15,7 +19,8 @@
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

private final StompChannelInterceptor stompChannelInterceptor;
private final ChatInterceptor chatInterceptor;
private final WebsocketPlayerArgumentResolver websocketPlayerArgumentResolver;

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
Expand All @@ -28,6 +33,11 @@ public void registerStompEndpoints(StompEndpointRegistry registry) {
);
}

@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(websocketPlayerArgumentResolver);
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/pub");
Expand All @@ -36,7 +46,12 @@ public void configureMessageBroker(MessageBrokerRegistry registry) {

@Override
public void configureClientInboundChannel(ChannelRegistration registry) {
registry.interceptors(stompChannelInterceptor);
registry.interceptors(
new PathMatcherInterceptor(chatInterceptor)
.includePathPattern("/sub/chat/**", StompCommand.SUBSCRIBE)
.includePathPattern("/pub/chat/**", StompCommand.MESSAGE)
.includePathPattern("/pub/chat/**", StompCommand.SEND)
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import lombok.RequiredArgsConstructor;
import mafia.mafiatogether.common.util.AuthExtractor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
Expand All @@ -16,8 +15,7 @@

@Component
@Configuration
@RequiredArgsConstructor
public class StompChannelInterceptor implements ChannelInterceptor {
public class ChatInterceptor implements ChannelInterceptor {

private static final String SUBSCRIBE_FORMAT = "%s/%s";
private static final String PUBLISHING_FORMAT = "%s/%s/%s";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package mafia.mafiatogether.common.interceptor;

import java.util.ArrayList;
import java.util.List;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
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.util.AntPathMatcher;
import org.springframework.util.PathMatcher;

public class PathMatcherInterceptor implements ChannelInterceptor {

private final ChannelInterceptor channelInterceptor;
private final PathMatcher pathMatcher;
private final List<StompMapping> includePathPatterns;
private final List<StompMapping> excludePathPatterns;

public PathMatcherInterceptor(final ChannelInterceptor channelInterceptor) {
this.channelInterceptor = channelInterceptor;
this.pathMatcher = new AntPathMatcher();
this.includePathPatterns = new ArrayList<>();
this.excludePathPatterns = new ArrayList<>();
}

@Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) {
StompHeaderAccessor headerAccessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

if (headerAccessor != null &&
headerAccessor.getDestination() != null &&
shouldIntercept(headerAccessor.getDestination(), headerAccessor.getCommand())) {
return channelInterceptor.preSend(message, channel);
}

return ChannelInterceptor.super.preSend(message, channel);
}

private boolean shouldIntercept(String destination, StompCommand command) {
boolean isExcluded = excludePathPatterns.stream()
.anyMatch(stompMapping -> matchesPathAndCommand(destination, command, stompMapping));

boolean isIncluded = includePathPatterns.stream()
.anyMatch(stompMapping -> matchesPathAndCommand(destination, command, stompMapping));

return isIncluded && !isExcluded;
}

private boolean matchesPathAndCommand(String destination, StompCommand command, StompMapping stompMapping) {
return pathMatcher.match(stompMapping.destination(), destination) &&
stompMapping.command() == command;
}

public PathMatcherInterceptor includePathPattern(String targetPath, StompCommand command) {
this.includePathPatterns.add(new StompMapping(targetPath, command));
return this;
}

public PathMatcherInterceptor excludePathPattern(String targetPath, StompCommand command) {
this.excludePathPatterns.add(new StompMapping(targetPath, command));
return this;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package mafia.mafiatogether.common.interceptor;

import org.springframework.messaging.simp.stomp.StompCommand;

public record StompMapping(
String destination,
StompCommand command
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package mafia.mafiatogether.common.resolver;

import lombok.RequiredArgsConstructor;
import mafia.mafiatogether.common.exception.AuthException;
import mafia.mafiatogether.common.exception.ExceptionCode;
import mafia.mafiatogether.common.util.AuthExtractor;
import org.springframework.stereotype.Component;

@Component
public class BasicAuthResolver {

public String[] resolve(final String authorization) {
if (authorization == null) {
throw new AuthException(ExceptionCode.MISSING_AUTHENTICATION_HEADER);
}
if (!authorization.startsWith("Basic")) {
throw new AuthException(ExceptionCode.MISSING_AUTHENTICATION_HEADER);
}

return AuthExtractor.extractByAuthorization(authorization);
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package mafia.mafiatogether.common.resolver;

import jakarta.servlet.http.HttpServletRequest;

import mafia.mafiatogether.common.util.AuthExtractor;
import lombok.RequiredArgsConstructor;
import mafia.mafiatogether.common.annotation.PlayerInfo;
import mafia.mafiatogether.common.exception.AuthException;
import mafia.mafiatogether.common.exception.ExceptionCode;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
@RequiredArgsConstructor
public class PlayerArgumentResolver implements HandlerMethodArgumentResolver {

private final BasicAuthResolver basicAuthResolver;

@Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.hasParameterAnnotation(PlayerInfo.class);
Expand All @@ -23,17 +27,12 @@ public boolean supportsParameter(final MethodParameter parameter) {
public PlayerInfoDto resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) {
HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class);

if (httpServletRequest == null) {
throw new AuthException(ExceptionCode.NOT_FOUND_REQUEST);
}

String authorization = httpServletRequest.getHeader("Authorization");
if (authorization == null) {
throw new AuthException(ExceptionCode.MISSING_AUTHENTICATION_HEADER);
}

String[] information = AuthExtractor.extractByAuthorization(authorization);
String[] information = basicAuthResolver.resolve(authorization);
return new PlayerInfoDto(information[0], information[1]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package mafia.mafiatogether.common.resolver;

import lombok.RequiredArgsConstructor;
import mafia.mafiatogether.common.annotation.PlayerInfo;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class WebsocketPlayerArgumentResolver implements HandlerMethodArgumentResolver {

private final BasicAuthResolver basicAuthResolver;

@Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.hasParameterAnnotation(PlayerInfo.class);
}

@Override
public Object resolveArgument(final MethodParameter parameter, final Message<?> message) throws Exception {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.wrap(message);
String authorization = headerAccessor.getFirstNativeHeader("Authorization");
String[] information = basicAuthResolver.resolve(authorization);

return new PlayerInfoDto(information[0], information[1]);
}

}
31 changes: 20 additions & 11 deletions src/main/java/mafia/mafiatogether/job/ui/JobController.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package mafia.mafiatogether.job.ui;


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Base64;
import lombok.RequiredArgsConstructor;
import mafia.mafiatogether.chat.annotation.SendToChatWithRedis;
import mafia.mafiatogether.chat.domain.Message;
import mafia.mafiatogether.common.annotation.PlayerInfo;
import mafia.mafiatogether.common.resolver.PlayerInfoDto;
import mafia.mafiatogether.job.application.JobService;
import mafia.mafiatogether.job.application.dto.request.JobExecuteAbilityRequest;
import mafia.mafiatogether.job.application.dto.response.JobExecuteAbilityResponse;
import mafia.mafiatogether.job.application.dto.response.JobResponse;
import mafia.mafiatogether.job.application.dto.response.JobResultResponse;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -25,22 +27,29 @@
@RequestMapping("/jobs")
public class JobController {

private final StringRedisTemplate stringRedisTemplate;
private final ObjectMapper objectMapper;
private final JobService jobService;

@GetMapping("/my")
public ResponseEntity<JobResponse> getJob(@PlayerInfo PlayerInfoDto playerInfoDto) {
return ResponseEntity.ok(jobService.getPlayerJob(playerInfoDto.code(), playerInfoDto.name()));
}

@MessageMapping("/skill/{code}/{name}")
@SendToChatWithRedis("/sub/mafia/{code}")
public Message executeSkill(
@DestinationVariable("code") String code,
@DestinationVariable("name") String name,
@MessageMapping("/jobs/skill")
public void executeWebSocketSkill(
@PlayerInfo PlayerInfoDto playerInfoDto,
@Payload JobExecuteAbilityRequest request
) {
JobExecuteAbilityResponse response = jobService.executeSkill(code, name, request);
return Message.ofChat(response.job(), response.result());
) throws JsonProcessingException {
JobExecuteAbilityResponse response = jobService.executeSkill(playerInfoDto.code(), playerInfoDto.name(),
request);
String auth = Base64.getEncoder()
.encodeToString((playerInfoDto.code() + ":" + playerInfoDto.name()).getBytes());

stringRedisTemplate.convertAndSend(
String.format("/sub/job/skill/%s/%s", response.job().toLowerCase(), auth),
objectMapper.writeValueAsString(response)
);
}

@PostMapping("/skill")
Expand Down
Loading

0 comments on commit 501f1d4

Please sign in to comment.