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,7 +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;
import jakarta.servlet.ServletException;
Expand All @@ -14,8 +13,10 @@
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

import static clap.server.adapter.inbound.security.WebSecurityUrl.LOGIN_ENDPOINT;
Expand All @@ -33,9 +34,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
throws ServletException, IOException {
try {
if (request.getRequestURI().equals(LOGIN_ENDPOINT)) {
String clientIp = getClientIp(request);

checkAccountLockStatusUseCase.checkAccountIsLocked(clientIp);
String nickname = request.getParameter("nickname");
checkAccountLockStatusUseCase.checkAccountIsLocked(nickname);

}
} catch (AuthException e) {
Expand All @@ -54,4 +54,14 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
filterChain.doFilter(request, response);
}

private String getRequestBody(HttpServletRequest request) {
try {
ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request;
byte[] content = cachingRequest.getContentAsByteArray();
return new String(content, StandardCharsets.UTF_8);
} catch (Exception e) {
return "요청 바디의 내용을 읽을 수 없음";
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
Expand All @@ -32,11 +33,11 @@ public class AuthController {
@LogType(LogStatus.LOGIN)
@Operation(summary = "로그인 API")
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(
public ResponseEntity<LoginResponse> login(@RequestParam @NotBlank String nickname,
@RequestBody LoginRequest request,
HttpServletRequest httpRequest) {
String clientIp = getClientIp(httpRequest);
LoginResponse response = loginUsecase.login(request.nickname(), request.password(), clientIp);
LoginResponse response = loginUsecase.login(nickname, request.password(), clientIp);
return ResponseEntity.ok(response);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package clap.server.adapter.inbound.web.dto.auth.request;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;

public record LoginRequest(
@NotNull
String nickname,
@NotNull
@NotBlank
String password
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public void deleteById(String clientIp) {
loginLogRepository.deleteById(clientIp);
}

public Optional<LoginLog> findByClientIp(String clientIp) {
return loginLogRepository.findById(clientIp).map(loginLogMapper::toDomain);
@Override
public Optional<LoginLog> findByNickname(String nickname) {
return loginLogRepository.findById(nickname).map(loginLogMapper::toDomain);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
import java.time.LocalDateTime;

@Getter
@RedisHash("loginLog")
@RedisHash(value = "loginLog", timeToLive = 3600)
@Builder
@ToString(of = {"clientIp", "attemptNickname", "lastAttemptAt", "failedCount", "isLocked"})
@EqualsAndHashCode(of = {"clientIp"})
@ToString(of = {"nickname", "clientIp", "lastAttemptAt", "failedCount", "isLocked"})
@EqualsAndHashCode(of = {"nickname"})
public class LoginLogEntity {
@Id
private String clientIp;
private String nickname;

private String attemptNickname;
private String clientIp;

@JsonSerialize(using = ToStringSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package clap.server.application.port.inbound.auth;

public interface CheckAccountLockStatusUseCase {
void checkAccountIsLocked(String clientIp);
void checkAccountIsLocked(String nickname);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
import java.util.Optional;

public interface LoadLoginLogPort {
Optional<LoginLog> findByClientIp(String clientIp);
Optional<LoginLog> findByNickname(String nickname);
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public LoginResponse login(String nickname, String password, String clientIp) {

CustomJwts jwtTokens = manageTokenService.issueTokens(member);
refreshTokenService.saveRefreshToken(manageTokenService.issueRefreshToken(member.getMemberId()));
loginAttemptService.resetFailedAttempts(clientIp);
loginAttemptService.resetFailedAttempts(nickname);
return AuthResponseMapper.toLoginResponse(jwtTokens.accessToken(), jwtTokens.refreshToken());
}

Expand All @@ -71,14 +71,14 @@ private void deleteAccessToken(Long memberId, String accessToken) {
private Member getMember(String inputNickname, String clientIp) {
return loadMemberPort.findByNickname(inputNickname).orElseThrow(() ->
{
loginAttemptService.recordFailedAttempt(clientIp, inputNickname);
loginAttemptService.recordFailedAttempt(inputNickname, clientIp);
return new AuthException(AuthErrorCode.LOGIN_REQUEST_FAILED);
});
}

private void validatePassword(String inputPassword, String encodedPassword, String inputNickname, String clientIp) {
if (!passwordEncoder.matches(inputPassword, encodedPassword)) {
loginAttemptService.recordFailedAttempt(clientIp, inputNickname);
loginAttemptService.recordFailedAttempt(inputNickname, clientIp);
throw new AuthException(AuthErrorCode.LOGIN_REQUEST_FAILED);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ public class LoginAttemptService implements CheckAccountLockStatusUseCase {
private static final int MAX_FAILED_ATTEMPTS = 5;
private static final long LOCK_TIME_DURATION = 30 * 60 * 1000; // 30분 (밀리초)

public void recordFailedAttempt(String clientIp, String attemptNickname) {
LoginLog loginLog = loadLoginLogPort.findByClientIp(clientIp).orElse(null);
public void recordFailedAttempt(String nickname, String clientIp) {
LoginLog loginLog = loadLoginLogPort.findByNickname(nickname).orElse(null);
if (loginLog == null) {
loginLog = LoginLog.createLoginLog(clientIp, attemptNickname);
loginLog = LoginLog.createLoginLog(nickname, clientIp);
} else {
int attemptCount = loginLog.recordFailedAttempt();
if (attemptCount >= MAX_FAILED_ATTEMPTS) {
Expand All @@ -38,8 +38,8 @@ public void recordFailedAttempt(String clientIp, String attemptNickname) {
}

@Override
public void checkAccountIsLocked(String clientIp) {
LoginLog loginLog = loadLoginLogPort.findByClientIp(clientIp).orElse(null);
public void checkAccountIsLocked(String nickname) {
LoginLog loginLog = loadLoginLogPort.findByNickname(nickname).orElse(null);
if (loginLog == null) {
return;
}
Expand All @@ -53,12 +53,12 @@ public void checkAccountIsLocked(String clientIp) {
if (minutesSinceLastAttemptInMillis <= LOCK_TIME_DURATION) {
throw new AuthException(AuthErrorCode.ACCOUNT_IS_LOCKED);
}
else commandLoginLogPort.deleteById(clientIp);
else commandLoginLogPort.deleteById(nickname);
}
}


public void resetFailedAttempts(String clientIp) {
commandLoginLogPort.deleteById(clientIp);
public void resetFailedAttempts(String nickname) {
commandLoginLogPort.deleteById(nickname);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public void createMemberLog(HttpServletRequest request, int statusCode, String c
}

public void createLoginFailedLog(HttpServletRequest request, int statusCode, String customCode, LogStatus logStatus, String requestBody, String nickName) {
LoginLog loginLog = loadLoginLogPort.findByClientIp(ClientIpParseUtil.getClientIp(request)).orElse(null);
LoginLog loginLog = loadLoginLogPort.findByNickname(nickName).orElse(null);
String responseBody = loginLog != null ? loginLog.toSummaryString() : null;
AnonymousLog anonymousLog = AnonymousLog.createAnonymousLog(request, statusCode,customCode, logStatus, responseBody, requestBody, nickName);
commandLogPort.saveAnonymousLog(anonymousLog);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import clap.server.application.port.outbound.auth.forbidden.ForbiddenTokenPort;
import clap.server.application.port.outbound.auth.JwtProvider;
import clap.server.application.service.auth.LoginAttemptService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
Expand Down
54 changes: 27 additions & 27 deletions src/main/java/clap/server/domain/model/auth/LoginLog.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,35 @@
@SuperBuilder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LoginLog {
private String clientIp;
private String attemptNickname;
private LocalDateTime lastAttemptAt;
private int failedCount;
private boolean isLocked;
private String nickname;
private String clientIp;
private LocalDateTime lastAttemptAt;
private int failedCount;
private boolean isLocked;

public static LoginLog createLoginLog(String clientIp, String attemptNickname) {
return LoginLog.builder()
.clientIp(clientIp)
.attemptNickname(attemptNickname)
.lastAttemptAt(LocalDateTime.now())
.failedCount(1)
.isLocked(false)
.build();
}
public static LoginLog createLoginLog(String nickname, String clientIp) {
return LoginLog.builder()
.nickname(nickname)
.clientIp(clientIp)
.lastAttemptAt(LocalDateTime.now())
.failedCount(1)
.isLocked(false)
.build();
}

public int recordFailedAttempt() {
this.failedCount++;
return this.failedCount;
}
public int recordFailedAttempt() {
this.failedCount++;
return this.failedCount;
}

public void setLocked(boolean locked) {
isLocked = locked;
}
public void setLocked(boolean locked) {
isLocked = locked;
}

public String toSummaryString() {
return "{" +
", failedCount=" + failedCount +
", isLocked=" + isLocked +
'}';
}
public String toSummaryString() {
return "{" +
"failedCount=" + failedCount +
", isLocked=" + isLocked +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
@Policy
public class ManagerDepartmentPolicy {
public void validateDepartment(final Department department, final MemberRole memberRole) {
if (!(department.isManager()
&& memberRole == MemberRole.ROLE_MANAGER)) {
throw new DomainException(MemberErrorCode.MANAGER_PERMISSION_DENIED);
if (!department.isManager() ){
if(memberRole == MemberRole.ROLE_MANAGER){
throw new DomainException(MemberErrorCode.MANAGER_PERMISSION_DENIED);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ void loginSuccess() {
assertNotNull(response);
assertEquals(jwtTokens.accessToken(), response.accessToken());
assertEquals(jwtTokens.refreshToken(), response.refreshToken());
verify(loginAttemptService).resetFailedAttempts(clientIp);
verify(loginAttemptService).resetFailedAttempts(nickname);
verify(refreshTokenService).saveRefreshToken(any());
}

Expand All @@ -85,7 +85,7 @@ void loginFailureWrongPassword() {

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


Expand Down
Loading