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
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
import com.jobdri.jobdri_api.domain.audit.entity.AuditLog;
import com.jobdri.jobdri_api.domain.audit.repository.AuditLogRepository;
import com.jobdri.jobdri_api.domain.user.entity.User;
import com.jobdri.jobdri_api.domain.user.repository.UserRepository;
import com.jobdri.jobdri_api.global.logging.LoggingContext;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
Expand All @@ -29,8 +31,9 @@ public class AuditLogService {

private final AuditLogRepository auditLogRepository;
private final ObjectMapper objectMapper;
private final UserRepository userRepository;

@Transactional
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void record(
User user,
String action,
Expand All @@ -41,7 +44,7 @@ public void record(
) {
HttpServletRequest request = currentRequest();
AuditLog auditLog = auditLogRepository.save(AuditLog.create(
user,
resolveVisibleUser(user),
action,
targetType,
targetId,
Expand All @@ -53,6 +56,13 @@ public void record(
writeAuditTrail(auditLog);
}

private User resolveVisibleUser(User user) {
if (user == null || user.getId() == null) {
return null;
}
return userRepository.findById(user.getId()).orElse(null);
}

private HttpServletRequest currentRequest() {
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attributes) {
return attributes.getRequest();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.jobdri.jobdri_api.domain.auth.service;

import com.jobdri.jobdri_api.domain.audit.service.AuditLogService;
import com.jobdri.jobdri_api.domain.auth.dto.request.LoginRequest;
import com.jobdri.jobdri_api.domain.auth.dto.request.LogoutRequest;
import com.jobdri.jobdri_api.domain.auth.dto.request.PasswordResetConfirmationRequest;
Expand All @@ -17,6 +18,7 @@
import com.jobdri.jobdri_api.global.logging.LoggingMdcKeys;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.redis.core.StringRedisTemplate;
Expand All @@ -30,9 +32,11 @@
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
@RequiredArgsConstructor
public class AuthService {

Expand All @@ -54,6 +58,7 @@ public class AuthService {
private final EmailService emailService;
private final AsyncEmailSender asyncEmailSender;
private final StringRedisTemplate redisTemplate;
private final AuditLogService auditLogService;

@Transactional
public void signup(SignupRequest request) {
Expand Down Expand Up @@ -98,6 +103,7 @@ public LoginResponse issueTokens(User user) {
String refreshTokenValue = jwtUtil.createRefreshToken(user.getEmail());

saveRefreshToken(user.getId(), refreshTokenValue);
recordLoginSuccess(user);

return LoginResponse.builder()
.accessToken(accessToken)
Expand Down Expand Up @@ -261,6 +267,21 @@ private void saveRefreshToken(Long userId, String refreshTokenValue) {
);
}

private void recordLoginSuccess(User user) {
try {
auditLogService.record(
user,
"LOGIN_SUCCESS",
"USER",
user.getId(),
null,
Map.of("loginMethod", user.getSocialType().name())
);
} catch (RuntimeException e) {
log.warn("Login success audit log recording failed. userId={}", user.getId(), e);
}
}

private String getRefreshTokenKey(Long userId) {
return REFRESH_TOKEN_PREFIX + userId;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.jobdri.jobdri_api.domain.auth.service;

import com.jobdri.jobdri_api.domain.audit.repository.AuditLogRepository;
import com.jobdri.jobdri_api.domain.audit.service.AuditLogService;
import com.jobdri.jobdri_api.domain.auth.dto.request.LoginRequest;
import com.jobdri.jobdri_api.domain.auth.dto.request.PasswordResetConfirmationRequest;
import com.jobdri.jobdri_api.domain.auth.dto.request.PasswordResetEmailRequest;
import com.jobdri.jobdri_api.domain.auth.dto.request.ReissueTokenRequest;
import com.jobdri.jobdri_api.domain.auth.dto.response.LoginResponse;
import com.jobdri.jobdri_api.domain.user.entity.SocialType;
import com.jobdri.jobdri_api.domain.user.entity.User;
import com.jobdri.jobdri_api.domain.user.repository.UserRepository;
Expand All @@ -15,22 +19,29 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.MDC;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.util.ReflectionTestUtils;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
Expand Down Expand Up @@ -61,6 +72,9 @@ class AuthServiceTest {
@Mock
private StringRedisTemplate redisTemplate;

@Mock
private AuditLogService auditLogService;

@Mock
private ValueOperations<String, String> valueOperations;

Expand All @@ -75,7 +89,69 @@ void setUp() {
jwtUtil,
emailService,
asyncEmailSender,
redisTemplate
redisTemplate,
auditLogService
);
}

@Test
@DisplayName("로컬 로그인 성공 시 refresh token 저장 후 로그인 성공 audit 로그를 남긴다")
void loginStoresRefreshTokenAndRecordsAuditLog() {
User user = localUser(1L, "local@example.com");
when(userRepository.findByEmail("local@example.com")).thenReturn(Optional.of(user));
when(passwordEncoder.matches("password123", "encoded-password")).thenReturn(true);
when(jwtUtil.createAccessToken(user.getEmail(), user.getId(), user.getRole())).thenReturn("access-token");
when(jwtUtil.createRefreshToken(user.getEmail())).thenReturn("refresh-token");
when(jwtUtil.getRefreshTokenTime()).thenReturn(604_800_000L);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);

LoginResponse response = authService.login(new LoginRequest("local@example.com", "password123"));

assertThat(response.accessToken()).isEqualTo("access-token");
assertThat(response.refreshToken()).isEqualTo("refresh-token");
verify(valueOperations).set(
"RefreshToken:1",
"refresh-token",
604_800_000L,
TimeUnit.MILLISECONDS
);
verify(auditLogService).record(
eq(user),
eq("LOGIN_SUCCESS"),
eq("USER"),
eq(1L),
isNull(),
eq(Map.of("loginMethod", "LOCAL"))
);
}

@Test
@DisplayName("소셜 로그인 토큰 발급도 Google 로그인 성공 audit 로그를 남긴다")
void issueTokensRecordsGoogleLoginAuditLog() {
User user = User.createSocialUser(
"구글 사용자",
"google@example.com",
"encoded-password",
SocialType.GOOGLE,
"google-id"
);
ReflectionTestUtils.setField(user, "id", 2L);
when(jwtUtil.createAccessToken(user.getEmail(), user.getId(), user.getRole())).thenReturn("google-access-token");
when(jwtUtil.createRefreshToken(user.getEmail())).thenReturn("google-refresh-token");
when(jwtUtil.getRefreshTokenTime()).thenReturn(604_800_000L);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);

LoginResponse response = authService.issueTokens(user);

assertThat(response.accessToken()).isEqualTo("google-access-token");
assertThat(response.refreshToken()).isEqualTo("google-refresh-token");
verify(auditLogService).record(
eq(user),
eq("LOGIN_SUCCESS"),
eq("USER"),
eq(2L),
isNull(),
eq(Map.of("loginMethod", "GOOGLE"))
);
}

Expand Down Expand Up @@ -258,3 +334,62 @@ private User localUser(Long id, String email) {
return user;
}
}

@SpringBootTest
@ActiveProfiles("test")
class AuthServiceAuditFailureIntegrationTest {

@Autowired
private AuthService authService;

@Autowired
private UserRepository userRepository;

@MockitoBean
private PasswordEncoder passwordEncoder;

@MockitoBean
private JwtUtil jwtUtil;

@MockitoBean
private EmailService emailService;

@MockitoBean
private AsyncEmailSender asyncEmailSender;

@MockitoBean
private StringRedisTemplate redisTemplate;

@MockitoBean
private AuditLogRepository auditLogRepository;

@Test
@DisplayName("로그인 성공 audit 저장이 실패해도 토큰 발급은 커밋된다")
void loginCommitsWhenAuditRecordingFails() {
User user = userRepository.save(User.signup(
"통합 테스트 사용자",
"audit-failure-login@example.com",
"encoded-password"
));
@SuppressWarnings("unchecked")
ValueOperations<String, String> valueOperations = mock(ValueOperations.class);
when(passwordEncoder.matches("password123", "encoded-password")).thenReturn(true);
when(jwtUtil.createAccessToken(user.getEmail(), user.getId(), user.getRole())).thenReturn("access-token");
when(jwtUtil.createRefreshToken(user.getEmail())).thenReturn("refresh-token");
when(jwtUtil.getRefreshTokenTime()).thenReturn(604_800_000L);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(auditLogRepository.save(any())).thenThrow(new RuntimeException("audit insert failed"));

LoginResponse response = authService.login(new LoginRequest(user.getEmail(), "password123"));

assertThat(response.accessToken()).isEqualTo("access-token");
assertThat(response.refreshToken()).isEqualTo("refresh-token");
verify(valueOperations).set(
"RefreshToken:" + user.getId(),
"refresh-token",
604_800_000L,
TimeUnit.MILLISECONDS
);
verify(auditLogRepository).save(any());
}
}
Loading