[Spring Boot Tutorial] 16. Swagger v3에 HTTP 기본인증(Basic Authentication) 설정하기

2020. 10. 12. 16:14Spring/Spring Boot Tutorial

반응형
  1. Security Scheme?
  2. Basic Authentication?
  3. securityScheme 설정 추가
  4. Authorization 헤더 관련 익셉션 발생
  5. @ControllerAdvice 를 이용한 익셉션별 response 설정

1. Security Scheme?

OpenAPI 3에서는 리소스에 액세스할 때, 액세스 권한이 있는지에 대한 인가처리를 위해 security scheme(보안체계)를 사용합니다.

OpenAPI 3에서 제공하고 있는 보안체계는 4가지가 있으며, 그에 대한 정보는 아래와 같습니다.

  • http
    • HTTP Authentications schema를 이용하는 방식
    • Authorization 헤더를 이용하는 방식
    • ex) Basic, Bearer 등...
  • apiKey
    • API key 헤더를 이용한 인증
  • oauth2
    • OAuth 2.0
  • openIdConnect
    • OpenID Connect Discovery 이용 [참고]

이번 포스팅에서는 HTTP Authentications schema(HTTP 인증 스키마) 중 가장 기본 방식인 Basic방식을 이용한 인증 인가 처리를 알아볼 것입니다.


2. Basic Authentication?

Basic Authentication 은 입력받은 username과 password를 username:password 형태로 만들어 Base64 인코딩방식으로 인코딩 한 후, prefix로 Basic을 붙여서 Authorization 헤더에 담아 인증을 처리합니다.

Swagger v3을 통해 문서화 될 api에 공통적으로 securityScheme을 설정해봅시다.


3. securityScheme 설정 추가

Swagger v3 문서의 info 설정을 위해 생성했던 OpenAPI Bean에 securityScheme 설정을 추가합니다.

@Component
public class OpenApiConfig {

	@Bean
	public OpenAPI openAPI(@Value("${demo.version}") String appVersion,
			@Value("${demo.url}") String url, @Value("${spring.profiles.active}") String active) {
		Info info = new Info().title("Demo API - " + active).version(appVersion)
				.description("Spring Boot를 이용한 Demo 웹 애플리케이션 API입니다.")
				.termsOfService("http://swagger.io/terms/")
				.contact(new Contact().name("jini").url("https://blog.jiniworld.me/").email("jini@jiniworld.me"))
				.license(new License().name("Apache License Version 2.0").url("http://www.apache.org/licenses/LICENSE-2.0"));

		List<Server> servers = Arrays.asList(new Server().url(url).description("demo (" + active +")"));

		SecurityScheme basichAuth = new SecurityScheme()
				.type(SecurityScheme.Type.HTTP).scheme("basic");
		SecurityRequirement securityItem = new SecurityRequirement().addList("basicAuth");

		return new OpenAPI()
				.components(new Components().addSecuritySchemes("basicAuth", basichAuth))
				.addSecurityItem(securityItem)
				.info(info)
				.servers(servers);
	}

}

HTTP 타입의 basic scheme을 추가합니다.

설정을 마친 후 다시 Swagger 창을 열어봅니다.

117

이전과 달리 각 api에 자물쇠 그림이 생겼고, Authorize 버튼이 생겼습니다.

Authorize 버튼을 눌러보면 아래와 같은 모달창이 뜹니다.

116

username과 password를 입력받을 수 있네요.
테스트로 jini와 jini123!을 입력한후 Authorize 버튼을 눌러보면 자물쇠가 열린 아이콘이 닫힌 아이콘으로 변한것을 확인할 수 있습니다.

이번에는 api를 실행해봅시다.

118

-H "Authorization: Basic amluaTpqaW5pMTIzIQ==" 라고 Authorization 헤더가 추가된 것을 확인할 수 있네요.

간단하게 디코딩해볼까요?

import java.util.Base64;

public class Test {
	public static void main(String[] args) throws Exception {
		String authorization = "Basic amluaTpqaW5pMTIzIQ==";
		authorization = authorization.replaceAll("^Basic( )*", "");
		String decodedStr = new String(Base64.getDecoder().decode(authorization), "UTF-8");
		System.out.println(decodedStr);
	}
}

prefix에 붙은 Basic을 제거한 후, 디코딩하여 원본 값을 출력해봅니다.
System.out.println으로 출력해보니 jini:jini123!가 나오네요. Base64로 잘 인코딩된 것을 확인할 수 있습니다.


4. Authorization 헤더 관련 익셉션 발생

위의 과정에서 우리는 Swagger에 username과 password를 설정할 경우 자동으로 Authorization 헤더를 추가하도록 만들었습니다.

