Skip to content
Original file line number Diff line number Diff line change
@@ -1 +1 @@
package com.example.arom1.common.config;import com.example.arom1.common.filter.JwtAuthenticationFilter;import com.example.arom1.common.util.jwt.TokenProvider;import com.example.arom1.service.OAuth2Service;import lombok.RequiredArgsConstructor;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;import org.springframework.security.config.http.SessionCreationPolicy;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.web.SecurityFilterChain;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@EnableWebSecurity@Configuration@RequiredArgsConstructorpublic class WebSecurityConfig { private final AuthenticationConfiguration configuration; private final TokenProvider tokenProvider; private final OAuth2Service oAuth2Service; @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } // 1. 스프링 시큐리티 기능 비활성화 //@Bean //public WebSecurityCustomizer configure() { // return (web) -> web.ignoring() // .requestMatchers(toH2Console()) // H2사용 x -> 수정 필요 // .requestMatchers(new AntPathRequestMatcher("/static/**")); //} // 2, 특정 HTTP 요청에 대한 웹 기반 보안 구성 @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/signup", "/login/refresh").permitAll() .anyRequest().authenticated()) .formLogin(AbstractHttpConfigurer::disable) //폼로그인 사용 X -> 폼로그인은 세션 방식에 적합// .formLogin(formLogin -> formLogin// .loginPage("/login")// .permitAll()// .loginProcessingUrl("/login_proc")// .defaultSuccessUrl("/home")// )// .logout(logout -> logout// .logoutSuccessUrl("/login")// .invalidateHttpSession(true)// ) //필터 추가 (위치 중요) //로그인 필터 -> 로그인 서비스 메서드 변경 (이유: 예외 처리 수월 등) //.addFilterAt(new JwtLoginFilter(authenticationManager(configuration), tokenProvider), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new JwtAuthenticationFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class) //csrf 설정 .csrf(AbstractHttpConfigurer::disable) .sessionManagement((session) -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .oauth2Login(oauth -> // OAuth2 로그인 기능에 대한 여러 설정의 진입점 // OAuth2 로그인 성공 이후 사용자 정보를 가져올 때의 설정을 담당 oauth.userInfoEndpoint(c -> c.userService(oAuth2Service))) .build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); }}
package com.example.arom1.common.config;import com.example.arom1.common.filter.JwtAuthenticationFilter;import com.example.arom1.common.util.jwt.TokenProvider;//import com.example.arom1.service.OAuth2Service;import lombok.RequiredArgsConstructor;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;import org.springframework.security.config.http.SessionCreationPolicy;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.web.SecurityFilterChain;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@EnableWebSecurity@Configuration@RequiredArgsConstructorpublic class WebSecurityConfig { private final AuthenticationConfiguration configuration; private final TokenProvider tokenProvider;// private final OAuth2Service oAuth2Service; @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } // 1. 스프링 시큐리티 기능 비활성화 //@Bean //public WebSecurityCustomizer configure() { // return (web) -> web.ignoring() // .requestMatchers(toH2Console()) // H2사용 x -> 수정 필요 // .requestMatchers(new AntPathRequestMatcher("/static/**")); //} // 2, 특정 HTTP 요청에 대한 웹 기반 보안 구성 @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/signup", "/login/refresh", "/stomp/chat").permitAll() .anyRequest().authenticated()) .formLogin(AbstractHttpConfigurer::disable) //폼로그인 사용 X -> 폼로그인은 세션 방식에 적합// .formLogin(formLogin -> formLogin// .loginPage("/login")// .permitAll()// .loginProcessingUrl("/login_proc")// .defaultSuccessUrl("/home")// )// .logout(logout -> logout// .logoutSuccessUrl("/login")// .invalidateHttpSession(true)// ) //필터 추가 (위치 중요) //로그인 필터 -> 로그인 서비스 메서드 변경 (이유: 예외 처리 수월 등) //.addFilterAt(new JwtLoginFilter(authenticationManager(configuration), tokenProvider), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new JwtAuthenticationFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class) //csrf 설정 .csrf(AbstractHttpConfigurer::disable) .sessionManagement((session) -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS))// .oauth2Login(oauth -> // OAuth2 로그인 기능에 대한 여러 설정의 진입점// // OAuth2 로그인 성공 이후 사용자 정보를 가져올 때의 설정을 담당// oauth.userInfoEndpoint(c -> c.userService(oAuth2Service))) .build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.example.arom1.common.config;


import com.example.arom1.handler.ChatPreHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
Expand All @@ -13,6 +15,8 @@
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

