728x90
반응형
SMALL
  1. WebSocket 설정

WebSocket을 활성화 하기 위해 @EnableWebSocket 어노테이션을 사용합니다.

javaCopy code
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatHandler(), "/chat/{roomId}")
                .setAllowedOrigins("*")
                .withSockJS();
    }

    @Bean
    public WebSocketHandler chatHandler() {
        return new ChatHandler();
    }
}

2. WebSocket Handler 구현

WebSocket 메시지를 처리하는 핸들러를 구현합니다. 아래는 간단한 예시입니다.

javaCopy code
public class ChatHandler extends TextWebSocketHandler {

    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String roomId = getRoomId(session);
        sessions.put(roomId, session);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String roomId = getRoomId(session);
        TextMessage msg = new TextMessage("[" + roomId + "] " + message.getPayload());
        broadcast(roomId, msg);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String roomId = getRoomId(session);
        sessions.remove(roomId);
    }

    private String getRoomId(WebSocketSession session) {
        return session.getUri().getPath().split("/")[2];
    }

    private void broadcast(String roomId, TextMessage message) throws IOException {
        for (WebSocketSession session : sessions.values()) {
            if (session.isOpen() && roomId.equals(getRoomId(session))) {
                session.sendMessage(message);
            }
        }
    }
}

3. 인증 및 권한 부여

스프링 시큐리티를 사용하여 인증과 권한 부여를 구현할 수 있습니다. 아래는 간단한 예시입니다.

javaCopy code
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/api/auth/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .apply(new JwtConfigurer(jwtTokenProvider));
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

4. 토큰 기반 인증

로그인 후에 클라이언트는 서버로부터 JWT 토큰을 받아서 이를 이용하여 채팅방에 접근할 수 있습니다. 아래는 간단한 예시입니다.

@PostMapping("/login")
public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {

    Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                    loginRequest.getUsername(),
                    loginRequest.getPassword()
            )
    );

    SecurityContextHolder.getContext().setAuthentication(authentication);

    String jwt = jwtTokenProvider.generateToken(authentication);
    return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
}

로그인 성공시 JWT 토큰을 반환하는 응답 클래스를 정의합니다.

javaCopy code
public class JwtAuthenticationResponse {

    private String accessToken;

    public JwtAuthenticationResponse(String accessToken) {
        this.accessToken = accessToken;
    }

    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }
}

JWT 토큰을 생성하는 유틸리티 클래스를 구현합니다.

javaCopy code
@Component
public class JwtTokenProvider {

    @Value("${app.jwtSecret}")
    private String jwtSecret;

    @Value("${app.jwtExpirationInMs}")
    private int jwtExpirationInMs;

    public String generateToken(Authentication authentication) {

        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();

        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);

        return Jwts.builder()
                .setSubject(Long.toString(userPrincipal.getId()))
                .setIssuedAt(new Date())
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, jwtSecret)
                .compact();
    }

    public Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(jwtSecret)
                .parseClaimsJws(token)
                .getBody();

        return Long.parseLong(claims.getSubject());
    }

    public boolean validateToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException ex) {
            logger.error("Invalid JWT signature");
        } catch (MalformedJwtException ex) {
            logger.error("Invalid JWT token");
        } catch (ExpiredJwtException ex) {
            logger.error("Expired JWT token");
        } catch (UnsupportedJwtException ex) {
            logger.error("Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            logger.error("JWT claims string is empty.");
        }
        return false;
    }
}

5. 채팅방 구현

WebSocket 세션 ID를 이용하여 각 채팅방을 구분합니다. 채팅방은 각 사용자별로 생성되며, 클라이언트는 채팅방의 ID를 이용하여 특정 채팅방에 접속합니다.

javaCopy code
@RestController
@RequestMapping("/api/chat")
public class ChatController {

    @MessageMapping("/{roomId}")
    @SendTo("/topic/{roomId}")
    public ChatMessage sendMessage(@DestinationVariable String roomId, ChatMessage message) {
        return message;
    }
}

6. 데이터베이스 저장

채팅방에서 주고받은 메시지를 데이터베이스에 저장할 수 있습니다. 메시지 모델과 메시지 레파지

토리를 구현합니다.

javaCopy code
@Repository
public interface MessageRepository extends JpaRepository<Message, Long> {

    List<Message> findAllByRoomIdOrderByCreatedAtAsc(Long roomId);
}

7. UI 구현

웹 브라우저에서 채팅방을 사용할 수 있는 UI를 구현합니다. 아래는 간단한 예시입니다.