이번에는 Authorization 헤더가 설정되어있지 않거나, 헤더에 들어있는 값이 부적절할 경우(Base64 방식으로 인코딩되지 않았거나, 원문이 username:password가 아닐 경우) 에러응답을 발생시키도록 해봅시다.

@Aspect를 이용하면 특정 패키지의 메서드를 실행하기 전에 헤더를 체크할 수 있습니다.

@Aspect 애너테이션을 설정한 클래스 파일을 생성합니다.

@Aspect
@Component
public class AuthorizationAspect {

	@Before("execution(public * me.jiniworld.demo.controllers.api.v1..*Controller.*(..)) ")
	public void insertAdminLog(JoinPoint joinPoint) throws AuthorizationHeaderNotExistsException, InvalidTokenException {
		HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();		

		String authorization = request.getHeader("Authorization");
		if(StringUtils.isBlank(authorization)){
			throw new AuthorizationHeaderNotExistsException();
		}
		authorization = authorization.replaceAll("^Basic( )*", "");
		try {
			String decodedStr = new String(Base64.getDecoder().decode(authorization));
			if(decodedStr.indexOf(":") < 0)
				throw new InvalidTokenException();
		} catch(Exception e) {
			throw new InvalidTokenException();
		}
	}
}

me.jiniworld.demo.controllers.api.v1 패키지의 컨트롤러 클래스의 메서드를 실행하기 전에 헤더 유무 및 유효성 체크를 합니다.

Authorization 헤더가 없을 경우 AuthorizationHeaderNotExistsException 익셉션을 발생시키고 (ln 11)

Base64로 디코딩 중 에러가 발생되거나, 디코딩된 문자열에 :가 없을 경우 InvalidTokenException 익셉션을 발생시킵니다.

※ 참고

public class AuthorizationHeaderNotExistsException extends RuntimeException {
	private static final long serialVersionUID = 4858506469476160448L;
	public AuthorizationHeaderNotExistsException() {
		super("Authorization 헤더가 없습니다.");
	}
}
public class InvalidTokenException extends RuntimeException {
	private static final long serialVersionUID = -2832108568693227235L;
	public InvalidTokenException() {
		super("유효하지 않은 토큰입니다.");
	}
}

username과 password를 설정하지 않은 상태에서 api를 테스트 해봅니다.

119

위와 같이 에러를 응답합니다.


5. @ControllerAdvice 를 이용한 익셉션별 response 설정

그러나 위와같은 response는 우리가 원하는 에러 response 가 아닙니다.

우리는 저렇게 지저분한 에러 메시지가 아닌 아래와 같은 간략한 에러 응답을 response로 보내고 싶습니다.

{
  "errorMessage": "401",
  "errorCode": "Authorization 헤더가 없습니다."
}

@ControllerAdvice를 이용하면 Exception별 response를 쉽게 설정할 수 있습니다.

먼저 클래스를 한 후 @ControllerAdvice 애네테이션을 설정합니다.

@ControllerAdvice
public class ExceptionAdvice {

	@ExceptionHandler(AuthorizationHeaderNotExistsException.class)
	protected ResponseEntity<BasicResponse> handleMethodAuthorizationHeaderNotExistsException(AuthorizationHeaderNotExistsException e) {
		HttpStatus status = HttpStatus.UNAUTHORIZED;
		final BasicResponse response = new ErrorResponse(status.value() +"", e.getMessage());
		return new ResponseEntity<>(response, status);
	}

	@ExceptionHandler(InvalidTokenException.class)
	protected ResponseEntity<BasicResponse> handleMethodInvalidTokenException(InvalidTokenException e) {
		HttpStatus status = HttpStatus.UNAUTHORIZED;
	    final BasicResponse response = new ErrorResponse(status.value() +"", e.getMessage());
	    return new ResponseEntity<>(response, status);
	}
}

AuthorizationHeaderNotExistsException, InvalidTokenException 익셉션 발생시, HTTP status code는 401(UNAUTHORIZED)로 설정하며 ErrorResponse 형태로 응답합니다.
에러 메시지는 익셉션에 설정된 에러메시지를 그대로 이용하도록 설정하였습니다.

다시, username과 password를 설정하지 않은 상태에서 api를 테스트 해봅시다.

120

이번에는 위와 같이 깔끔한 형태로 응답이 되는 것을 확인할 수 있습니다


Swagger에 Basic 기본 인증을 설정하는 방법은 매우 간편한 방식입니다.
다만, 만일 헤더가 유출되었을 경우 Base64로 디코딩했을 시, username:password 가 그대로 드러나기 때문에 권장하지 않습니다.

다음 시간에는 이런 단점을 보완한 Bearer Authentication 인증방식을 알아보도록 합시다.


+++

  • OpenAPI 3 Authorization header 설정
  • OpenAPI 3 Authorize 설정
  • Swagger v3 Basic Authentication
  • Swagger v3 HTTP 기본 인증
728x90
반응형