From 229e0956dfa8b4761619f145e52e36953ad7a8f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Wed, 10 Jul 2024 02:26:52 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8A=A4=EC=BF=BC=EC=8B=9C=20=EB=A8=B8?= =?UTF-8?q?=EC=A7=80:=20feat/#397?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomBasicAuthenticationFilter.java | 40 ++++++++++++--- .../auth/filter/JwtAuthenticationFilter.java | 13 ++--- .../slack/domain/SecurityAlertType.java | 3 +- .../api/global/config/SecurityConfig.java | 17 ++----- .../config/WhitelistPatternsProperties.java | 13 ++++- .../clab/api/global/util/SwaggerUtil.java | 33 ------------ .../clab/api/global/util/WhitelistUtil.java | 51 +++++++++++++++++++ 7 files changed, 103 insertions(+), 67 deletions(-) delete mode 100644 src/main/java/page/clab/api/global/util/SwaggerUtil.java create mode 100644 src/main/java/page/clab/api/global/util/WhitelistUtil.java diff --git a/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java b/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java index 231cf24c5..56f3f1626 100644 --- a/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java +++ b/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java @@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -14,9 +15,11 @@ import page.clab.api.domain.blacklistIp.dao.BlacklistIpRepository; import page.clab.api.global.auth.application.RedisIpAccessMonitorService; import page.clab.api.global.auth.application.WhitelistService; +import page.clab.api.global.common.slack.application.SlackService; +import page.clab.api.global.common.slack.domain.SecurityAlertType; import page.clab.api.global.util.HttpReqResUtil; import page.clab.api.global.util.ResponseUtil; -import page.clab.api.global.util.SwaggerUtil; +import page.clab.api.global.util.WhitelistUtil; import java.io.IOException; import java.util.Base64; @@ -26,28 +29,29 @@ public class CustomBasicAuthenticationFilter extends BasicAuthenticationFilter { private final RedisIpAccessMonitorService redisIpAccessMonitorService; - private final BlacklistIpRepository blacklistIpRepository; - private final WhitelistService whitelistService; + private final SlackService slackService; public CustomBasicAuthenticationFilter( AuthenticationManager authenticationManager, RedisIpAccessMonitorService redisIpAccessMonitorService, BlacklistIpRepository blacklistIpRepository, - WhitelistService whitelistService + WhitelistService whitelistService, + SlackService slackService ) { super(authenticationManager); this.redisIpAccessMonitorService = redisIpAccessMonitorService; this.blacklistIpRepository = blacklistIpRepository; this.whitelistService = whitelistService; + this.slackService = slackService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String path = request.getRequestURI(); - if (!SwaggerUtil.isSwaggerRequest(path)) { + if (!WhitelistUtil.isWhitelistRequest(path)) { chain.doFilter(request, response); return; } @@ -75,12 +79,15 @@ private boolean authenticateUserCredentials(HttpServletRequest request, HttpServ String username = credentials[0]; String password = credentials[1]; UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); - Authentication authentication = getAuthenticationManager().authenticate(authRequest); - if (authentication == null) { + try { + Authentication authentication = getAuthenticationManager().authenticate(authRequest); + SecurityContextHolder.getContext().setAuthentication(authentication); + sendAuthenticationSuccessAlertSlackMessage(request); + } catch (BadCredentialsException e) { + sendAuthenticationFailureAlertSlackMessage(request); ResponseUtil.sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED); return false; } - SecurityContextHolder.getContext().setAuthentication(authentication); return true; } @@ -105,4 +112,21 @@ private static String[] decodeCredentials(String authorizationHeader) { return credentials.split(":", 2); } + private void sendAuthenticationSuccessAlertSlackMessage(HttpServletRequest request) { + String path = request.getRequestURI(); + if (WhitelistUtil.isSwaggerIndexEndpoint(path)) { + slackService.sendSecurityAlertNotification(request, SecurityAlertType.API_DOCS_ACCESS,"API 문서에 대한 접근이 허가되었습니다."); + } else if (WhitelistUtil.isActuatorRequest(path)) { + slackService.sendSecurityAlertNotification(request, SecurityAlertType.ACTUATOR_ACCESS,"Actuator에 대한 접근이 허가되었습니다."); + } + } + + private void sendAuthenticationFailureAlertSlackMessage(HttpServletRequest request) { + String path = request.getRequestURI(); + if (WhitelistUtil.isSwaggerIndexEndpoint(path)) { + slackService.sendSecurityAlertNotification(request, SecurityAlertType.API_DOCS_ACCESS,"API 문서에 대한 접근이 거부되었습니다."); + } else if (WhitelistUtil.isActuatorRequest(path)) { + slackService.sendSecurityAlertNotification(request, SecurityAlertType.ACTUATOR_ACCESS,"Actuator에 대한 접근이 거부되었습니다."); + } + } } \ 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 a35181dfe..374b7fe6c 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 @@ -20,7 +20,7 @@ import page.clab.api.global.common.slack.domain.SecurityAlertType; import page.clab.api.global.util.HttpReqResUtil; import page.clab.api.global.util.ResponseUtil; -import page.clab.api.global.util.SwaggerUtil; +import page.clab.api.global.util.WhitelistUtil; import java.io.IOException; @@ -29,13 +29,9 @@ public class JwtAuthenticationFilter extends GenericFilterBean { private final JwtTokenProvider jwtTokenProvider; - private final ManageRedisTokenUseCase manageRedisTokenUseCase; - private final RedisIpAccessMonitorService redisIpAccessMonitorService; - private final SlackService slackService; - private final BlacklistIpRepository blacklistIpRepository; @Override @@ -43,7 +39,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; String path = httpServletRequest.getRequestURI(); - if (SwaggerUtil.isSwaggerRequest(path)) { + if (WhitelistUtil.isWhitelistRequest(path)) { chain.doFilter(request, response); return; } @@ -77,7 +73,7 @@ private boolean authenticateToken(HttpServletRequest request, HttpServletRespons } if (!redisToken.getIp().equals(clientIpAddress)) { manageRedisTokenUseCase.deleteByAccessToken(token); - sendSlackMessage(request, redisToken); + sendSecurityAlertSlackMessage(request, redisToken); log.warn("[{}] 토큰 발급 IP와 다른 IP에서 접속하여 토큰을 삭제하였습니다.", clientIpAddress); ResponseUtil.sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED); return false; @@ -90,11 +86,10 @@ private boolean authenticateToken(HttpServletRequest request, HttpServletRespons } } - private void sendSlackMessage(HttpServletRequest request, RedisToken redisToken) { + private void sendSecurityAlertSlackMessage(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/domain/SecurityAlertType.java b/src/main/java/page/clab/api/global/common/slack/domain/SecurityAlertType.java index a0921ba64..3ac2c1b28 100644 --- a/src/main/java/page/clab/api/global/common/slack/domain/SecurityAlertType.java +++ b/src/main/java/page/clab/api/global/common/slack/domain/SecurityAlertType.java @@ -10,6 +10,8 @@ public enum SecurityAlertType implements AlertType { ABNORMAL_ACCESS("비정상적인 접근", "Unexpected access pattern detected."), REPEATED_LOGIN_FAILURES("지속된 로그인 실패", "Multiple consecutive failed login attempts."), DUPLICATE_LOGIN("중복 로그인", "Duplicate login attempt."), + API_DOCS_ACCESS("API 문서 접근", "API Documentation access attempt."), + ACTUATOR_ACCESS("Actuator 접근", "Actuator endpoint access attempt."), UNAUTHORIZED_ACCESS("인가되지 않은 접근", "Unauthorized access attempt."), BLACKLISTED_IP_ADDED("블랙리스트 IP 등록", "IP address has been added to the blacklist."), BLACKLISTED_IP_REMOVED("블랙리스트 IP 해제", "IP address has been removed from the blacklist."), @@ -20,5 +22,4 @@ public enum SecurityAlertType implements AlertType { private final String title; private final String defaultMessage; - } diff --git a/src/main/java/page/clab/api/global/config/SecurityConfig.java b/src/main/java/page/clab/api/global/config/SecurityConfig.java index b3e3a64a4..44e355ed7 100644 --- a/src/main/java/page/clab/api/global/config/SecurityConfig.java +++ b/src/main/java/page/clab/api/global/config/SecurityConfig.java @@ -44,25 +44,15 @@ public class SecurityConfig { private final JwtTokenProvider jwtTokenProvider; - private final ManageRedisTokenUseCase manageRedisTokenUseCase; - private final RedisIpAccessMonitorService redisIpAccessMonitorService; - private final BlacklistIpRepository blacklistIpRepository; - private final SlackService slackService; - private final WhitelistService whitelistService; - private final CorsConfigurationSource corsConfigurationSource; - private final AuthenticationConfig authenticationConfig; - private final WhitelistAccountProperties whitelistAccountProperties; - - private final WhitelistPatternsProperties WhitelistPatternsProperties; - + private final WhitelistPatternsProperties whitelistPatternsProperties; private final IPInfoConfig ipInfoConfig; @Value("${resource.file.url}") @@ -94,7 +84,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { UsernamePasswordAuthenticationFilter.class ) .addFilterBefore( - new CustomBasicAuthenticationFilter(authenticationManager, redisIpAccessMonitorService, blacklistIpRepository, whitelistService), + new CustomBasicAuthenticationFilter(authenticationManager, redisIpAccessMonitorService, blacklistIpRepository, whitelistService, slackService), UsernamePasswordAuthenticationFilter.class ) .addFilterBefore( @@ -115,7 +105,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { private ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry configureRequests(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry authorizeRequests) { return authorizeRequests - .requestMatchers(WhitelistPatternsProperties.getPatterns()).hasRole(whitelistAccountProperties.getRole()) + .requestMatchers(whitelistPatternsProperties.getWhitelistPatterns()).hasRole(whitelistAccountProperties.getRole()) .requestMatchers(SecurityConstants.PERMIT_ALL).permitAll() .requestMatchers(HttpMethod.GET, SecurityConstants.PERMIT_ALL_API_ENDPOINTS_GET).permitAll() .requestMatchers(HttpMethod.POST, SecurityConstants.PERMIT_ALL_API_ENDPOINTS_POST).permitAll() @@ -144,5 +134,4 @@ private void apiLogging(HttpServletRequest request, HttpServletResponse response int httpStatus = response.getStatus(); log.info("[{}:{}] {} {} {} {}", clientIpAddress, id, requestUrl, httpMethod, httpStatus, message); } - } \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/config/WhitelistPatternsProperties.java b/src/main/java/page/clab/api/global/config/WhitelistPatternsProperties.java index 906d3c3f4..33ef866fe 100644 --- a/src/main/java/page/clab/api/global/config/WhitelistPatternsProperties.java +++ b/src/main/java/page/clab/api/global/config/WhitelistPatternsProperties.java @@ -5,12 +5,21 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; +import java.util.Arrays; +import java.util.stream.Stream; + @Setter @Getter @Configuration -@ConfigurationProperties(prefix = "security.whitelist") +@ConfigurationProperties(prefix = "security.whitelist.patterns") public class WhitelistPatternsProperties { - private String[] patterns; + private String[] actuator; + private String[] apiDocs; + public String[] getWhitelistPatterns() { + return Stream.of(apiDocs, actuator) + .flatMap(Arrays::stream) + .toArray(String[]::new); + } } diff --git a/src/main/java/page/clab/api/global/util/SwaggerUtil.java b/src/main/java/page/clab/api/global/util/SwaggerUtil.java deleted file mode 100644 index 78e716255..000000000 --- a/src/main/java/page/clab/api/global/util/SwaggerUtil.java +++ /dev/null @@ -1,33 +0,0 @@ -package page.clab.api.global.util; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import page.clab.api.global.config.WhitelistPatternsProperties; - -import java.util.regex.Pattern; - - -@Component -public class SwaggerUtil implements InitializingBean { - - private static String[] swaggerPatterns; - - @Autowired - private WhitelistPatternsProperties whitelistPatternsProperties; - - @Override - public void afterPropertiesSet() throws Exception { - swaggerPatterns = whitelistPatternsProperties.getPatterns(); - } - - public static boolean isSwaggerRequest(String path) { - for (String pattern : swaggerPatterns) { - if (Pattern.compile(pattern).matcher(path).find()) { - return true; - } - } - return false; - } - -} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/util/WhitelistUtil.java b/src/main/java/page/clab/api/global/util/WhitelistUtil.java new file mode 100644 index 000000000..0ef5ce2be --- /dev/null +++ b/src/main/java/page/clab/api/global/util/WhitelistUtil.java @@ -0,0 +1,51 @@ +package page.clab.api.global.util; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import page.clab.api.global.config.WhitelistPatternsProperties; + +import java.util.regex.Pattern; + +@Component +public class WhitelistUtil implements InitializingBean { + + private static String[] swaggerPatterns; + private static String[] actuatorPatterns; + private static String[] whitelistPatterns; + + @Autowired + private WhitelistPatternsProperties whitelistPatternsProperties; + + @Override + public void afterPropertiesSet() throws Exception { + swaggerPatterns = whitelistPatternsProperties.getApiDocs(); + actuatorPatterns = whitelistPatternsProperties.getActuator(); + whitelistPatterns = whitelistPatternsProperties.getWhitelistPatterns(); + } + + public static boolean isSwaggerRequest(String path) { + return isPatternMatch(path, swaggerPatterns); + } + + public static boolean isActuatorRequest(String path) { + return isPatternMatch(path, actuatorPatterns); + } + + public static boolean isWhitelistRequest(String path) { + return isPatternMatch(path, whitelistPatterns); + } + + public static boolean isSwaggerIndexEndpoint(String path) { + return swaggerPatterns[2].equals(path); + } + + private static boolean isPatternMatch(String path, String[] patterns) { + for (String pattern : patterns) { + if (Pattern.compile(pattern).matcher(path).find()) { + return true; + } + } + return false; + } +} \ No newline at end of file