Skip to content

Commit

Permalink
화이트리스트 접근 시 슬랙 알림 전송 로직 작성 완료 (#398)
Browse files Browse the repository at this point in the history
  • Loading branch information
limehee authored Jul 9, 2024
1 parent 3fe57d0 commit 78647c8
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@
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;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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에 대한 접근이 거부되었습니다.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -29,21 +29,17 @@
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
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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에서 접속하여 토큰을 삭제하였습니다.");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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."),
Expand All @@ -20,5 +22,4 @@ public enum SecurityAlertType implements AlertType {

private final String title;
private final String defaultMessage;

}
17 changes: 3 additions & 14 deletions src/main/java/page/clab/api/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down Expand Up @@ -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(
Expand All @@ -115,7 +105,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

private ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry configureRequests(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.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()
Expand Down Expand Up @@ -144,5 +134,4 @@ private void apiLogging(HttpServletRequest request, HttpServletResponse response
int httpStatus = response.getStatus();
log.info("[{}:{}] {} {} {} {}", clientIpAddress, id, requestUrl, httpMethod, httpStatus, message);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
33 changes: 0 additions & 33 deletions src/main/java/page/clab/api/global/util/SwaggerUtil.java

This file was deleted.

51 changes: 51 additions & 0 deletions src/main/java/page/clab/api/global/util/WhitelistUtil.java
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit 78647c8

Please sign in to comment.