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

RE: 화이트리스트 접근 시 슬랙 알림 전송 로직 작성 완료 #402

Merged
merged 1 commit into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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;
}
}