htmlCopy code
<!DOCTYPE html>
<html>
<head>
    <title>Chat Room</title>
    <script src="/webjars/sockjs-client/1.0.2/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/2.3.3/stomp.min.js"></script>
    <script>
        var stompClient = null;

        function connect() {
            var socket = new SockJS('/chat');
            stompClient = Stomp.over(socket);
            stompClient.connect({}, function (frame) {
                console.log('Connected: ' + frame);
                stompClient.subscribe('/topic/' + roomId, function (message) {
                    showMessage(JSON.parse(message.body));
                });
            });
        }

        function sendMessage() {
            var message = document.getElementById("message").value;
            stompClient.send("/app/chat/" + roomId, {}, JSON.stringify({content: message}));
            document.getElementById("message").value = "";
        }

        function showMessage(message) {
            var messageArea = document.getElementById("messageArea");
            messageArea.innerHTML += message.content + "\\n";
        }

        window.onload = function () {
            connect();
        };
    </script>
</head>
<body>
<div>
    <h1>Chat Room</h1>
    <p>Room ID: <span id="roomId"></span></p>
    <textarea id="messageArea" rows="10" cols="50"></textarea>
    <br>
    <input type="text" id="message" name="message">
    <button onclick="sendMessage()">Send</button>
</div>
<script>
    var urlParams = new URLSearchParams(window.location.search);
    var roomId = urlParams.get('roomId');
    document.getElementById("roomId").innerHTML = roomId;
</script>
</body>
</html>

 

728x90
반응형
LIST

'개발 > Spring' 카테고리의 다른 글

웹소캣 - 채팅 기본 구현 (테스트 코드 연습)  (0) 2023.04.09
웹소켓 알림 메세지 - session  (0) 2023.04.09
yml 파일이란 ?  (0) 2023.03.12
WebSock 구현  (0) 2023.03.07
JWT 장점과 단점  (0) 2023.02.15
728x90
반응형
SMALL
package com.test.sbp.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.*;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.Map;

//@Configuration
//@EnableWebSocketMessageBroker //STOMP를 사용하기 위한 어노테이션           //@EnableWebSocket : 기본적인 웹소켓 사용을 위한 어노테이션
//public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
//
//    @Override   //TODO:property 분리
//    public void configureMessageBroker(MessageBrokerRegistry registry) {
//        registry.setApplicationDestinationPrefixes("/pub") //서버에서 클라이언트로부터의 메서지를 받을 api의 prefix설정, 아래topic러는 곳에 구독하면, 실제경로는 "topic/pub"
//                .enableStompBrokerRelay("/topic") //SimpleBroker의 기능과 외부 message broker(RabbitMQ, ActiveMQ 등)에 메시지를 전달하는 기능을 가지고 있다.
//                .setRelayHost("localhost")                 // 여기부터 외부브로커인 RabbitMQ를 사용하기위한 설정.
//                .setVirtualHost("/")
//                .setRelayPort(61613)
//                .setClientLogin("guest")
//                .setClientPasscode("guest");
//    }
//
//    @Override
//    public void registerStompEndpoints(StompEndpointRegistry registry) {
//        registry.addEndpoint("/ws")     //엔드포인트는 /ws
//                .setAllowedOrigins("*");
//    }
//}
@RequiredArgsConstructor
@Configuration
@EnableWebSocket
public class WebSockConfig implements WebSocketConfigurer {
    private final WebSocketHandler webSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler, "ws/chat").setAllowedOrigins("*");
    }
}
package com.test.sbp;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.sbp.dto.ChatMessage;
import com.test.sbp.dto.ChatRoom;
import com.test.sbp.service.ChatService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Slf4j
@RequiredArgsConstructor
@Component
public class WebSocketHandler extends TextWebSocketHandler {
    private final ObjectMapper objectMapper;
    private final ChatService chatService;

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        log.info("{}", payload);
        ChatMessage chatMessage = objectMapper.readValue(payload, ChatMessage.class);

        ChatRoom chatRoom = chatService.findRoomById(chatMessage.getRoomId());
        chatRoom.handlerActions(session, chatMessage, chatService);
    }
}
package com.test.sbp.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ChatMessage {
    // 메시지 타입 : 입장, 채팅
    public enum MessageType {
        ENTER, TALK
    }
    private MessageType type; // 메시지 타입
    private String roomId; // 방번호
    private String sender; // 메시지 보낸사람
    private String message; // 메시지
}
package com.test.sbp.dto;

import com.test.sbp.service.ChatService;
import lombok.Builder;
import lombok.Getter;
import org.springframework.web.socket.WebSocketSession;

import java.util.HashSet;
import java.util.Set;

@Getter
public class ChatRoom {
    private String roomId;
    private String name;
    private Set<WebSocketSession> sessions = new HashSet<>();

    @Builder
    public ChatRoom(String roomId, String name) {
        this.roomId = roomId;
        this.name = name;
    }