private final ChatPreHandler chatPreHandler;

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
Expand All @@ -22,8 +26,14 @@ public void configureMessageBroker(MessageBrokerRegistry registry) {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS();
.addEndpoint("/stomp/chat")
.setAllowedOriginPatterns("*");
}

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


}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public JwtAuthenticationFilter(TokenProvider tokenProvider) {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//로그인, 회원가입 시 인증 필터 무시
String requestURI = request.getRequestURI();
if (requestURI.equals("/login") || requestURI.equals("/signup") || requestURI.equals("/login/refresh") ) {
if (requestURI.equals("/login") || requestURI.equals("/signup") || requestURI.equals("/login/refresh") || requestURI.equals("/stomp/chat")) {
filterChain.doFilter(request, response);
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public String getEmail(String token) {
}

//parseClaimsJws 메서드가 exception 던짐
private Claims getClaims(String token) {
public Claims getClaims(String token) {
try {
return Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
Expand All @@ -104,4 +104,17 @@ public String resolveToken(HttpServletRequest request) {
}
return null;
}

// JWT 토큰 추출
public String extractJwt(String authorizationHeader) {
// Authorization 헤더 값이 "Bearer <token>" 형식일 때
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
// "Bearer " 접두사 제거하고 토큰 추출
return authorizationHeader.substring(7);
}
// 토큰이 없거나 형식이 올바르지 않을 때 null 반환
return null;
}


}
Original file line number Diff line number Diff line change
@@ -1,38 +1,43 @@
package com.example.arom1.controller;

import com.example.arom1.common.util.jwt.TokenProvider;
import com.example.arom1.dto.ChatMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Controller;

@Controller
@RequiredArgsConstructor
public class ChatController {
private final SimpMessageSendingOperations simpMessageSendingOperations;
private final TokenProvider tokenProvider;

@MessageMapping("/chat/{id}/Message")
public void sendMessage(@DestinationVariable("id") Long id, ChatMessage chatMessage) {
if (isJoin(chatMessage)) {
chatMessage.setMessage(chatMessage.getSender() + "님이 입장하였습니다");
@MessageMapping("/chat/message/{chatroomId}") //websocket "pub/chat/message"로 들어오는 메세지 처리
public void sendMessage(@DestinationVariable Long chatRoomId, ChatMessage chatMessage, @Header("Authorization")String Authorization ) {
String authorization = tokenProvider.extractJwt(Authorization);
Object memberId = tokenProvider.getClaims(authorization).get("memberId");
chatMessage.setSender((String) memberId);

if(isENTER(chatMessage)){
chatMessage.setMessage(chatMessage.getSender() + "님이 입장하셨습니다.");
}
else if (isLeave(chatMessage)) {
chatMessage.setMessage(chatMessage.getSender() + "님이 퇴장하였습니다");

if(isLEAVE(chatMessage)){
chatMessage.setMessage(chatMessage.getSender() + "님이 퇴장하셨습니다.");
}

// 특정 채팅방에 메시지를 브로드캐스트
simpMessageSendingOperations.convertAndSend("/sub/chat/" + chatMessage.getChatroomId(), chatMessage);
simpMessageSendingOperations.convertAndSend("/sub/chat/room" + chatMessage.getChatroomId(), chatMessage);
}

private boolean isJoin(ChatMessage message) {
return message.getMessageType().equals(ChatMessage.MessageType.ENTER);
private boolean isENTER(ChatMessage messageType) {
return messageType.getMessageType().equals(ChatMessage.MessageType.ENTER);
}

private boolean isLeave(ChatMessage message) {
return message.getMessageType().equals(ChatMessage.MessageType.LEAVE);
private boolean isLEAVE(ChatMessage messageType){
return messageType.getMessageType().equals(ChatMessage.MessageType.LEAVE);
}

}
2 changes: 1 addition & 1 deletion arom1/src/main/java/com/example/arom1/dto/ChatMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class ChatMessage {

private MessageType messageType;
private String sender;
private String chatroomId;
private long chatroomId;
private String message;

public enum MessageType{
Expand Down
1 change: 0 additions & 1 deletion arom1/src/main/java/com/example/arom1/entity/ChatRoom.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import lombok.NoArgsConstructor;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

import java.util.ArrayList;
import java.util.List;

Expand Down
60 changes: 60 additions & 0 deletions arom1/src/main/java/com/example/arom1/handler/ChatPreHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.example.arom1.handler;

import com.example.arom1.common.exception.BaseException;
import com.example.arom1.common.util.jwt.TokenProvider;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.RequiredArgsConstructor;
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.stereotype.Component;

import java.security.Principal;
import java.util.Map;

@Component
@RequiredArgsConstructor
public class ChatPreHandler implements ChannelInterceptor {
private final TokenProvider tokenProvider;

@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
// ! 연결 요청시 JWT 검증
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// ! Authorization 헤더 추출
String token = accessor.getFirstNativeHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
try {
// ! JWT 토큰 검증
tokenProvider.validateToken(token);
// 토큰에서 memberId 추출
Claims claims = Jwts.parser()
.parseClaimsJws(token)
.getBody();

Long memberId = Long.parseLong(claims.get("memberId").toString());

// WebSocket 연결 사용자 설정
accessor.setUser(new Principal() {
@Override
public String getName() {
return memberId.toString();
}
});
} catch (BaseException e) {
// JWT 토큰 검증 실패 시 연결 거부
return null;
}
} else {
// 토큰이 없거나 형식이 잘못된 경우 연결 거부
return null;
}
}

return message;
}
}
32 changes: 32 additions & 0 deletions arom1/src/main/java/com/example/arom1/handler/StompHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.example.arom1.handler;


import com.example.arom1.common.util.jwt.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
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.stereotype.Component;

@Component
@RequiredArgsConstructor
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class StompHandler implements ChannelInterceptor {
private final TokenProvider tokenProvider;

//websocket을 통해 들어온 요청이 처리되기 전 실행됨
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
if(StompCommand.CONNECT == accessor.getCommand()) {
String authorization = tokenProvider.extractJwt(accessor.getFirstNativeHeader("Authorization"));
tokenProvider.validateToken(authorization);
}
return message;
}


}
Loading