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
12 changes: 9 additions & 3 deletions nodejs-frontend/static/js/common/ajaxUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ function ajaxToJson(url, method, async, data, successCallback, errorCallback, co
}

function tokenAjax(url, method, async, data, successCallback, errorCallback, completeCallback) {
var headers = {
'Authorization': localStorage.getItem('access_token') || ''
};

var roomToken = sessionStorage.getItem('roomAccessToken');
if (roomToken) {
headers['X-Room-Token'] = roomToken;
}
$.ajax({
url: url,
type: method,
Expand All @@ -66,9 +74,7 @@ function tokenAjax(url, method, async, data, successCallback, errorCallback, com
xhrFields: {
withCredentials: true
},
headers: {
'Authorization': localStorage.getItem('access_token') || ''
},
headers: headers,
success: function (data) {
if (successCallback && typeof successCallback === 'function') {
successCallback(data);
Expand Down
4 changes: 3 additions & 1 deletion nodejs-frontend/static/js/popup/room_popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ const RoomPopup = {
}

let successCallback = function(result) {
if (result?.data && result?.result === 'success') {
if (result?.data?.isValidate === true) {
var roomToken = result.data.token;
sessionStorage.setItem('roomAccessToken', roomToken);
self.showToast('방에 정상적으로 입장했습니다!', 'success');
$('#enterRoomModal').modal('hide');

Expand Down
4 changes: 4 additions & 0 deletions nodejs-frontend/static/js/popup/room_settings_popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ const RoomSettingsPopup = {

let successCallback = function (result) {
if (result && result.result === 'success' && result.data) {
var roomToken = result.data.token;
if (roomToken) {
sessionStorage.setItem('roomAccessToken', roomToken);
}
self.loadRoomInfo();
} else {
self.showToast('비밀번호가 일치하지 않습니다.', 'error');
Expand Down
3 changes: 3 additions & 0 deletions nodejs-frontend/static/js/rtc/kurento-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@ function register() {
if (error?.responseJSON && ['40050', '40051', '40052'].includes(error.responseJSON.code)) {
self.showToast('로그인이 필요한 서비스입니다.');
window.location.href = window.__CONFIG__.BASE_URL + '/login/chatlogin.html';
} else if (error?.responseJSON && '40061' === error.responseJSON.code) {
alert("입장 정보가 확인되지 않았습니다. 다시 시도해주세요.")
window.location.href = window.__CONFIG__.BASE_URL + '/roomlist.html';
}
};
// AJAX 요청 실행
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000", "https://hjproject.kro.kr:8653", "https://hjproject.kro.kr/chatforyou")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Authorization", "Content-Type", "X-Requested-With")
.allowedHeaders("Authorization", "Content-Type", "X-Requested-With", "X-Room-Token")
.exposedHeaders("Custom-Header")
.allowCredentials(true)
.maxAge(3600);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import webChat.model.room.out.ChatRoomOutVo;
import webChat.model.chat.ChatType;
import webChat.model.user.UserDto;
import webChat.security.jwt.JwtRoomProvider;
import webChat.service.chatroom.ChatRoomService;
import webChat.service.redis.RedisService;
import webChat.service.routing.RoutingInstanceProvider;
Expand All @@ -43,6 +44,7 @@ public class ChatRoomController {
private final RoutingInstanceProvider instanceProvider;
private final RedisService redisService;
private final UserService userService;
private final JwtRoomProvider jwtRoomProvider;

// 채팅방 생성
@PostMapping("/room")
Expand Down Expand Up @@ -82,6 +84,7 @@ public ResponseEntity<ChatForYouResponse> createRoom(
public ResponseEntity<ChatForYouResponse> joinRoom(
@PathVariable String roomId,
@RequestHeader("Authorization") String authorization,
@RequestHeader(value = "X-Room-Token", required = false) String roomToken,
HttpServletRequest request,
HttpServletResponse response) throws Exception {

Expand All @@ -95,6 +98,11 @@ public ResponseEntity<ChatForYouResponse> joinRoom(

ChatRoom chatRoom = chatRoomService.findRoomById(roomId);

// JWT 토큰 검증
if (chatRoom.isSecretChk()) {
jwtRoomProvider.validate(roomToken, chatRoom.getRoomId());
}

if (StringUtil.isNullOrEmpty(chatRoom.getInstanceId()) || !instanceProvider.isHealthy(chatRoom.getInstanceId())){
// TODO 서버가 죽었을때 예외처리 필요 :: 방 삭제 및 임시 조치로 메인 대시보드로 이동
chatRoomService.delChatRoom(roomId);
Expand Down Expand Up @@ -163,7 +171,7 @@ public ResponseEntity<ChatForYouResponse> validatePwd(
// TODO 추후 401 권한 에러로 수정할 것
return ResponseEntity.ok(ChatForYouResponse.builder()
.result("success")
.data(chatRoomService.validatePwd(roomId, roomPwd))
.data(chatRoomService.validatePwd(oauthRedis.getEmail(), roomId, roomPwd))
.build());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,20 @@ public ExpiredQRSession(String message) {
result.put("message", "QR Session Expired");
return result;
}

public static class InvalidRoomAccessException extends BadRequestException {
public InvalidRoomAccessException(String message) {
super(message);
}
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(InvalidRoomAccessException.class)
@ResponseBody
public Map<String, Object> handleInvalidRoomAccess() {
Map<String, Object> result = new HashMap<>();
result.put("code", "40061");
result.put("message", "Invalid or missing room access token");
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package webChat.security.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import webChat.controller.ExceptionController;
import webChat.security.jwt.core.JwtCoreProvider;
import webChat.utils.StringUtil;

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtRoomProvider {
private final long EXPIRE_MS;
private final JwtCoreProvider jwtCore;

public JwtRoomProvider(
@Qualifier("roomJwtKey") Key key,
@Value("${jwt.room.expire-ms:1800000}") long expireMs
) {
this.jwtCore = new JwtCoreProvider(key);
this.EXPIRE_MS = expireMs;
}

public String create(String roomId, String userId) {
Map<String, Object> claims = new HashMap<>();
claims.put("roomId", roomId);
claims.put("type", JwtTokenType.ROOM_ACCESS);

return jwtCore.create(userId, claims, EXPIRE_MS);
}

public void validate(String token, String roomId) throws ExceptionController.InvalidRoomAccessException {
try {
if (StringUtil.isNullOrEmpty(token)) {
throw new ExceptionController.InvalidRoomAccessException("Invalid access");
}
Claims claims = jwtCore.parse(token);

if (!JwtTokenType.ROOM_ACCESS.name().equals(claims.get("type"))) {
throw new ExceptionController.InvalidRoomAccessException("Invalid room access info");
}

String tokenRoomId = claims.get("roomId", String.class);
if (!roomId.equals(tokenRoomId)) {
throw new ExceptionController.InvalidRoomAccessException("Invalid room access info");
}
} catch (Exception e) {
throw new ExceptionController.InvalidRoomAccessException("Invalid access");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package webChat.security.jwt;

public enum JwtTokenType {
ROOM_ACCESS
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package webChat.security.jwt.config;

import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.security.Key;

@Configuration
public class JwtKeyConfig {

@Bean
@Qualifier("roomJwtKey")
public Key roomJwtKey(
@Value("${jwt.room.secret}") String secret
) {
return Keys.hmacShaKeyFor(
Decoders.BASE64.decode(secret)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package webChat.security.jwt.core;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;
import java.util.Map;

@Component
public class JwtCoreProvider {

private final Key key;

public JwtCoreProvider(Key key) {
this.key = key;
}

public String create(String subject, Map<String, Object> claims, long expireMs) {
return Jwts.builder()
.setSubject(subject)
.addClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expireMs))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}

public Claims parse(String token) {
return Jwts.parser()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import webChat.model.room.KurentoRoom;
import webChat.model.room.RoomState;
import webChat.model.room.in.ChatRoomInVo;
import webChat.security.jwt.JwtRoomProvider;
import webChat.service.analysis.AnalysisService;
import webChat.service.file.FileService;
import webChat.service.kafka.ChatKafkaProducer;
Expand Down Expand Up @@ -50,6 +51,8 @@ public class ChatRoomService {

private final ChatKafkaProducer chatKafkaProducer;

private final JwtRoomProvider jwtRoomProvider;

@Value("${chatforyou.room.max_user_count}")
private int MAX_USER_COUNT;

Expand Down Expand Up @@ -134,16 +137,23 @@ public ChatRoom findRoomById(String roomId) throws BadRequestException {
}

// 채팅방 비밀번호 조회
public boolean validatePwd(String roomId, String roomPwd) throws BadRequestException {
public Map<String, Object> validatePwd(String email, String roomId, String roomPwd) throws BadRequestException {
Map<String, Object> result = new HashMap<>();
ChatRoom chatRoom = redisService.getRedisDataByDataType(roomId, DataType.CHATROOM, KurentoRoom.class);
if(chatRoom == null) {
// TODO 방정보 찾을 수 없는 경우 예외처리
}

boolean validPwd = chatRoom.getRoomPwd().equals(roomPwd);
boolean overUserCnt = chatRoom.getUserCount() + 1 > chatRoom.getMaxUserCnt();
boolean isValidate = validPwd && !overUserCnt;
result.put("isValidate", isValidate);

return validPwd && !overUserCnt;
// jwt 토큰 발급
if (isValidate) {
String token = jwtRoomProvider.create(chatRoom.getRoomId(), email);
result.put("token", token);
}
return result;
}

// maxUserCnt 에 따른 채팅방 입장 여부
Expand Down