From 83af91907ee5168d0879c58f38ed2c2a94bea76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= <85067003+limehee@users.noreply.github.com> Date: Mon, 13 May 2024 17:25:40 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8A=AC=EB=9E=99=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EA=B0=9C=EC=84=A0=20=EC=99=84=EB=A3=8C=20(#347)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(Slack): 중복 로그인에 대한 경고 대상을 관리자 이상의 멤버로 제한 및 사용자 아이디를 불러올 수 있도록 변경 * refactor(Slack): 지속적인 로그인 실패에 대한 경고 대상을 관리자 이상의 멤버로 제한 및 사용자 아이디를 불러올 수 있도록 변경 * refactor(Slack): 미리보기에 알림 제목이 표시되도록 개선 --- .../application/AccountLockInfoService.java | 11 +++++- .../api/domain/login/domain/RedisToken.java | 4 ++ .../auth/filter/JwtAuthenticationFilter.java | 9 ++++- .../slack/application/SlackService.java | 39 +++++++++++++------ 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/main/java/page/clab/api/domain/login/application/AccountLockInfoService.java b/src/main/java/page/clab/api/domain/login/application/AccountLockInfoService.java index f5dc7bb61..f372ec533 100644 --- a/src/main/java/page/clab/api/domain/login/application/AccountLockInfoService.java +++ b/src/main/java/page/clab/api/domain/login/application/AccountLockInfoService.java @@ -76,13 +76,13 @@ public void handleAccountLockInfo(String memberId) throws MemberLockedException, @Transactional public void handleLoginFailure(HttpServletRequest request, String memberId) throws MemberLockedException, LoginFaliedException { + Member member = memberService.getMemberById(memberId); AccountLockInfo accountLockInfo = ensureAccountLockInfoForMemberId(memberId); validateAccountLockStatus(accountLockInfo); accountLockInfo.incrementLoginFailCount(); if (accountLockInfo.shouldBeLocked(maxLoginFailures)) { accountLockInfo.lockAccount(lockDurationMinutes); - slackService.sendSecurityAlertNotification(request, SecurityAlertType.REPEATED_LOGIN_FAILURES, - "[" + accountLockInfo.getMember().getId() + "/" + accountLockInfo.getMember().getName() + "]" + " 로그인 실패 횟수 초과로 계정이 잠겼습니다."); + sendSlackMessage(request, member); } accountLockInfoRepository.save(accountLockInfo); } @@ -109,4 +109,11 @@ private void validateAccountLockStatus(AccountLockInfo accountLockInfo) throws M } } + private void sendSlackMessage(HttpServletRequest request, Member member) { + if (member.isAdminRole()) { + request.setAttribute("member", member.getId() + " " + member.getName()); + slackService.sendSecurityAlertNotification(request, SecurityAlertType.REPEATED_LOGIN_FAILURES, "로그인 실패 횟수 초과로 계정이 잠겼습니다."); + } + } + } diff --git a/src/main/java/page/clab/api/domain/login/domain/RedisToken.java b/src/main/java/page/clab/api/domain/login/domain/RedisToken.java index 3be84a176..b2929f859 100644 --- a/src/main/java/page/clab/api/domain/login/domain/RedisToken.java +++ b/src/main/java/page/clab/api/domain/login/domain/RedisToken.java @@ -49,4 +49,8 @@ public boolean isSameIp(String ip) { return this.ip.equals(ip); } + public boolean isAdminToken() { + return role == Role.ADMIN || role == Role.SUPER; + } + } \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java b/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java index c408b5a68..83773232a 100644 --- a/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java +++ b/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java @@ -77,7 +77,7 @@ private boolean authenticateToken(HttpServletRequest request, HttpServletRespons } if (!redisToken.getIp().equals(clientIpAddress)) { redisTokenService.deleteRedisTokenByAccessToken(token); - slackService.sendSecurityAlertNotification(request, SecurityAlertType.DUPLICATE_LOGIN, "토큰 발급 IP와 다른 IP에서 접속하여 토큰을 삭제하였습니다."); + sendSlackMessage(request, redisToken); log.warn("[{}] 토큰 발급 IP와 다른 IP에서 접속하여 토큰을 삭제하였습니다.", clientIpAddress); ResponseUtil.sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED); return false; @@ -90,4 +90,11 @@ private boolean authenticateToken(HttpServletRequest request, HttpServletRespons } } + private void sendSlackMessage(HttpServletRequest request, RedisToken redisToken) { + if (redisToken.isAdminToken()) { + request.setAttribute("member", redisToken.getId()); + slackService.sendSecurityAlertNotification(request, SecurityAlertType.DUPLICATE_LOGIN, "토큰 발급 IP와 다른 IP에서 접속하여 토큰을 삭제하였습니다."); + } + } + } \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/common/slack/application/SlackService.java b/src/main/java/page/clab/api/global/common/slack/application/SlackService.java index 0dfdff842..48c76eeb3 100644 --- a/src/main/java/page/clab/api/global/common/slack/application/SlackService.java +++ b/src/main/java/page/clab/api/global/common/slack/application/SlackService.java @@ -2,7 +2,6 @@ import com.slack.api.Slack; import com.slack.api.model.Attachment; -import com.slack.api.model.Attachments; import static com.slack.api.model.block.Blocks.actions; import static com.slack.api.model.block.Blocks.section; import com.slack.api.model.block.LayoutBlock; @@ -16,6 +15,7 @@ import io.ipinfo.spring.strategies.attribute.AttributeStrategy; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.core.env.Environment; @@ -37,6 +37,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -97,10 +98,11 @@ public void sendServerStartNotification() { private CompletableFuture sendSlackMessageWithBlocks(List blocks) { return CompletableFuture.supplyAsync(() -> { Payload payload = Payload.builder() + .blocks(List.of(blocks.getFirst())) .attachments(Collections.singletonList( Attachment.builder() .color(color) - .blocks(blocks) + .blocks(blocks.subList(1, blocks.size())) .build() )).build(); try { @@ -121,9 +123,7 @@ private CompletableFuture sendSlackMessageWithBlocks(List private List createErrorBlocks(HttpServletRequest request, Exception e) { String httpMethod = request.getMethod(); String requestUrl = request.getRequestURI(); - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String username = (authentication == null || authentication.getName() == null) ? "anonymous" : authentication.getName(); + String username = getUsername(); String errorMessage = e.getMessage() == null ? "No error message provided" : e.getMessage(); String detailedMessage = extractMessageAfterException(errorMessage); @@ -142,11 +142,8 @@ private List createErrorBlocks(HttpServletRequest request, Exceptio private List createSecurityAlertBlocks(HttpServletRequest request, SecurityAlertType alertType, String additionalMessage) { String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); String requestUrl = request.getRequestURI(); - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String username = (authentication == null || authentication.getName() == null) ? "anonymous" : authentication.getName(); - - IPResponse ipResponse = attributeStrategy.getAttribute(request); - String location = ipResponse == null ? "Unknown" : ipResponse.getCountryName() + ", " + ipResponse.getCity(); + String username = getUsername(request); + String location = getLocation(request); return Arrays.asList( section(section -> section.text(markdownText(String.format(":imp: *%s*", alertType.getTitle())))), @@ -162,8 +159,7 @@ private List createSecurityAlertBlocks(HttpServletRequest request, private List createAdminLoginBlocks(HttpServletRequest request, Member loginMember) { String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); - IPResponse ipResponse = attributeStrategy.getAttribute(request); - String location = ipResponse == null ? "Unknown" : ipResponse.getCountryName() + ", " + ipResponse.getCity(); + String location = getLocation(request); return Arrays.asList( section(section -> section.text(markdownText(String.format(":mechanic: *%s Login*", loginMember.getRole().getDescription())))), @@ -249,4 +245,23 @@ private String formatMemoryUsage(MemoryUsage memoryUsage) { return String.format("%dMB / %dMB (%.2f%%)", usedMemory, maxMemory, ((double) usedMemory / maxMemory) * 100); } + private static String getUsername() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return (authentication == null || authentication.getName() == null) ? "anonymous" : authentication.getName(); + } + + private @NotNull String getUsername(HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return Optional.ofNullable(request.getAttribute("member")) + .map(Object::toString) + .orElseGet(() -> Optional.ofNullable(authentication) + .map(Authentication::getName) + .orElse("anonymous")); + } + + private @NotNull String getLocation(HttpServletRequest request) { + IPResponse ipResponse = attributeStrategy.getAttribute(request); + return ipResponse == null ? "Unknown" : ipResponse.getCountryName() + ", " + ipResponse.getCity(); + } + }