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
@@ -1,5 +1,6 @@
package clap.server.adapter.inbound.security.filter;

import clap.server.application.port.inbound.auth.CheckAccountLockStatusUseCase;
import clap.server.application.service.auth.LoginAttemptService;
import clap.server.exception.AuthException;
import jakarta.servlet.FilterChain;
Expand All @@ -25,7 +26,7 @@
@Slf4j
public class LoginAttemptFilter extends OncePerRequestFilter {

private final LoginAttemptService loginAttemptService;
private final CheckAccountLockStatusUseCase checkAccountLockStatusUseCase;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
Expand All @@ -34,7 +35,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
if (request.getRequestURI().equals(LOGIN_ENDPOINT)) {
String clientIp = getClientIp(request);

loginAttemptService.checkAccountIsLocked(clientIp);
checkAccountLockStatusUseCase.checkAccountIsLocked(clientIp);

}
} catch (AuthException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package clap.server.application.port.inbound.auth;

public interface CheckAccountLockStatusUseCase {
void checkAccountIsLocked(String clientIp);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package clap.server.application.port.outbound.log;

import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus;
import jakarta.servlet.http.HttpServletRequest;

public interface LoggingPort {
void createAnonymousLog(HttpServletRequest request, int statusCode, String customCode, LogStatus logStatus, Object responseBody, String requestBody, String nickName);
void createMemberLog(HttpServletRequest request, int statusCode, String customCode,LogStatus logStatus, Object responseBody, String requestBody, Long memberId);
void createLoginFailedLog(HttpServletRequest request, int statusCode, String customCode, LogStatus logStatus, String requestBody, String nickName);
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
package clap.server.application.service.auth;

import clap.server.application.port.inbound.auth.CheckAccountLockStatusUseCase;
import clap.server.application.port.outbound.auth.loginLog.CommandLoginLogPort;
import clap.server.application.port.outbound.auth.loginLog.LoadLoginLogPort;
import clap.server.domain.model.auth.LoginLog;
import clap.server.exception.AuthException;
import clap.server.exception.code.AuthErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

@RequiredArgsConstructor
@Component
@Service
@Transactional
public class LoginAttemptService {
public class LoginAttemptService implements CheckAccountLockStatusUseCase {
private final LoadLoginLogPort loadLoginLogPort;
private final CommandLoginLogPort commandLoginLogPort;
private static final int MAX_FAILED_ATTEMPTS = 5;
Expand All @@ -36,6 +37,7 @@ public void recordFailedAttempt(String clientIp, String attemptNickname) {
commandLoginLogPort.save(loginLog);
}

@Override
public void checkAccountIsLocked(String clientIp) {
LoginLog loginLog = loadLoginLogPort.findByClientIp(clientIp).orElse(null);
if (loginLog == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package clap.server.application.port.inbound.domain;
package clap.server.application.service.log;

import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus;
import clap.server.application.port.inbound.domain.MemberService;
import clap.server.application.port.outbound.auth.loginLog.LoadLoginLogPort;
import clap.server.application.port.outbound.log.CommandLogPort;
import clap.server.application.port.outbound.log.LoggingPort;
import clap.server.common.utils.ClientIpParseUtil;
import clap.server.domain.model.auth.LoginLog;
import clap.server.domain.model.log.AnonymousLog;
import clap.server.domain.model.log.MemberLog;
import clap.server.domain.model.member.Member;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -18,11 +18,10 @@
@Service
@RequiredArgsConstructor
@Transactional
public class LogService {
public class LogService implements LoggingPort {
private final CommandLogPort commandLogPort;
private final MemberService memberService;
private final LoadLoginLogPort loadLoginLogPort;
private final ObjectMapper objectMapper;

public void createAnonymousLog(HttpServletRequest request, int statusCode, String customCode, LogStatus logStatus, Object responseBody, String requestBody, String nickName) {
AnonymousLog anonymousLog = AnonymousLog.createAnonymousLog(request, statusCode,customCode, logStatus, responseBody, requestBody, nickName);
Expand All @@ -35,7 +34,7 @@ public void createMemberLog(HttpServletRequest request, int statusCode, String c
commandLogPort.saveMemberLog(memberLog);
}

public void createLoginFailedLog(HttpServletRequest request, int statusCode, String customCode, LogStatus logStatus, String requestBody, String nickName) throws JsonProcessingException {
public void createLoginFailedLog(HttpServletRequest request, int statusCode, String customCode, LogStatus logStatus, String requestBody, String nickName) {
LoginLog loginLog = loadLoginLogPort.findByClientIp(ClientIpParseUtil.getClientIp(request)).orElse(null);
String responseBody = loginLog != null ? loginLog.toSummaryString() : null;
AnonymousLog anonymousLog = AnonymousLog.createAnonymousLog(request, statusCode,customCode, logStatus, responseBody, requestBody, nickName);
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/clap/server/config/aop/LoggingAspect.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import clap.server.adapter.inbound.security.service.SecurityUserDetails;

import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus;
import clap.server.application.port.inbound.domain.LogService;
import clap.server.application.port.outbound.log.LoggingPort;
import clap.server.common.annotation.log.LogType;
import clap.server.exception.BaseException;
import com.fasterxml.jackson.core.JsonProcessingException;
Expand Down Expand Up @@ -34,7 +34,7 @@
@RequiredArgsConstructor
public class LoggingAspect {
private final ObjectMapper objectMapper;
private final LogService logService;
private final LoggingPort loggingPort;

@Pointcut("execution(* clap.server.adapter.inbound.web..*Controller.*(..))")
public void controllerMethods() {
Expand Down Expand Up @@ -77,7 +77,7 @@ public Object logApiRequests(ProceedingJoinPoint joinPoint) throws Throwable {
} else {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof SecurityUserDetails userDetails) {
logService.createMemberLog(request, statusCode, customCode, logStatus, result, getRequestBody(request), userDetails.getUserId());
loggingPort.createMemberLog(request, statusCode, customCode, logStatus, result, getRequestBody(request), userDetails.getUserId());
}
}
}
Expand All @@ -88,9 +88,9 @@ public Object logApiRequests(ProceedingJoinPoint joinPoint) throws Throwable {

private void handleLoginLog(int statusCode, HttpServletRequest request, String customCode, LogStatus logStatus, Object result) throws JsonProcessingException {
if (statusCode == HttpStatus.SC_OK) {
logService.createAnonymousLog(request, statusCode, customCode, logStatus, result, getRequestBody(request), getNicknameFromRequestBody(request));
loggingPort.createAnonymousLog(request, statusCode, customCode, logStatus, result, getRequestBody(request), getNicknameFromRequestBody(request));
} else {
logService.createLoginFailedLog(request, statusCode, customCode, logStatus, getRequestBody(request), getNicknameFromRequestBody(request));
loggingPort.createLoginFailedLog(request, statusCode, customCode, logStatus, getRequestBody(request), getNicknameFromRequestBody(request));
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ spring:
multipart:
max-file-size: 5MB



server:
port: ${APPLICATION_PORT:8080}
tomcat:
Expand All @@ -29,6 +27,10 @@ server:
domain:
local: ${TASKFLOW_LOCAL_SERVER:127.0.0.1:8080}
service: ${TASKFLOW_SERVICE_SERVER:127.0.0.1:8080}
servlet.session.cookie:
http-only: true
path: /
secure: true

web:
domain:
Expand Down
35 changes: 31 additions & 4 deletions src/test/java/clap/server/TestDataFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public static Member createAdmin() {
.emailNotificationEnabled(true)
.imageUrl(null)
.status(MemberStatus.ACTIVE)
.password("1111")
.password("Password123!")
.department(createDepartment())
.build();
}
Expand All @@ -43,7 +43,7 @@ public static Member createManagerWithReviewer() {
.emailNotificationEnabled(true)
.imageUrl(null)
.status(MemberStatus.ACTIVE)
.password("1111")
.password("Password456!")
.department(createDepartment())
.build();
}
Expand All @@ -58,7 +58,7 @@ public static Member createManager() {
.emailNotificationEnabled(true)
.imageUrl(null)
.status(MemberStatus.ACTIVE)
.password("1111")
.password("Password789!")
.department(createDepartment())
.build();
}
Expand All @@ -73,7 +73,7 @@ public static Member createUser() {
.emailNotificationEnabled(true)
.imageUrl(null)
.status(MemberStatus.ACTIVE)
.password("1111")
.password("Password000!")
.department(createDepartment())
.build();
}
Expand All @@ -90,6 +90,33 @@ public static MemberInfo createAdminInfo() {
.build();
}

public static Member createNotApprovedUser() {
return Member.builder()
.memberId(4L)
.memberInfo(createNotApprovedUserInfo())
.admin(createAdmin())
.kakaoworkNotificationEnabled(true)
.agitNotificationEnabled(true)
.emailNotificationEnabled(true)
.imageUrl(null)
.status(MemberStatus.APPROVAL_REQUEST)
.password("Password000!")
.department(createDepartment())
.build();
}

public static MemberInfo createNotApprovedUserInfo() {
return MemberInfo.builder()
.name("홍길동(등록 대기중인 사용자)")
.email("atom8426@naver.com")
.nickname("atom.user")
.isReviewer(false)
.department(null)
.role(MemberRole.ROLE_USER)
.departmentRole("인프라")
.build();
}

public static MemberInfo createManagerWithReviewerInfo() {
return MemberInfo.builder()
.name("홍길동(리뷰어)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ class RegisterMemberCSVServiceTest {
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
// 예시로 Department가 빌더를 제공한다고 가정
dummyDepartment = Department.builder().departmentId(100L).build();
}

Expand Down Expand Up @@ -142,7 +141,6 @@ void testRegisterMembersFromCsv_duplicateThrowsException() throws Exception {
Member adminMember = mock(Member.class);
when(memberService.findActiveMember(adminId)).thenReturn(adminMember);

// 중복 체크: 닉네임 또는 email 중 하나라도 중복이 있으면 에러 발생
when(loadMemberPort.existsByNicknamesOrEmails(Set.of(dummyMemberInfo1.getNickname()), Set.of(dummyMemberInfo1.getEmail())))
.thenReturn(true);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package clap.server.application.service.auth;

import clap.server.TestDataFactory;
import clap.server.adapter.inbound.web.dto.auth.response.LoginResponse;
import clap.server.application.port.outbound.member.LoadMemberPort;
import clap.server.domain.model.auth.CustomJwts;
import clap.server.domain.model.member.Member;
import clap.server.exception.AuthException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class AuthServiceTest {

@InjectMocks
private AuthService authService;

@Mock
private LoadMemberPort loadMemberPort;
@Mock
private ManageTokenService manageTokenService;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private LoginAttemptService loginAttemptService;
@Mock
private RefreshTokenService refreshTokenService;

private Member user;
private Member notApprovedUser;

@BeforeEach
void setUp() {
user = TestDataFactory.createUser();
notApprovedUser = TestDataFactory.createNotApprovedUser();
}

@Test
@DisplayName("로그인 성공")
void loginSuccess() {
// Given
String nickname = "atom.user";
String inputPassword = "Password000!";
String clientIp = "127.0.0.1";
Member member = user;
CustomJwts jwtTokens = new CustomJwts("accessToken", "refreshToken");

when(loadMemberPort.findByNickname(nickname)).thenReturn(Optional.of(member));
when(passwordEncoder.matches(inputPassword, member.getPassword())).thenReturn(true);
when(manageTokenService.issueTokens(member)).thenReturn(jwtTokens);

// When
LoginResponse response = authService.login(nickname, inputPassword, clientIp);

// Then
assertNotNull(response);
assertEquals(jwtTokens.accessToken(), response.accessToken());
assertEquals(jwtTokens.refreshToken(), response.refreshToken());
verify(loginAttemptService).resetFailedAttempts(clientIp);
verify(refreshTokenService).saveRefreshToken(any());
}

@Test
@DisplayName("잘못된 비밀번호를 입력하면 로그인 실패한다.")
void loginFailureWrongPassword() {
// Given
String nickname = "atom.user";
String inputPassword = "wrongPassword000!";
String clientIp = "127.0.0.1";
Member member = user;

when(loadMemberPort.findByNickname(nickname)).thenReturn(Optional.of(member));
when(passwordEncoder.matches(inputPassword, member.getPassword())).thenReturn(false);

// When & Then
assertThrows(AuthException.class, () -> authService.login(nickname, inputPassword, clientIp));
verify(loginAttemptService).recordFailedAttempt(clientIp, nickname);
}


@Test
@DisplayName("사용자가 초기 로그인 시 임시 토큰이 발급된다.")
void loginWithApprovalRequestStatus() {
// Given
String nickname = "atom.user";
String inputPassword = "Password000!";
String clientIp = "127.0.0.1";

Member member = notApprovedUser;
String temporaryToken = "temporaryToken";

when(loadMemberPort.findByNickname(nickname)).thenReturn(Optional.of(member));
when(passwordEncoder.matches(inputPassword, member.getPassword())).thenReturn(true);
when(manageTokenService.issueTemporaryToken(notApprovedUser.getMemberId())).thenReturn(temporaryToken);

// When
LoginResponse response = authService.login(nickname, inputPassword, clientIp);

// Then
assertNotNull(response);
assertEquals(temporaryToken, response.accessToken());
assertNull(response.refreshToken());
verify(manageTokenService).issueTemporaryToken(notApprovedUser.getMemberId());
verify(manageTokenService, never()).issueTokens(any());
verify(refreshTokenService, never()).saveRefreshToken(any());
verify(loginAttemptService, never()).resetFailedAttempts(any());
}

}
Loading