    public void handlerActions(WebSocketSession session, ChatMessage chatMessage, ChatService chatService) {
        if (chatMessage.getType().equals(ChatMessage.MessageType.ENTER)) {
            sessions.add(session);
            chatMessage.setMessage(chatMessage.getSender() + "님이 입장했습니다.");
        }
        sendMessage(chatMessage, chatService);

    }

    private <T> void sendMessage(T message, ChatService chatService) {
        sessions.parallelStream()
                .forEach(session -> chatService.sendMessage(session, message));
    }
}
package com.test.sbp.controller;

import com.test.sbp.dto.ChatRoom;
import com.test.sbp.service.ChatService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequiredArgsConstructor
@RestController
@RequestMapping("/chat")
public class ChatController {
    private final ChatService chatService;

    @PostMapping
    public ChatRoom createRoom(@RequestBody String name) {
        return chatService.createRoom(name);
    }

    @GetMapping
    public List<ChatRoom> findAllRoom() {
        return chatService.findAllRoom();
    }
}
package com.test.sbp.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.sbp.dto.ChatRoom;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.*;

@Slf4j
@RequiredArgsConstructor
@Service
public class ChatService {
    private final ObjectMapper objectMapper;
    private Map<String, ChatRoom> chatRooms;

    @PostConstruct
    private void init() {
        chatRooms = new LinkedHashMap<>();
    }

    public List<ChatRoom> findAllRoom() {
        return new ArrayList<>(chatRooms.values());
    }

    public ChatRoom findRoomById(String roomId) {
        return chatRooms.get(roomId);
    }

    public ChatRoom createRoom(String name) {
        String randomId = UUID.randomUUID().toString();
        ChatRoom chatRoom = ChatRoom.builder()
                .roomId(randomId)
                .name(name)
                .build();
        chatRooms.put(randomId, chatRoom);
        return chatRoom;
    }

    public <T> void sendMessage(WebSocketSession session, T message) {
        try{
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message)));
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }
}
728x90
반응형
LIST

'개발 > Spring' 카테고리의 다른 글

웹소캣 1:1 채팅방 (수정전)  (0) 2023.04.09
웹소켓 알림 메세지 - session  (0) 2023.04.09
yml 파일이란 ?  (0) 2023.03.12
WebSock 구현  (0) 2023.03.07
JWT 장점과 단점  (0) 2023.02.15
728x90
반응형
SMALL

WebSocketSession 을 사용 token을 사용하는 방법 다시 찾아보기!

build.gradle

// websocket, stomp
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
    implementation 'org.webjars:stomp-websocket:2.3.3'
    implementation 'org.webjars:webjars-locator-core'
    implementation 'org.webjars:sockjs-client:1.0.2'
    implementation 'org.webjars:bootstrap:3.3.7'
    implementation 'org.webjars:jquery:3.1.1-1'

WebSocketConfig

package com.ddalggak.finalproject.global.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

import com.ddalggak.finalproject.global.handler.SocketHandler;

import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		registry.addHandler(new SocketHandler(), "/socket").setAllowedOrigins("*");
	}
}

SocketHandler

package com.ddalggak.finalproject.global.handler;

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

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class SocketHandler extends TextWebSocketHandler {
	private List<WebSocketSession> sessions = new ArrayList<>();
	// WebSocketSession 저장
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		sessions.add(session);
	}
	// WebSocketSession 삭제
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		sessions.remove(session);
	}

	public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
		String payload = message.getPayload();
		session.sendMessage(new TextMessage("알림 메세지"));
	}
}

기본 설정 끝

알람이 필요한 부분에 코드 추가

private SimpMessagingTemplate simpMessagingTemplate;

// 리뷰 등록
	@Transactional
	public ResponseEntity<?> createReview(User user, ReviewRequestDto reviewRequestDto) {
		validateUserByEmail(user.getEmail());
		Ticket ticket = validateTicket(reviewRequestDto.getTicketId());
		Review review = Review.create(reviewRequestDto, ticket);
		reviewRepository.save(review);
		simpMessagingTemplate.convertAndSend("/topic/mewReview", review);
		return SuccessResponseDto.toResponseEntity(SuccessCode.CREATED_SUCCESSFULLY);
	}

we://localhost:8080/topic/mewReview 로 확인!

728x90
반응형
LIST

'개발 > Spring' 카테고리의 다른 글

웹소캣 1:1 채팅방 (수정전)  (0) 2023.04.09
웹소캣 - 채팅 기본 구현 (테스트 코드 연습)  (0) 2023.04.09
yml 파일이란 ?  (0) 2023.03.12
WebSock 구현  (0) 2023.03.07
JWT 장점과 단점  (0) 2023.02.15

+ Recent posts