Skip to content

Commit

Permalink
Merge pull request #47 from dnd-side-project/dev
Browse files Browse the repository at this point in the history
�dev -> main
  • Loading branch information
FacerAin authored Feb 18, 2024
2 parents d4993a0 + 287c5c5 commit d73b071
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package org.dnd.timeet.common.interceptor;

import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.firebase.database.annotations.Nullable;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dnd.timeet.common.security.CustomUserDetails;
import org.dnd.timeet.common.security.JwtProvider;
import org.dnd.timeet.meeting.application.WebSocketSessionManager;
import org.dnd.timeet.member.application.MemberFindService;
import org.dnd.timeet.member.domain.Member;
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.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

/**
* WebSocket 채널에 JWT 검증하는 인터셉터
*/

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtChannelInterceptor implements ChannelInterceptor {

private final MemberFindService userUtilityService;
private final WebSocketSessionManager sessionManager;

@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
// 연결 요청시 JWT 검증
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// Authorization 헤더 추출
List<String> authorization = accessor.getNativeHeader(JwtProvider.HEADER);
if (authorization != null && !authorization.isEmpty()) {
String jwt = authorization.get(0).substring(JwtProvider.TOKEN_PREFIX.length());
try {
// JWT 토큰 검증
DecodedJWT decodedJWT = JwtProvider.verify(jwt);
Long memberId = decodedJWT.getClaim("id").asLong();
// 사용자 정보 조회
Member member = userUtilityService.getUserById(memberId);

// 사용자 인증 정보 설정
CustomUserDetails userDetails = new CustomUserDetails(member);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);

// 세션 추가
String sessionId = accessor.getSessionId();
sessionManager.addUserSession(sessionId, memberId);
log.info("User Added. Active User Count: " + sessionManager.getActiveUserCount());
} catch (JWTVerificationException e) {
log.error("JWT Verification Failed: " + e.getMessage());
return null;
} catch (Exception e) {
log.error("An unexpected error occurred: " + e.getMessage());
return null;
}
} else {
// 클라이언트 측 타임아웃 처리
log.error("Authorization header is not found");
return null;
}
}
return message;
}

@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, @Nullable Exception ex) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);

