복붙노트

[SPRING] @ExceptionHandler를 사용하여 봄 보안 인증 예외 처리

SPRING

@ExceptionHandler를 사용하여 봄 보안 인증 예외 처리

나는 Spring MVC의 @ControllerAdvice와 @ExceptionHandler를 사용하여 REST API의 모든 예외를 처리한다. 그것은 웹 mvc 컨트롤러에 의해 던져 예외에 대한 잘 작동하지만 컨트롤러 메서드를 호출하기 전에 실행되기 때문에 봄 보안 사용자 지정 필터에 의해 throw 된 예외 작동하지 않습니다.

토큰 기반 인증을 수행하는 사용자 정의 스프링 보안 필터가 있습니다.

public class AegisAuthenticationFilter extends GenericFilterBean {

...

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

        try {

            ...         
        } catch(AuthenticationException authenticationException) {

            SecurityContextHolder.clearContext();
            authenticationEntryPoint.commence(request, response, authenticationException);

        }

    }

}

이 사용자 정의 엔트리 포인트 :

@Component("restAuthenticationEntryPoint")
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{

    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
    }

}

그리고이 클래스는 예외를 전역 적으로 처리합니다.

@ControllerAdvice
public class RestEntityResponseExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler({ InvalidTokenException.class, AuthenticationException.class })
    @ResponseStatus(value = HttpStatus.UNAUTHORIZED)
    @ResponseBody
    public RestError handleAuthenticationException(Exception ex) {

        int errorCode = AegisErrorCode.GenericAuthenticationError;
        if(ex instanceof AegisException) {
            errorCode = ((AegisException)ex).getCode();
        }

        RestError re = new RestError(
            HttpStatus.UNAUTHORIZED,
            errorCode, 
            "...",
            ex.getMessage());

        return re;
    }
}

내가해야 할 일은 스프링 보안 AuthenticationException에 대해서조차도 상세한 JSON 본문을 반환하는 것이다. Spring 보안 AuthenticationEntryPoint와 spring mvc @ExceptionHandler가 함께 작동하는 방법이 있습니까?

저는 봄 보안 3.1.4와 봄 mvc 3.2.4를 사용하고 있습니다.

