Skip to content
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
@@ -1,5 +1,7 @@
package com.example.solidconnection.config.security;

import com.example.solidconnection.custom.exception.CustomAccessDeniedHandler;
import com.example.solidconnection.custom.exception.CustomAuthenticationEntryPoint;
import com.example.solidconnection.custom.security.filter.ExceptionHandlerFilter;
import com.example.solidconnection.custom.security.filter.JwtAuthenticationFilter;
import com.example.solidconnection.custom.security.filter.SignOutCheckFilter;
Expand All @@ -13,7 +15,6 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
Expand All @@ -30,6 +31,8 @@ public class SecurityConfiguration {
private final ExceptionHandlerFilter exceptionHandlerFilter;
private final SignOutCheckFilter signOutCheckFilter;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;

@Bean
public CorsConfigurationSource corsConfigurationSource() {
Expand Down Expand Up @@ -62,9 +65,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/admin/**").hasRole(ADMIN.name())
.anyRequest().permitAll()
)
.exceptionHandling(exception -> exception
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
)
.addFilterBefore(jwtAuthenticationFilter, BasicAuthenticationFilter.class)
.addFilterBefore(signOutCheckFilter, JwtAuthenticationFilter.class)
.addFilterAfter(exceptionHandlerFilter, ExceptionTranslationFilter.class)
.addFilterBefore(exceptionHandlerFilter, SignOutCheckFilter.class)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.solidconnection.custom.exception;

import com.example.solidconnection.custom.response.ErrorResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

private final ObjectMapper objectMapper;

@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
ErrorResponse errorResponse =
new ErrorResponse(ErrorCode.ACCESS_DENIED);
response.setStatus(ErrorCode.ACCESS_DENIED.getCode());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.solidconnection.custom.exception;

import com.example.solidconnection.custom.response.ErrorResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final ObjectMapper objectMapper;

@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
ErrorResponse errorResponse =
new ErrorResponse(ErrorCode.AUTHENTICATION_FAILED);
response.setStatus(ErrorCode.AUTHENTICATION_FAILED.getCode());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ public ErrorResponse(CustomException e) {
this(e.getMessage());
}

public ErrorResponse(ErrorCode e) {
this(e.getMessage());
}

public ErrorResponse(ErrorCode e, String detail) {
this(e.getMessage() + " : " + detail);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,12 @@
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_DENIED;
import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED;

@Component
Expand All @@ -36,10 +32,6 @@ protected void doFilterInternal(@NonNull HttpServletRequest request,
filterChain.doFilter(request, response);
} catch (CustomException e) {
customCommence(response, e);
} catch (AccessDeniedException e) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
ErrorCode errorCode = auth instanceof AnonymousAuthenticationToken ? AUTHENTICATION_FAILED : ACCESS_DENIED;
generalCommence(response, e, errorCode);
} catch (Exception e) {
generalCommence(response, e, AUTHENTICATION_FAILED);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.example.solidconnection.custom.exception;

import com.example.solidconnection.custom.response.ErrorResponse;
import com.example.solidconnection.support.TestContainerSpringBootTest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.access.AccessDeniedException;

import java.io.IOException;

import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_DENIED;
import static org.assertj.core.api.Assertions.assertThat;

@TestContainerSpringBootTest
@DisplayName("커스텀 인가 예외 처리 테스트")
class CustomAccessDeniedHandlerTest {

@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;

@Autowired
private ObjectMapper objectMapper;

private MockHttpServletRequest request;
private MockHttpServletResponse response;

@BeforeEach
void setUp() {
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
}

@Test
void 권한이_없는_사용자_접근시_403_예외_응답을_반환한다() throws IOException {
// given
AccessDeniedException accessDeniedException = new AccessDeniedException(ACCESS_DENIED.getMessage());

// when
accessDeniedHandler.handle(request, response, accessDeniedException);

// then
ErrorResponse errorResponse = objectMapper.readValue(response.getContentAsString(), ErrorResponse.class);
assertThat(response.getStatus()).isEqualTo(ACCESS_DENIED.getCode());
assertThat(errorResponse.message()).isEqualTo(ACCESS_DENIED.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.example.solidconnection.custom.exception;

import com.example.solidconnection.custom.response.ErrorResponse;
import com.example.solidconnection.support.TestContainerSpringBootTest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;

import java.io.IOException;

import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED;
import static org.assertj.core.api.Assertions.assertThat;

@TestContainerSpringBootTest
@DisplayName("커스텀 인증 예외 처리 테스트")
class CustomAuthenticationEntryPointTest {

@Autowired
private CustomAuthenticationEntryPoint authenticationEntryPoint;

@Autowired
private ObjectMapper objectMapper;

private MockHttpServletRequest request;
private MockHttpServletResponse response;

@BeforeEach
void setUp() {
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
}

@Test
void 인증되지_않은_사용자_접근시_401_예외_응답을_반환한다() throws IOException {
// given
AuthenticationException authException = new AuthenticationServiceException(AUTHENTICATION_FAILED.getMessage());

// when
authenticationEntryPoint.commence(request, response, authException);

// then
ErrorResponse errorResponse = objectMapper.readValue(response.getContentAsString(), ErrorResponse.class);
assertThat(response.getStatus()).isEqualTo(AUTHENTICATION_FAILED.getCode());
assertThat(errorResponse.message()).isEqualTo(AUTHENTICATION_FAILED.getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,34 +85,6 @@ void setUp() {
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
}

@Test
void 익명_사용자의_접근_거부시_401_예외_응답을_반환한다() throws Exception {
// given
Authentication anonymousAuth = getAnonymousAuth();
SecurityContextHolder.getContext().setAuthentication(anonymousAuth);
willThrow(new AccessDeniedException("Access Denied")).given(filterChain).doFilter(request, response);

// when
exceptionHandlerFilter.doFilterInternal(request, response, filterChain);

// then
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
}

@Test
void 인증된_사용자의_접근_거부하면_403_예외_응답을_반환한다() throws Exception {
// given
Authentication auth = new TestingAuthenticationToken("user", "password", "ROLE_USER");
SecurityContextHolder.getContext().setAuthentication(auth);
willThrow(new AccessDeniedException("Access Denied")).given(filterChain).doFilter(request, response);

// when
exceptionHandlerFilter.doFilterInternal(request, response, filterChain);

// then
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
}

private static Stream<Throwable> provideException() {
return Stream.of(
new RuntimeException(),
Expand Down