// 연결 해제 시 세션 정보 제거
if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
String sessionId = accessor.getSessionId();
sessionManager.removeUserSession(sessionId);
log.info("User Disconnected. Active User Count: " + sessionManager.getActiveUserCount());
}
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ public JwtAuthenticationFilter(AuthenticationManager authenticationManager, Memb
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {

String jwt = request.getHeader(JWTProvider.HEADER);
String jwt = request.getHeader(JwtProvider.HEADER);

try {
if (jwt != null && !isNonProtectedUrl(request)) { // 토큰이 있고 보호된 URL일 경우 토큰 검증
DecodedJWT decodedJWT = JWTProvider.verify(jwt);
DecodedJWT decodedJWT = JwtProvider.verify(jwt);
Long id = decodedJWT.getClaim("id").asLong();

Member member = userUtilityService.getUserById(id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import org.springframework.stereotype.Component;

@Component
public class JWTProvider {
public class JwtProvider {

public static final Long EXP = 1000L * 60 * 60 * 48; // 48시간
public static final String TOKEN_PREFIX = "Bearer ";
Expand Down
6 changes: 2 additions & 4 deletions src/main/java/org/dnd/timeet/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand Down Expand Up @@ -62,7 +61,6 @@ public class SecurityConfig {
"/swagger-ui/**",
"/swagger-resources/**",
// open url
"/api/members/**",
//h2-console
"/h2-console/**",
// oauth2
Expand Down Expand Up @@ -129,9 +127,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, Authentication

// 인증, 권한 필터 설정
http.authorizeHttpRequests(auth -> auth
.requestMatchers(PUBLIC_URLS).permitAll() // 인증 없이 접근 허용
.requestMatchers(PUBLIC_URLS).permitAll() // 인증 없이 접근 허용

.anyRequest().authenticated()
.anyRequest().authenticated()
);

http.oauth2Login(oauth2 -> oauth2
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/org/dnd/timeet/config/WebSocketConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.dnd.timeet.config;

import org.dnd.timeet.common.interceptor.JwtChannelInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
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 @@ -10,6 +13,9 @@
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Autowired
private JwtChannelInterceptor jwtChannelInterceptor;

// 메시지 브로커 설정
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
Expand All @@ -24,5 +30,10 @@ public void registerStompEndpoints(StompEndpointRegistry registry) {
.setAllowedOriginPatterns("*"); // 모든 도메인에서 접근 허용
// .withSockJS(); // /ws로 접속하면 SockJS를 통해 웹소켓 연결
}

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.dnd.timeet.meeting.application;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.stereotype.Service;

/**
* 활성 사용자 수 추적 및 세션 관리
*/
@Service
public class WebSocketSessionManager {

private final AtomicInteger activeUserCount = new AtomicInteger(0);
private final Map<String, Long> sessionUserMap = new ConcurrentHashMap<>();

public void addUserSession(String sessionId, Long userId) {
sessionUserMap.put(sessionId, userId);
activeUserCount.incrementAndGet();
}

public void removeUserSession(String sessionId) {
if (sessionUserMap.remove(sessionId) != null) {
activeUserCount.decrementAndGet();
}
}

public int getActiveUserCount() {
return activeUserCount.get();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,28 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Transactional(readOnly = true)
@Transactional
@RequiredArgsConstructor
@Service
public class MemberService {

private final PasswordEncoder passwordEncoder;
private final MemberRepository memberRepository;

@Transactional
public void upsertFcmToken(Long id, String fcmToken) {
Member member = memberRepository.findById(id)
.orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("MemberId", "Member not found")));
member.setFcmToken(fcmToken);

}

public String updateNickname(Long id, String nickname) {
Member member = memberRepository.findById(id)
.orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("MemberId", "Member not found")));
member.changeName(nickname);

return member.getName();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.dnd.timeet.common.security.annotation.ReqUser;
import org.dnd.timeet.common.utils.ApiUtils;
import org.dnd.timeet.common.utils.ApiUtils.ApiResult;
import org.dnd.timeet.member.application.MemberService;
import org.dnd.timeet.member.domain.Member;
import org.dnd.timeet.member.dto.MemberNicknameRequest;
import org.dnd.timeet.member.dto.RegisterFcmRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -25,7 +29,17 @@ public class MemberController {
public void registerFcmToken(@RequestBody RegisterFcmRequest registerFcmRequest,
@ReqUser Member member) {
memberService.upsertFcmToken(member.getId(), registerFcmRequest.getFcmToken());


}

@PatchMapping("/nickname")
@Operation(summary = "닉네임 추가", description = "닉네임을 추가한다.")
public ResponseEntity<ApiResult<String>> registerNickname(
@RequestBody MemberNicknameRequest memberNicknameRequest,
@ReqUser Member member) {
String updatedNickname = memberService.updateNickname(member.getId(),
memberNicknameRequest.getNickname());
return ResponseEntity.ok(ApiUtils.success(updatedNickname));
}
}

4 changes: 4 additions & 0 deletions src/main/java/org/dnd/timeet/member/domain/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,8 @@ public Member(MemberRole role, String name, String imageUrl, Long oauthId, OAuth
public void setFcmToken(String fcmToken) {
this.fcmToken = fcmToken;
}

public void changeName(String name) {
this.name = name;
}
}
16 changes: 16 additions & 0 deletions src/main/java/org/dnd/timeet/member/dto/MemberNicknameRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.dnd.timeet.member.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Schema(description = "nickname 등록 요청")
@Getter
@Setter
@NoArgsConstructor
public class MemberNicknameRequest {

@Schema(description = "nickname", nullable = false, example = "greenfrog")
private String nickname;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import org.dnd.timeet.common.exception.BadRequestError.ErrorCode;
import org.dnd.timeet.common.security.CookieAuthorizationRequestRepository;
import org.dnd.timeet.common.security.CustomUserDetails;
import org.dnd.timeet.common.security.JWTProvider;
import org.dnd.timeet.common.security.JwtProvider;
import org.dnd.timeet.common.utils.CookieUtil;
import org.dnd.timeet.member.domain.Member;
import org.dnd.timeet.member.domain.MemberRepository;
Expand Down Expand Up @@ -65,7 +65,7 @@ protected String determineTargetUrl(HttpServletRequest request, HttpServletRespo

// jwt

String accessToken = JWTProvider.create(user.get());
String accessToken = JwtProvider.create(user.get());

return UriComponentsBuilder.fromUriString(targetUrl)
.queryParam("code", accessToken)
Expand Down

0 comments on commit d73b071

Please sign in to comment.