본문 바로가기
공부/프로그래밍

[spring security] token 요청 시 에러 Exception 핸들링하기(/oauth/token 에러, ControllerAdvice 적용안됨)

by demonic_ 2021. 2. 15.
반응형

서버중에 oauth2를 발급하는 서버가 있는데, 계정과 비밀번호를 틀리게 되었을 때 다음처럼 리턴값을 받게 된다

문제는 이 리턴방식이 회사에서 규격하는 form과 다를 확률이 높다는 점이다. 이 경우 선택할 수 있는건 2가지다 httpStatus가 401일 경우 처리방식을 고정하는 것, 아니면 이번 포스팅처럼 output을 설정하는 것이다.

 

이번은 output의 form을 설정하는 것에 관한 포스팅이다.

그럼 시작.

 

 

아무것도 작업하지 않으면 다음과 같이 리턴을 받을 것이다.

httpStatus: 401

{
    "error": "invalid_grant",
    "error_description": "자격 증명에 실패하였습니다."
}

 

 

UserDetailsService 를 구현한 클래스에서 DB에서 조회하여 UserDetails를 리턴하게 되어있다.

내 경우 데이터가 없다면 throw Exception을 던지는데, 이때 ControllerAdvice를 사용해도 캐치가 되지 않는다.

 

UserDetailsService를 구현한 클래스

@Component
public class CustomUserDetailsService implements UserDetailsService {
    private final LoginMapper loginMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        ResultFindUserPassword findUser = loginMapper.findByUserEmail(username);
        if(findUser == null) {
            // Exception의 경우 프로젝트에 따라 다양하지만 여기선 Exception 으로 사용
            throw new Exception("아이디 또는 패스워드가 틀립니다");
        }
        ...
    }
    
...

 

 

그럼 Authorization을 실패하게 될때 어떻게 ExceptionHandler를 하여 Result값을 변경할 수 있을까?

 

일단 로그를 보면 TokenEndpoint 가 찍히는 것을 알 수 있다. 한번 살펴보자.

아래는 아이디가 검색되지 않을때의 로그다 (InternalAuthenticationServiceException)

...
o.s.s.o.provider.endpoint.TokenEndpoint  : Handling error: InternalAuthenticationServiceException, 아이디 또는 패스워드가 틀립니다
...

 

 

아래는 패스워드가 틀렸을때 발생하는 Exception

