IT

@ExceptionHandler로 스프링 보안 인증 예외 처리

lottoking 2020. 10. 10. 10:27

@ExceptionHandler로 스프링 보안 인증 예외 처리


저는 Spring MVC를 사용 @ControllerAdvice하고 있으며 @ExceptionHandlerREST 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 본문을 반환하는 것입니다. 스프링 보안 AuthenticationEntryPoint와 spring mvc @ExceptionHandler가 함께 작동하는 방법이 있습니까?

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


좋아, 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을 빌드하지 않는 일부 클래스 인스턴스를 소규모화합니다.


이것은 Spring SecuritySpring Web 프레임 워크가 응답을 처리하는 방식이 매우 일관 적이 지 않다는 매우 흥미로운 문제입니다 . MessageConverter방법으로 오류 메시지 처리를 기본적으로 지원해야 할 것 입니다.

나는 MessageConverter그들의 예외를 적용하고 내용 협상에 따라 올바른 형식으로 봄 보안 에 필요한 우아한 방법을 도입하고 노력 했다 . 그래도 아래의 솔루션은 우아하지만 스프링 코드를 사용합니다.

Jackson 및 JAXB 라이브러리를 포함하는 방법을 알고 가정합니다. 정렬되지 않습니다. 총 3 단계가 있습니다.

1 단계 -MessageConverters를 저장하는 독립 실행 형 클래스 만들기

이 클래스는 마술을하지 않습니다. 메시지 변환기와 프로세서 만 저장합니다 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;
    }
}

2 단계 -AuthenticationEntryPoint 만들기

많은 안내에서와 같이이 클래스는 사용자 지정 오류 처리를 구현하는 데 존재합니다.

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();
        }
    }
}

3 단계-진입 점 등록

언급했듯이 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 또는 기타 형식으로 예외가 발생해야합니다.


내가 가장 좋은 방법은 예외를 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를 사용하여 원하는 방식으로 응답 형식을 수 있습니다.


Spring Boot 및의 경우 Java 구성 대신 @EnableResourceServer확장 하고 메서드 내부에서 재정의 및 사용하여 사용자 지정 등록하는 것이 좋습니다 .ResourceServerConfigurerAdapterWebSecurityConfigurerAdapterAuthenticationEntryPointconfigure(ResourceServerSecurityConfigurer resources)resources.authenticationEntryPoint(customAuthEntryPoint())

이 같은 :

@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"헤더를 추가합니다.

이것이 누군가를 도울 수 있기를 바랍니다.


@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, deserializer 등으로 변환하는 데 사용하는 모든 것을 삽입 할 수 있습니다.


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.이 경우 추가해야합니다 @Qualifier("handlerExceptionResolver").HandlerExceptionResolver


필터에서 'unsuccessfulAuthentication'메서드를 간단히 처리 할 수 ​​있습니다. 거기에서 원하는 HTTP 상태 코드로 클라이언트에 오류 응답을 보냅니다.

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException failed) throws IOException, ServletException {

    if (failed.getCause() instanceof RecordNotFoundException) {
        response.sendError((HttpServletResponse.SC_NOT_FOUND), failed.getMessage());
    }
}

objectMapper를 사용하고 있습니다. 모든 휴식 서비스는 대부분 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)
     }}

업데이트 : 당신이 좋아하고 코드를 직접 보는 것을 선호한다면 두 가지 예가 있습니다. 하나는 당신이보고있는 표준 Spring Security를 ​​사용하고 다른 하나는 Reactive Web과 Reactive Security에서 동등한 것을 사용하는 것입니다.
- 일반 웹 + Jwt 보안
- 반응 형 Jwt

JSON 기반 엔드 포인트는 항상 사용하는 것은 다음과 같습니다.

@Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {

    @Autowired
    ObjectMapper mapper;

    private static final Logger logger = LoggerFactory.getLogger(JwtAuthEntryPoint.class);

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException e)
            throws IOException, ServletException {
        // Called when the user tries to access an endpoint which requires to be authenticated
        // we just return unauthorizaed
        logger.error("Unauthorized error. Message - {}", e.getMessage());

        ServletServerHttpResponse res = new ServletServerHttpResponse(response);
        res.setStatusCode(HttpStatus.UNAUTHORIZED);
        res.getServletResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        res.getBody().write(mapper.writeValueAsString(new ErrorResponse("You must authenticated")).getBytes());
    }
}

객체 매퍼는 스프링 웹 스타터를 추가 한 후에 이미 빈이지만 사용자 정의하는 것을 선호하므로 ObjectMapper에 대해 수행하는 작업은 다음과 같습니다.

  @Bean
    public Jackson2ObjectMapperBuilder objectMapperBuilder() {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        builder.modules(new JavaTimeModule());

        // for example: Use created_at instead of createdAt
        builder.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);

        // skip null fields
        builder.serializationInclusion(JsonInclude.Include.NON_NULL);
        builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        return builder;
    }

WebSecurityConfigurerAdapter 클래스에서 기본값으로 설정 한 AuthenticationEntryPoint :

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ............
   @Autowired
    private JwtAuthEntryPoint unauthorizedHandler;
@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .authorizeRequests()
                // .antMatchers("/api/auth**", "/api/login**", "**").permitAll()
                .anyRequest().permitAll()
                .and()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);


        http.headers().frameOptions().disable(); // otherwise H2 console is not available
        // There are many ways to ways of placing our Filter in a position in the chain
        // You can troubleshoot any error enabling debug(see below), it will print the chain of Filters
        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    }
// ..........
}

참고 URL : https://stackoverflow.com/questions/19767267/handle-spring-security-authentication-exceptions-with-exceptionhandler