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
1 change: 1 addition & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ jobs:
ENV_FILE=/home/ubuntu/deare/.env.prod
COMPOSE_FILE=/home/ubuntu/deare/docker-compose-dev.yml


docker-compose --env-file $ENV_FILE -f $COMPOSE_FILE pull app

if docker-compose --env-file $ENV_FILE -f $COMPOSE_FILE up -d; then
Expand Down
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
Expand Down
16 changes: 13 additions & 3 deletions src/main/java/com/deare/backend/api/auth/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public OAuthCallbackResult handleOAuthCallback(String provider, String code) {
);

log.info("신규 회원 - Signup Token 발급 및 Redis 저장 - Provider: {}, ProviderId: {}, Email: {}",
provider, oauthInfo.providerUserId(), oauthInfo.email());
provider, oauthInfo.providerUserId(), maskEmail(oauthInfo.email()));

return new OAuthCallbackResult(
false,
Expand All @@ -120,7 +120,7 @@ private OAuthCallbackResult issueJwtAndReturn(User user, String logMessage) {

jwtService.saveRefreshToken(user.getId(), refreshToken);

log.info("{} - User ID: {}, Email: {}", logMessage, user.getId(), user.getEmail());
log.info("{} - User ID: {}, Email: {}", logMessage, user.getId(), maskEmail(user.getEmail()));

return new OAuthCallbackResult(
true,
Expand Down Expand Up @@ -235,7 +235,7 @@ public SignupResult signup(String signupToken, SignupRequestDTO request) {
userRepository.save(newUser);

log.info("회원가입 완료 - User ID: {}, Provider: {}, Email: {}",
newUser.getId(), provider, email);
newUser.getId(), provider, maskEmail(email));

// 약관 동의 처리
userTermService.createUserTerms(newUser, request.termIds());
Expand All @@ -261,4 +261,14 @@ public void logout(Long userId) {
jwtService.deleteRefreshToken(userId);
log.info("로그아웃 - User ID: {}", userId);
}

private String maskEmail(String email) {
if (email == null || !email.contains("@")) return "***";
String[] parts = email.split("@");
String local = parts[0];
String masked = local.length() <= 2
? "***"
: local.substring(0, 2) + "***";
return masked + "@" + parts[1];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ private void validateRequiredTerms(List<Term> terms) {
// 모든 필수 약관 조회
List<Term> allTerms = termRepository.findAll();
List<Term> requiredTerms = allTerms.stream()
.filter(Term::isRequired)
.filter(term -> term.isRequired() && term.isActive())
.toList();

// 필수 약관이 없으면 통과
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/com/deare/backend/global/aop/LoggingAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.deare.backend.global.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class LoggingAspect {

@Around("execution(* com.deare.backend.api..*Service*.*(..))")
public Object logServiceMethod(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = signature.getName();

long start = System.currentTimeMillis();

try {
Object result = joinPoint.proceed();
long elapsed = System.currentTimeMillis() - start;
log.info("[{}] {} - {}ms", className, methodName, elapsed);
return result;
} catch (Exception e) {
long elapsed = System.currentTimeMillis() - start;
log.error("[{}] {} - {}ms - 예외: {}", className, methodName, elapsed, e.getMessage());
throw e;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public ResponseCookie createSignupTokenCookie(String signupToken) {
return ResponseCookie.from("signup_token", signupToken)
.httpOnly(true)
.secure(true)
.path("/auth")
.path("/api/v1/auth")
.maxAge(Duration.ofMillis(signupTokenProperties.getExpiration()))
.sameSite("None")
.build();
Expand All @@ -60,7 +60,7 @@ public ResponseCookie expireSignupTokenCookie() {
return ResponseCookie.from("signup_token", "")
.httpOnly(true)
.secure(true)
.path("/auth")
.path("/api/v1/auth")
.maxAge(0)
.sameSite("None")
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ public Long getUserIdFromToken(String token) {
Claims claims = parseClaims(token);
return claims.get("userId", Long.class);
}

/**
* 토큰에서 사용자 Role 추출
*/
public String getRoleFromToken(String token) {
Claims claims = parseClaims(token);
return claims.get("role", String.class);
}

/**
* 토큰 파싱
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.deare.backend.global.auth.jwt.filter;

import com.deare.backend.domain.user.entity.User;
import com.deare.backend.domain.user.repository.UserRepository;
import com.deare.backend.global.auth.jwt.JwtProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand All @@ -24,7 +22,6 @@
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtProvider jwtProvider;
private final UserRepository userRepository;

private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
Expand All @@ -43,19 +40,16 @@ protected void doFilterInternal(
// 토큰이 있고 유효한 경우
if (StringUtils.hasText(token) && jwtProvider.validateToken(token)) {

// 토큰에서 사용자 ID 추출
// 토큰에서 사용자 ID, Role 추출
Long userId = jwtProvider.getUserIdFromToken(token);

// DB에서 사용자 조회
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found"));

String role = jwtProvider.getRoleFromToken(token);

// Spring Security 인증 객체 생성
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userId,
null,
Collections.singletonList(new SimpleGrantedAuthority(user.getRole().name()))
Collections.singletonList(new SimpleGrantedAuthority(role))
);

authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
Expand Down
25 changes: 15 additions & 10 deletions src/main/java/com/deare/backend/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,28 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
authorizeHttpRequests(auth -> auth

.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/api/v1/auth/logout").authenticated()
.requestMatchers(

// 헬스체크
// 헬스체크 & 모니터링
"/actuator/health",
"/actuator/health/**",
"/actuator/prometheus",

// 테스트 API 열기 + 스웨거 세팅
"/test/**",
"/swagger-ui/**",
"/v3/api-docs/**",
"/images/**",
// 스웨거 (비활성화)
// "/swagger-ui/**",
// "/v3/api-docs/**",

// 인증 관련 엔드포인트 오픈
"/auth/**",
// 테스트 API
"/api/v1/test/**",

// OAuth2 사용 시 - 콜백/리다이렉트 경로도 permitAll 필요
"/login/**",
// 이미지
"/api/v1/images/**",

// 인증 관련 엔드포인트
"/api/v1/auth/**",

// OAuth2
"/oauth2/**"
).permitAll()
.anyRequest().authenticated()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
// @Configuration
public class SwaggerConfig {

@Bean
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/com/deare/backend/global/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.deare.backend.global.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix(
"/api/v1",
c -> c.isAnnotationPresent(RestController.class)
&& c.getPackageName().startsWith("com.deare.backend.api")
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.deare.backend.global.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.UUID;

@Component
public class MDCLoggingFilter extends OncePerRequestFilter {

private static final String REQUEST_ID = "requestId";
private static final String USER_ID = "userId";
private static final String METHOD = "method";
private static final String URI = "uri";

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {

try {
MDC.put(REQUEST_ID, UUID.randomUUID().toString().substring(0, 8));
MDC.put(METHOD, request.getMethod());
MDC.put(URI, request.getRequestURI());

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof Long userId) {
MDC.put(USER_ID, String.valueOf(userId));
} else {
MDC.put(USER_ID, "anonymous");
}

filterChain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
14 changes: 11 additions & 3 deletions src/main/resources/application-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,22 @@ oauth:
google:
redirect-uri: ${GOOGLE_REDIRECT_URI}

# health check
# health check & monitoring
management:
endpoints:
web:
exposure:
include: health
include: health, prometheus
endpoint:
health:
show-details: never
probes:
enabled: true
enabled: true
prometheus:
enabled: true
metrics:
export:
prometheus:
enabled: true
tags:
application: ${spring.application.name}
Loading