feat: jwt 토큰 비활성화 및 회의예약API 개발

This commit is contained in:
djeon 2025-10-24 13:52:32 +09:00
parent d9261bad2c
commit 6e7b910a8d
11 changed files with 3274 additions and 5 deletions

File diff suppressed because it is too large Load Diff

1
meeting/logs/meeting.log Normal file
View File

@ -0,0 +1 @@
nohup: ./gradlew: No such file or directory

View File

@ -37,6 +37,16 @@ public class Meeting {
*/
private LocalDateTime scheduledAt;
/**
* 회의 종료 예정 일시
*/
private LocalDateTime endTime;
/**
* 회의 장소
*/
private String location;
/**
* 회의 시작 일시
*/

View File

@ -78,8 +78,8 @@ public class MeetingDTO {
.meetingId(meeting.getMeetingId())
.title(meeting.getTitle())
.startTime(meeting.getStartedAt() != null ? meeting.getStartedAt() : meeting.getScheduledAt())
.endTime(meeting.getEndedAt())
.location(null) // Meeting 도메인에 location 필드가 없어서 null로 설정
.endTime(meeting.getEndedAt() != null ? meeting.getEndedAt() : meeting.getEndTime())
.location(meeting.getLocation())
.agenda(meeting.getDescription())
.participants(meeting.getParticipants().stream()
.map(participantId -> ParticipantDTO.builder()

View File

@ -49,6 +49,8 @@ public class MeetingService implements
.title(command.title())
.description(command.description())
.scheduledAt(command.scheduledAt())
.endTime(command.endTime())
.location(command.location())
.status("SCHEDULED")
.organizerId(command.organizerId())
.participants(command.participants())

View File

@ -22,6 +22,8 @@ public interface CreateMeetingUseCase {
String title,
String description,
LocalDateTime scheduledAt,
LocalDateTime endTime,
String location,
String organizerId,
List<String> participants,
String templateId

View File

@ -12,11 +12,15 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
/**
* Spring Security 설정
@ -48,7 +52,8 @@ public class SecurityConfig {
// WebSocket endpoints
.requestMatchers("/ws/**").permitAll()
// All other requests require authentication
.anyRequest().authenticated()
// .anyRequest().authenticated()
.anyRequest().permitAll()
)
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class)
@ -69,7 +74,8 @@ public class SecurityConfig {
// 허용할 헤더
configuration.setAllowedHeaders(Arrays.asList(
"Authorization", "Content-Type", "X-Requested-With", "Accept",
"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"
"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers",
"X-User-Id", "X-User-Name", "X-User-Email"
));
// 자격 증명 허용
@ -82,4 +88,24 @@ public class SecurityConfig {
source.registerCorsConfiguration("/**", configuration);
return source;
}
/**
* HttpFirewall 설정
* 한글을 포함한 모든 문자를 헤더 값으로 허용
*/
@Bean
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
// 한글을 포함한 모든 문자를 허용하도록 설정
firewall.setAllowedHeaderValues(header -> true);
// URL 인코딩된 슬래시 허용
firewall.setAllowUrlEncodedSlash(true);
// 세미콜론 허용
firewall.setAllowSemicolon(true);
return firewall;
}
}

View File

@ -32,6 +32,36 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 1. X-User-* 헤더를 통한 인증 (개발/테스트용)
String headerUserId = request.getHeader("X-User-Id");
String headerUserName = request.getHeader("X-User-Name");
String headerUserEmail = request.getHeader("X-User-Email");
if (StringUtils.hasText(headerUserId)) {
// X-User-* 헤더가 있으면 이를 사용하여 인증
UserPrincipal userPrincipal = UserPrincipal.builder()
.userId(headerUserId)
.username(headerUserName != null ? headerUserName : "unknown")
.email(headerUserEmail)
.authority("USER")
.build();
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userPrincipal,
null,
Collections.singletonList(new SimpleGrantedAuthority("USER"))
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("헤더 기반 인증된 사용자: {} ({})", userPrincipal.getUsername(), headerUserId);
filterChain.doFilter(request, response);
return;
}
// 2. JWT 토큰을 통한 인증
String token = jwtTokenProvider.resolveToken(request);
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
@ -69,7 +99,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("인증된 사용자: {} ({})", userPrincipal.getUsername(), userId);
log.debug("JWT 기반 인증된 사용자: {} ({})", userPrincipal.getUsername(), userId);
}
}

View File

@ -23,6 +23,11 @@ public class UserPrincipal {
*/
private final String username;
/**
* 사용자 이메일
*/
private final String email;
/**
* 사용자 권한
*/

View File

@ -65,6 +65,8 @@ public class MeetingController {
request.getTitle(),
request.getAgenda(),
request.getStartTime(),
request.getEndTime(),
request.getLocation(),
userId,
request.getParticipants(),
null // 템플릿 ID는 나중에 적용

View File

@ -37,6 +37,12 @@ public class MeetingEntity extends BaseTimeEntity {
@Column(name = "scheduled_at", nullable = false)
private LocalDateTime scheduledAt;
@Column(name = "end_time")
private LocalDateTime endTime;
@Column(name = "location", length = 200)
private String location;
@Column(name = "started_at")
private LocalDateTime startedAt;
@ -62,6 +68,8 @@ public class MeetingEntity extends BaseTimeEntity {
.title(this.title)
.description(this.description)
.scheduledAt(this.scheduledAt)
.endTime(this.endTime)
.location(this.location)
.startedAt(this.startedAt)
.endedAt(this.endedAt)
.status(this.status)
@ -77,6 +85,8 @@ public class MeetingEntity extends BaseTimeEntity {
.title(meeting.getTitle())
.description(meeting.getDescription())
.scheduledAt(meeting.getScheduledAt())
.endTime(meeting.getEndTime())
.location(meeting.getLocation())
.startedAt(meeting.getStartedAt())
.endedAt(meeting.getEndedAt())
.status(meeting.getStatus())