해결법

  1. ==============================

    1.좋아, 나는 AuthenticationEntryPoint에서 직접 json을 작성하는 것이 좋습니다.

    좋아, 나는 AuthenticationEntryPoint에서 직접 json을 작성하는 것이 좋습니다.

    테스트를 위해 response.sendError를 제거하여 AutenticationEntryPoint를 변경했습니다.

    @Component("restAuthenticationEntryPoint")
    public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{
    
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
    
            response.setContentType("application/json");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getOutputStream().println("{ \"error\": \"" + authenticationException.getMessage() + "\" }");
    
        }
    }
    

    이 방법으로 여러분은 Spring Security AuthenticationEntryPoint를 사용하고 있더라도 권한이없는 401과 함께 커스텀 json 데이터를 보낼 수 있습니다.

    분명히 당신은 테스트 목적을 위해했던 것처럼 json을 만들지 않을 것이지만 당신은 어떤 클래스 인스턴스를 직렬화 할 것이다.

  2. ==============================

    2.이것은 스프링 보안과 스프링 웹 프레임 워크가 응답을 처리하는 방식에있어 일관성이 없다는 매우 흥미로운 문제이다. 나는 그것이 MessageConverter와 함께 에러 메시지 처리를 네이티브하게 지원해야한다고 생각한다.

    이것은 스프링 보안과 스프링 웹 프레임 워크가 응답을 처리하는 방식에있어 일관성이 없다는 매우 흥미로운 문제이다. 나는 그것이 MessageConverter와 함께 에러 메시지 처리를 네이티브하게 지원해야한다고 생각한다.

    Spring Security에 MessageConverter를 삽입하여 예외를 잡아 내고 내용 협상에 따라 올바른 형식으로 반환 할 수있는 우아한 방법을 찾으려고했습니다. 여전히, 아래의 내 솔루션은 우아하지 않지만 최소한 봄 코드를 사용합니다.

    잭슨과 JAXB 라이브러리를 포함시키는 방법을 알고 있다고 가정합니다. 그렇지 않으면 계속 진행할 필요가 없습니다. 총 3 단계가 있습니다.

    이 클래스는 마술을 수행하지 않습니다. 단순히 메시지 변환기와 프로세서 RequestResponseBodyMethodProcessor를 저장합니다. 마법은 내용 협상을 포함하여 모든 작업을 수행하고 그에 따라 응답 본문을 변환하는 프로세서 내부에 있습니다.

    public class MessageProcessor { // Any name you like
        // List of HttpMessageConverter
        private List<HttpMessageConverter<?>> messageConverters;
        // under org.springframework.web.servlet.mvc.method.annotation
        private RequestResponseBodyMethodProcessor processor;
    
        /**
         * Below class name are copied from the framework.
         * (And yes, they are hard-coded, too)
         */
        private static final boolean jaxb2Present =
            ClassUtils.isPresent("javax.xml.bind.Binder", MessageProcessor.class.getClassLoader());
    
        private static final boolean jackson2Present =
            ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", MessageProcessor.class.getClassLoader()) &&
            ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", MessageProcessor.class.getClassLoader());
    
        private static final boolean gsonPresent =
            ClassUtils.isPresent("com.google.gson.Gson", MessageProcessor.class.getClassLoader());
    
        public MessageProcessor() {
            this.messageConverters = new ArrayList<HttpMessageConverter<?>>();
    
            this.messageConverters.add(new ByteArrayHttpMessageConverter());
            this.messageConverters.add(new StringHttpMessageConverter());
            this.messageConverters.add(new ResourceHttpMessageConverter());
            this.messageConverters.add(new SourceHttpMessageConverter<Source>());
            this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
    
            if (jaxb2Present) {
                this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
            }
            if (jackson2Present) {
                this.messageConverters.add(new MappingJackson2HttpMessageConverter());
            }
            else if (gsonPresent) {
                this.messageConverters.add(new GsonHttpMessageConverter());
            }
    
            processor = new RequestResponseBodyMethodProcessor(this.messageConverters);
        }
    
        /**
         * This method will convert the response body to the desire format.
         */
        public void handle(Object returnValue, HttpServletRequest request,
            HttpServletResponse response) throws Exception {
            ServletWebRequest nativeRequest = new ServletWebRequest(request, response);
            processor.handleReturnValue(returnValue, null, new ModelAndViewContainer(), nativeRequest);
        }
    
        /**
         * @return list of message converters
         */
        public List<HttpMessageConverter<?>> getMessageConverters() {
            return messageConverters;
        }
    }
    

    많은 튜토리얼에서와 마찬가지로이 클래스는 사용자 정의 오류 처리를 구현하는 데 필수적입니다.

    public class CustomEntryPoint implements AuthenticationEntryPoint {
        // The class from Step 1
        private MessageProcessor processor;
    
        public CustomEntryPoint() {
            // It is up to you to decide when to instantiate
            processor = new MessageProcessor();
        }
    
        @Override
        public void commence(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {
    
            // This object is just like the model class, 
            // the processor will convert it to appropriate format in response body
            CustomExceptionObject returnValue = new CustomExceptionObject();
            try {
                processor.handle(returnValue, request, response);
            } catch (Exception e) {
                throw new ServletException();
            }
        }
    }
    

    앞서 언급했듯이 Java Config를 사용하여 작업을 수행합니다. 여기서는 관련 구성을 보여 주며 세션 상태 비 저장 등의 다른 구성이 있어야합니다.

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.exceptionHandling().authenticationEntryPoint(new CustomEntryPoint());
        }
    }
    

    일부 인증 실패 사례를 시도해보십시오. 요청 헤더에 Accept : XXX가 포함되어야하며 JSON, XML 또는 기타 형식으로 예외가 발생해야합니다.

  3. ==============================

    3.내가 찾은 가장 좋은 방법은 예외를 HandlerExceptionResolver에 위임하는 것이다.

    내가 찾은 가장 좋은 방법은 예외를 HandlerExceptionResolver에 위임하는 것이다.

    @Component("restAuthenticationEntryPoint")
    public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
        @Autowired
        private HandlerExceptionResolver resolver;
    
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            resolver.resolveException(request, response, null, exception);
        }
    }
    

    @ExceptionHandler를 사용하여 원하는 방식으로 응답 형식을 지정할 수 있습니다.

  4. ==============================

    4.Spring Boot 및 @EnableResourceServer의 경우 Java 구성에서 WebSecurityConfigurerAdapter 대신 ResourceServerConfigurerAdapter를 확장하고 configure (ResourceServerSecurityConfigurer 자원)를 대체하고 메소드 내부에서 resources.authenticationEntryPoint (customAuthEntryPoint ())를 사용하여 사용자 정의 AuthenticationEntryPoint를 등록하는 것이 상대적으로 쉽고 편리합니다. .

    Spring Boot 및 @EnableResourceServer의 경우 Java 구성에서 WebSecurityConfigurerAdapter 대신 ResourceServerConfigurerAdapter를 확장하고 configure (ResourceServerSecurityConfigurer 자원)를 대체하고 메소드 내부에서 resources.authenticationEntryPoint (customAuthEntryPoint ())를 사용하여 사용자 정의 AuthenticationEntryPoint를 등록하는 것이 상대적으로 쉽고 편리합니다. .

    이 같은:

    @Configuration
    @EnableResourceServer
    public class CommonSecurityConfig extends ResourceServerConfigurerAdapter {
    
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.authenticationEntryPoint(customAuthEntryPoint());
        }
    
        @Bean
        public AuthenticationEntryPoint customAuthEntryPoint(){
            return new AuthFailureHandler();
        }
    }
    

    또한 최종 OAuth2AuthenticationEntryPoint가 확장되어 (최종적이지 않기 때문에) 사용자 정의 AuthenticationEntryPoint를 구현하는 동안 부분적으로 재사용 될 수 있습니다. 특히 오류 관련 세부 정보가있는 "WWW-Authenticate"헤더를 추가합니다.

    희망이 사람을 도울 것입니다.

  5. ==============================

    5.@Nicola 및 @Victor Wing의 답변을 얻고보다 표준화 된 방법을 추가하십시오.

    @Nicola 및 @Victor Wing의 답변을 얻고보다 표준화 된 방법을 추가하십시오.

    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.converter.HttpMessageConverter;
    import org.springframework.http.server.ServerHttpResponse;
    import org.springframework.http.server.ServletServerHttpResponse;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    public class UnauthorizedErrorAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {
    
        private HttpMessageConverter messageConverter;
    
        @SuppressWarnings("unchecked")
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
    
            MyGenericError error = new MyGenericError();
            error.setDescription(exception.getMessage());
    
            ServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
            outputMessage.setStatusCode(HttpStatus.UNAUTHORIZED);
    
            messageConverter.write(error, null, outputMessage);
        }
    
        public void setMessageConverter(HttpMessageConverter messageConverter) {
            this.messageConverter = messageConverter;
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
    
            if (messageConverter == null) {
                throw new IllegalArgumentException("Property 'messageConverter' is required");
            }
        }
    
    }
    

    이제 구성된 Jackson, Jaxb 또는 MVC 주석 또는 XML 기반 구성에서 응답 본문을 serializer, deserializers 등으로 변환하는 데 사용하는 모든 것을 주입 할 수 있습니다.

  6. ==============================

    6.objectMapper를 사용하고 있습니다. 모든 Rest Service는 대부분 json과 함께 작업하며 구성 중 하나에서 이미 객체 매퍼를 구성했습니다.

    objectMapper를 사용하고 있습니다. 모든 Rest Service는 대부분 json과 함께 작업하며 구성 중 하나에서 이미 객체 매퍼를 구성했습니다.

    코드는 Kotlin으로 작성되었으므로 괜찮습니다.

    @Bean
    fun objectMapper(): ObjectMapper {
        val objectMapper = ObjectMapper()
        objectMapper.registerModule(JodaModule())
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
    
        return objectMapper
    }
    
    class UnauthorizedAuthenticationEntryPoint : BasicAuthenticationEntryPoint() {
    
        @Autowired
        lateinit var objectMapper: ObjectMapper
    
        @Throws(IOException::class, ServletException::class)
        override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) {
            response.addHeader("Content-Type", "application/json")
            response.status = HttpServletResponse.SC_UNAUTHORIZED
    
            val responseError = ResponseError(
                message = "${authException.message}",
            )
    
            objectMapper.writeValue(response.writer, responseError)
         }}
    
  7. ==============================

    7.이 경우 HandlerExceptionResolver를 사용할 필요가 있습니다.

    이 경우 HandlerExceptionResolver를 사용할 필요가 있습니다.

    @Component
    public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
        @Autowired
        //@Qualifier("handlerExceptionResolver")
        private HandlerExceptionResolver resolver;
    
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
            resolver.resolveException(request, response, null, authException);
        }
    }
    

    또한 개체를 반환하려면 예외 처리기 클래스를 추가해야합니다.

    @RestControllerAdvice
    public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    
        @ExceptionHandler(AuthenticationException.class)
        public GenericResponseBean handleAuthenticationException(AuthenticationException ex, HttpServletResponse response){
            GenericResponseBean genericResponseBean = GenericResponseBean.build(MessageKeys.UNAUTHORIZED);
            genericResponseBean.setError(true);
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return genericResponseBean;
        }
    }
    

    HandlerExceptionResolver의 여러 구현 때문에 프로젝트를 실행할 때 오류가 발생할 수 있습니다.이 경우 HandlerExceptionResolver에 @Qualifier ( "handlerExceptionResolver")를 추가해야합니다.

  8. from https://stackoverflow.com/questions/19767267/handle-spring-security-authentication-exceptions-with-exceptionhandler by cc-by-sa and MIT license