(참고로 비밀번호가 틀린것에 대해서는 UserDetailsService 에서 핸들링 할 수 없다. 그래서 Exception의 종류가 다르다. InvalidGrantException으로 잡힌다.

...
o.s.s.o.provider.endpoint.TokenEndpoint  : Handling error: InvalidGrantException, 자격 증명에 실패하였습니다.
...

 

서로 다른 Exception이기 때문에 핸들링 되는 위치가 조금 다르다. 그래도 같은 클래스를 보여주는 TokenEndpoint를 한번 살펴보는게 좋겠다.

 

TokenEndpoint는 어느 클래스일까? 확인해보니 다음 경로에 있는 클래스인데, 그 안에 다양한 Exception에 대한 Handler도 들어있음을 알 수 있다.

package org.springframework.security.oauth2.provider.endpoint;
...

@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {
...
	@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
	public ResponseEntity<OAuth2Exception> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) throws Exception {
		if (logger.isInfoEnabled()) {
			logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
		}
	    return getExceptionTranslator().translate(e);
	}
	
	@ExceptionHandler(Exception.class)
	public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
		if (logger.isWarnEnabled()) {
			logger.warn("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
		}
		return getExceptionTranslator().translate(e);
	}
	
	@ExceptionHandler(ClientRegistrationException.class)
	public ResponseEntity<OAuth2Exception> handleClientRegistrationException(Exception e) throws Exception {
		if (logger.isWarnEnabled()) {
			logger.warn("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
		}
		return getExceptionTranslator().translate(new BadClientCredentialsException());
	}

	@ExceptionHandler(OAuth2Exception.class)
	public ResponseEntity<OAuth2Exception> handleException(OAuth2Exception e) throws Exception {
		if (logger.isWarnEnabled()) {
			logger.warn("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
		}
		return getExceptionTranslator().translate(e);
	}
...

 

 

그래서 Break Point를 찍어봤더니 계정이 없는 경우는 Exception.class Handler 에서, 비밀번호가 틀린 경우는 OAuth2Exception.class Handler 에서 캐치한다

 

아래는 계정이 없는 경우(InternalAuthenticationServiceException.class)

 

아래는 비밀번호가 틀린경우(InvalidGrantException.class)

 

잘 살펴보면 공통점이 getExceptionTranslator().translate(e)를 호출한다는 점이다. 이것의 추상클래스인 AbstractEndPoint의 exceptionTranslator 를 호출하는 것이고, 해당 클래스의 메서드인 translate 를 호출한다. 즉, TokenEndpoint의 exceptionTranslator를 갈아끼워주면 된다(이런 패턴을 전략패턴(스트레지 패턴)이라 부른다)

 

AbstractEndPoint.class 의 구성

public class AbstractEndpoint implements InitializingBean {
...
    private WebResponseExceptionTranslator<OAuth2Exception> providerExceptionHandler = new DefaultWebResponseExceptionTranslator();
...

    protected WebResponseExceptionTranslator<OAuth2Exception> getExceptionTranslator() {
		return providerExceptionHandler;
	}
...

 

WebResponseExceptionTranslator.class 인터페이스

public interface WebResponseExceptionTranslator<T> {
	
	ResponseEntity<T> translate(Exception e) throws Exception;

}

 

WebResponseExceptionTranslator를 구현한 것을 알 수 있고, OAuth2Exception을 지정한 것을 알 수 있다.

(OAuth2Exception은 RuntimeException을 상속받은 것이다)

 

그럼 이제부터 이것을 설정하는 방법을 살펴보자

 

Spring Security 에서 @EnableAuthorizationServer를 구현한 클래스를 찾는다.

그 안에 WebResponseExceptionTranslator를 리턴할 다음의 메서드를 만든다.

...
    public WebResponseExceptionTranslator authorizationWebResponseExceptionTranslator() {
        return new DefaultWebResponseExceptionTranslator() {

            @Override
            public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
                Map responseMap = new HashMap();
                responseMap.put("message", e.getMessage());
                return new ResponseEntity(responseMap, HttpStatus.UNAUTHORIZED);
            }
        };
    }
...

 

그리고 오버라이드 한 것중에 AuthorizationServerEndpointsConfigurer 를 구현한 것이 있다면 다음 구절을 추가한다

...
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        ... 
        // 이전 설정에서 다음을 추가
        endpoints.exceptionTranslator(authorizationWebResponseExceptionTranslator());
    }
....

 

 

그럼 이제 호출해보자.

httpStatus에 401 에러를 리턴한것과 메세지가 내려오는 것을 확인할 수 있다.

 

비밀번호 틀린 것(자격 검증 실패)도 확인

 

 

 

끝.

 

ps. 이 단계에서는 로그인을 통한 oauth를 받는것에 한해 발생하는 Exception에 관한 설정이다. 즉 지금까지는 AuthorizationServer 에서만 유효한 설정이란 뜻.

다음은 잘못된 토큰을 요청하여 발생하는 에러인 invalid_token에 대해 알아볼 것이다.

이건 ResourceServer에 해당하는 것이므로 여기서 가능한게 아니기 때문.

 

 

 

 

 

참조 사이트

https://github.com/spring-projects/spring-security-oauth/issues/610

 

How to customize my own exception handler in token endpoint? · Issue #610 · spring-projects/spring-security-oauth

The exceptions thrown in each endpoint are caught by @ExceptionHandler methods in the same class. What i want to do is to catch these exceptions (such as OAuth2Exception, InvalidTokenException) in ...

github.com

https://stackoverflow.com/questions/32912460/how-to-inject-webresponseexceptiontranslator-in-tokenendpoint-spring-oauth2

 

How to inject WebResponseExceptionTranslator in TokenEndPoint Spring oAuth2

I am trying to customize the way the Spring Security oAuth TokenEndpoint class handles exceptions. I want to return a more customized JSON response than the DefaultWebResponseExceptionTranslator

stackoverflow.com

 

반응형

댓글