Skip to content
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
Expand Up @@ -51,7 +51,7 @@ public List<Category> findSubCategory() {

@Override
public boolean existsByNameOrCode(String name, String code) {
return categoryRepository.existsByNameOrCode(name, code);
return categoryRepository.existsByNameOrCodeAndIsDeletedFalse(name, code);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ public interface CategoryRepository extends JpaRepository<CategoryEntity, Long>
List<CategoryEntity> findByIsDeletedFalseAndMainCategoryIsNull();
List<CategoryEntity> findByIsDeletedFalseAndMainCategoryIsNotNull();

boolean existsByNameOrCode(String name, String code);
boolean existsByNameOrCodeAndIsDeletedFalse(String name, String code);
}
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 @@ -12,6 +12,7 @@
import org.springframework.transaction.annotation.Transactional;

import static clap.server.exception.code.MemberErrorCode.ACTIVE_MEMBER_NOT_FOUND;
import static clap.server.exception.code.TaskErrorCode.CATEGORY_DUPLICATE;
import static clap.server.exception.code.TaskErrorCode.CATEGORY_NOT_FOUND;

@ApplicationService
Expand All @@ -25,6 +26,7 @@ public class UpdateCategoryService implements UpdateCategoryUsecase {
@Transactional
public void updateCategory(Long adminId, Long categoryId, String name, String code, String descriptionExample) {
Member admin = loadMemberPort.findActiveMemberById(adminId).orElseThrow(() -> new ApplicationException(ACTIVE_MEMBER_NOT_FOUND));
if (loadCategoryPort.existsByNameOrCode(name, code)) throw new ApplicationException(CATEGORY_DUPLICATE);
Category category = loadCategoryPort.findById(categoryId)
.orElseThrow(() -> new ApplicationException(CATEGORY_NOT_FOUND));
category.updateCategory(admin, name, code, descriptionExample);
Expand Down
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