Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: 알림 시스템 이벤트 기반 리팩토링 및 멀티플랫폼 지원 구현 완료 #615

Merged
merged 25 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c0452cf
refactor(Notification): 외부 알림 전송 로직 패키지 구조 변경 및 슬랙 알림 전송 로직과의 결합도 완화
limehee Nov 13, 2024
c7f39d2
refactor(Notification): 헥사고날 아키텍처의 구조에 맞게 클래스를 분리하고 추상화함
limehee Nov 13, 2024
f4d5a19
feat(Dependency): 디스코드 웹훅 연결을 위한 OpenFeign 의존성 추가
limehee Nov 13, 2024
448a3ee
feat(Notification): 디스코드 메시지 형식 정의
limehee Nov 13, 2024
0e1ec8a
refactor(Notification): Slack 도메인 패키지 위치 이동
limehee Nov 13, 2024
54d73a7
refactor(Notification): 비즈니스 로직에서 발행시킨 이벤트를 구독하여 플랫폼에 알림을 보내도록 변경, 알림…
limehee Nov 13, 2024
0ba7f82
refactor(Notification): SlackServiceHelper -> SlackWebhookClient 클래스명 변경
limehee Nov 13, 2024
67fc420
refactor(Notification): SlackWebhookClient 코드 리팩토링
limehee Nov 13, 2024
dd95f80
feat(Notification): Discord 웹훅 및 웹훅 클라이언트 인터페이스 추가
limehee Nov 14, 2024
4c737e3
refactor(Notification): 알림 관련 DTO 클래스명 및 패키지 위치 변경
limehee Nov 14, 2024
54b9b99
refactor(Dependency): OpenFeign 의존성 제거
limehee Nov 14, 2024
a1fda49
refactor(Dependency): OpenFeign 의존성 제거
limehee Nov 14, 2024
258a5be
refactor(Notification): Discord 웹훅 인라인 컬러 통일
limehee Nov 14, 2024
0920923
refactor(Notification): 알림 시스템 리팩토링에 따른 YAML 설정 변경
limehee Nov 14, 2024
f4d8813
refactor(Notification): YAML 주석 수정
limehee Nov 14, 2024
1a9f704
refactor(Notification): 주석 제거
limehee Nov 14, 2024
149cad9
refactor(Notification): 웹훅 추상 클래스 추가 및 공통 로직 분리
limehee Nov 14, 2024
7349882
refactor(Notification): 버튼 Swagger를 API Docs로 변경
limehee Nov 14, 2024
7b2b987
refactor(Notification): NotificationListener 리팩토링
limehee Nov 14, 2024
aa64979
refactor(Notification): AbstractWebhookClient 패키지 이동
limehee Nov 15, 2024
060fa83
refactor(Notification): API Docs 설명 변경
limehee Nov 17, 2024
0345e73
refactor(message): additionalMessage 변수명 변경
limehee Nov 17, 2024
3e78f20
refactor(NotificationSetting): 웹훅 알림 설정 변경 관련 네이밍을 update -> toggle 변경
limehee Nov 17, 2024
02c0ca9
refactor(NotificationSetting): ToggleNotificationSettingUseCase -> Ma…
limehee Nov 17, 2024
6b380f7
Merge branch 'develop' into refactor/#611
limehee Nov 27, 2024
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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ tasks.named('test') {
def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile

sourceSets {
main.java.srcDirs += [ querydslDir ]
main.java.srcDirs += [querydslDir]
}

tasks.withType(JavaCompile).configureEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.accountLockInfo.application.port.in.BanMemberUseCase;
Expand All @@ -11,8 +12,8 @@
import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto;
import page.clab.api.external.auth.redisToken.application.port.ExternalManageRedisTokenUseCase;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
import page.clab.api.global.common.slack.application.SlackService;
import page.clab.api.global.common.slack.domain.SecurityAlertType;
import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;

@Service
@RequiredArgsConstructor
Expand All @@ -22,15 +23,15 @@ public class MemberBanService implements BanMemberUseCase {
private final RegisterAccountLockInfoPort registerAccountLockInfoPort;
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
private final ExternalManageRedisTokenUseCase externalManageRedisTokenUseCase;
private final SlackService slackService;
private final ApplicationEventPublisher eventPublisher;

/**
* 멤버를 영구적으로 차단합니다.
*
* <p>해당 멤버의 계정 잠금 정보를 조회하고, 없으면 새로 생성합니다.
* Redis에 저장된 해당 멤버의 인증 토큰을 삭제하며, Slack에 밴 알림을 전송합니다.</p>
*
* @param request 현재 요청 객체
* @param request 현재 요청 객체
* @param memberId 차단할 멤버의 ID
* @return 저장된 계정 잠금 정보의 ID
*/
Expand Down Expand Up @@ -58,6 +59,8 @@ private AccountLockInfo createAccountLockInfo(String memberId) {

private void sendSlackBanNotification(HttpServletRequest request, String memberId) {
String memberName = externalRetrieveMemberUseCase.getMemberBasicInfoById(memberId).getMemberName();
slackService.sendSecurityAlertNotification(request, SecurityAlertType.MEMBER_BANNED, "ID: " + memberId + ", Name: " + memberName);
String memberBannedMessage = "ID: " + memberId + ", Name: " + memberName;
eventPublisher.publishEvent(
new NotificationEvent(this, SecurityAlertType.MEMBER_BANNED, request, memberBannedMessage));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.accountLockInfo.application.port.in.UnbanMemberUseCase;
Expand All @@ -10,8 +11,8 @@
import page.clab.api.domain.auth.accountLockInfo.domain.AccountLockInfo;
import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
import page.clab.api.global.common.slack.application.SlackService;
import page.clab.api.global.common.slack.domain.SecurityAlertType;
import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;

@Service
@RequiredArgsConstructor
Expand All @@ -20,15 +21,15 @@ public class MemberUnbanService implements UnbanMemberUseCase {
private final RetrieveAccountLockInfoPort retrieveAccountLockInfoPort;
private final RegisterAccountLockInfoPort registerAccountLockInfoPort;
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
private final SlackService slackService;
private final ApplicationEventPublisher eventPublisher;

/**
* 차단된 멤버를 해제합니다.
*
* <p>해당 멤버의 계정 잠금 정보를 조회하고 해제합니다.
* 해제된 정보는 저장되며, Slack에 해제 알림이 전송됩니다.</p>
*
* @param request 현재 요청 객체
* @param request 현재 요청 객체
* @param memberId 해제할 멤버의 ID
* @return 업데이트된 계정 잠금 정보의 ID
*/
Expand All @@ -55,6 +56,8 @@ private AccountLockInfo createAccountLockInfo(String memberId) {

private void sendSlackUnbanNotification(HttpServletRequest request, String memberId) {
String memberName = externalRetrieveMemberUseCase.getMemberBasicInfoById(memberId).getMemberName();
slackService.sendSecurityAlertNotification(request, SecurityAlertType.MEMBER_UNBANNED, "ID: " + memberId + ", Name: " + memberName);
String memberUnbannedMessage = "ID: " + memberId + ", Name: " + memberName;
eventPublisher.publishEvent(
new NotificationEvent(this, SecurityAlertType.MEMBER_UNBANNED, request, memberUnbannedMessage));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.blacklistIp.application.dto.mapper.BlacklistIpDtoMapper;
Expand All @@ -10,26 +11,25 @@
import page.clab.api.domain.auth.blacklistIp.application.port.out.RegisterBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RetrieveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp;
import page.clab.api.global.common.slack.application.SlackService;
import page.clab.api.global.common.slack.domain.SecurityAlertType;
import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;

@Service
@RequiredArgsConstructor
public class BlacklistIpRegisterService implements RegisterBlacklistIpUseCase {

private final RegisterBlacklistIpPort registerBlacklistIpPort;
private final RetrieveBlacklistIpPort retrieveBlacklistIpPort;
private final SlackService slackService;
private final ApplicationEventPublisher eventPublisher;
private final BlacklistIpDtoMapper mapper;

/**
* 지정된 IP 주소를 블랙리스트에 등록합니다.
*
* <p>해당 IP 주소가 이미 블랙리스트에 존재하는지 확인하고,
* 존재하지 않을 경우 새롭게 등록합니다.
* 새로운 IP가 등록되면 Slack을 통해 보안 알림이 전송됩니다.</p>
* 존재하지 않을 경우 새롭게 등록합니다. 새로운 IP가 등록되면 Slack을 통해 보안 알림이 전송됩니다.</p>
*
* @param request 현재 요청 객체
* @param request 현재 요청 객체
* @param requestDto 블랙리스트에 추가할 IP 주소 정보를 담은 DTO
* @return 기존에 존재하거나 새로 추가된 블랙리스트 IP 주소
*/
Expand All @@ -42,7 +42,12 @@ public String registerBlacklistIp(HttpServletRequest request, BlacklistIpRequest
.orElseGet(() -> {
BlacklistIp blacklistIp = mapper.fromDto(requestDto);
registerBlacklistIpPort.save(blacklistIp);
slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_ADDED, "Added IP: " + ipAddress);

String blacklistAddedMessage = "Added IP: " + ipAddress;
eventPublisher.publishEvent(
new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_ADDED, request,
blacklistAddedMessage));

return ipAddress;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,31 @@

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.blacklistIp.application.port.in.RemoveBlacklistIpUseCase;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RemoveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RetrieveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp;
import page.clab.api.global.common.slack.application.SlackService;
import page.clab.api.global.common.slack.domain.SecurityAlertType;
import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;

@Service
@RequiredArgsConstructor
public class BlacklistIpRemoveService implements RemoveBlacklistIpUseCase {

private final RetrieveBlacklistIpPort retrieveBlacklistIpPort;
private final RemoveBlacklistIpPort removeBlacklistIpPort;
private final SlackService slackService;
private final ApplicationEventPublisher eventPublisher;

/**
* 지정된 IP 주소를 블랙리스트에서 제거합니다.
*
* <p>블랙리스트에 등록된 IP 주소 정보를 조회하고 해당 정보를 삭제합니다.
* 삭제가 완료되면 Slack을 통해 보안 알림이 전송됩니다.</p>
*
* @param request 현재 요청 객체
* @param request 현재 요청 객체
* @param ipAddress 제거할 블랙리스트 IP 주소
* @return 삭제된 블랙리스트 IP 주소
*/
Expand All @@ -34,7 +35,12 @@ public class BlacklistIpRemoveService implements RemoveBlacklistIpUseCase {
public String removeBlacklistIp(HttpServletRequest request, String ipAddress) {
BlacklistIp blacklistIp = retrieveBlacklistIpPort.getByIpAddress(ipAddress);
removeBlacklistIpPort.delete(blacklistIp);
slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_REMOVED, "Deleted IP: " + ipAddress);

String blacklistRemovedMessage = "Deleted IP: " + ipAddress;
eventPublisher.publishEvent(
new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_REMOVED, request,
blacklistRemovedMessage));

return blacklistIp.getIpAddress();
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
package page.clab.api.domain.auth.blacklistIp.application.service;

import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.blacklistIp.application.port.in.ResetBlacklistIpsUseCase;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RemoveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RetrieveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp;
import page.clab.api.global.common.slack.application.SlackService;
import page.clab.api.global.common.slack.domain.SecurityAlertType;

import java.util.List;
import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;

@Service
@RequiredArgsConstructor
public class BlacklistIpResetService implements ResetBlacklistIpsUseCase {

private final RetrieveBlacklistIpPort retrieveBlacklistIpPort;
private final RemoveBlacklistIpPort removeBlacklistIpPort;
private final SlackService slackService;
private final ApplicationEventPublisher eventPublisher;

/**
* 블랙리스트에 등록된 모든 IP 주소를 초기화합니다.
Expand All @@ -38,7 +38,12 @@ public List<String> resetBlacklistIps(HttpServletRequest request) {
.map(BlacklistIp::getIpAddress)
.toList();
removeBlacklistIpPort.deleteAll();
slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_REMOVED, "Deleted IP: ALL");

String blacklistRemovedMessage = "Deleted IP: ALL";
eventPublisher.publishEvent(
new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_REMOVED, request,
blacklistRemovedMessage));

return blacklistedIps;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package page.clab.api.domain.auth.login.application.service;

import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.accountAccessLog.domain.AccountAccessResult;
Expand All @@ -21,11 +23,10 @@
import page.clab.api.external.auth.redisToken.application.port.ExternalManageRedisTokenUseCase;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
import page.clab.api.global.auth.jwt.JwtTokenProvider;
import page.clab.api.global.common.slack.application.SlackService;
import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
import page.clab.api.global.common.notificationSetting.domain.GeneralAlertType;
import page.clab.api.global.util.HttpReqResUtil;

import java.util.List;

@Service
@RequiredArgsConstructor
@Qualifier("twoFactorAuthenticationService")
Expand All @@ -36,12 +37,14 @@ public class TwoFactorAuthenticationService implements ManageLoginUseCase {
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
private final ExternalRegisterAccountAccessLogUseCase externalRegisterAccountAccessLogUseCase;
private final ExternalManageRedisTokenUseCase externalManageRedisTokenUseCase;
private final SlackService slackService;
private final ApplicationEventPublisher eventPublisher;
private final JwtTokenProvider jwtTokenProvider;

@Transactional
@Override
public LoginResult authenticate(HttpServletRequest request, TwoFactorAuthenticationRequestDto twoFactorAuthenticationRequestDto) throws LoginFailedException, MemberLockedException {
public LoginResult authenticate(HttpServletRequest request,
TwoFactorAuthenticationRequestDto twoFactorAuthenticationRequestDto)
throws LoginFailedException, MemberLockedException {
String memberId = twoFactorAuthenticationRequestDto.getMemberId();
MemberLoginInfoDto loginMember = externalRetrieveMemberUseCase.getMemberLoginInfoById(memberId);
String totp = twoFactorAuthenticationRequestDto.getTotp();
Expand All @@ -55,9 +58,11 @@ public LoginResult authenticate(HttpServletRequest request, TwoFactorAuthenticat
return LoginResult.create(header, true);
}

private void verifyTwoFactorAuthentication(String memberId, String totp, HttpServletRequest request) throws MemberLockedException, LoginFailedException {
private void verifyTwoFactorAuthentication(String memberId, String totp, HttpServletRequest request)
throws MemberLockedException, LoginFailedException {
if (!manageAuthenticatorUseCase.isAuthenticatorValid(memberId, totp)) {
externalRegisterAccountAccessLogUseCase.registerAccountAccessLog(request, memberId, AccountAccessResult.FAILURE);
externalRegisterAccountAccessLogUseCase.registerAccountAccessLog(request, memberId,
AccountAccessResult.FAILURE);
SongJaeHoonn marked this conversation as resolved.
Show resolved Hide resolved
externalManageAccountLockUseCase.handleLoginFailure(request, memberId);
throw new LoginFailedException("잘못된 인증번호입니다.");
}
Expand All @@ -67,18 +72,21 @@ private void verifyTwoFactorAuthentication(String memberId, String totp, HttpSer
private TokenInfo generateAndSaveToken(MemberLoginInfoDto memberInfo) {
TokenInfo tokenInfo = jwtTokenProvider.generateToken(memberInfo.getMemberId(), memberInfo.getRole());
String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist();
externalManageRedisTokenUseCase.saveToken(memberInfo.getMemberId(), memberInfo.getRole(), tokenInfo, clientIpAddress);
externalManageRedisTokenUseCase.saveToken(memberInfo.getMemberId(), memberInfo.getRole(), tokenInfo,
clientIpAddress);
return tokenInfo;
}

private void sendAdminLoginNotification(HttpServletRequest request, MemberLoginInfoDto loginMember) {
if (loginMember.isSuperAdminRole()) {
slackService.sendAdminLoginNotification(request, loginMember);
eventPublisher.publishEvent(
new NotificationEvent(this, GeneralAlertType.ADMIN_LOGIN, request, loginMember));
}
}

@Override
public LoginResult login(HttpServletRequest request, LoginRequestDto requestDto) throws LoginFailedException, MemberLockedException {
public LoginResult login(HttpServletRequest request, LoginRequestDto requestDto)
throws LoginFailedException, MemberLockedException {
throw new UnsupportedOperationException("Method not implemented");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,29 @@

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.in.RemoveAbnormalAccessIpUseCase;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.RemoveIpAccessMonitorPort;
import page.clab.api.global.common.slack.application.SlackService;
import page.clab.api.global.common.slack.domain.SecurityAlertType;
import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;

@Service
@RequiredArgsConstructor
public class AbnormalAccessIpRemoveService implements RemoveAbnormalAccessIpUseCase {

private final RemoveIpAccessMonitorPort removeIpAccessMonitorPort;
private final SlackService slackService;
private final ApplicationEventPublisher eventPublisher;

@Override
@Transactional
public String removeAbnormalAccessIp(HttpServletRequest request, String ipAddress) {
removeIpAccessMonitorPort.deleteById(ipAddress);
slackService.sendSecurityAlertNotification(request, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, "Deleted IP: " + ipAddress);
String abnormalAccessIpDeletedMessage = "Deleted IP: " + ipAddress;
eventPublisher.publishEvent(
new NotificationEvent(this, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, request,
abnormalAccessIpDeletedMessage));
return ipAddress;
}
}
Loading