본문 바로가기
프로그래밍/기타

[Spring Security] Spring Security 기본 아키텍처, 맛보기 샘플, 그리고 default 값

by 사바라다 2022. 12. 4.

안녕하세요. 이전시간에 서비스의 일반적인 보안 취약성 이라는 주제로 개발하면서 지켜야할 기본적인 보안에 대해서 알아보는 시간을 가져보았습니다. 오늘은 Spring Security를 이용하여 실전으로 서비스의 보안을 강화해 보는 시간을 가져보도록 하겠습니다. 오늘은 그 첫 시간으로 Spring Security의 기본에 대해서 알아보는 시간을 가져보겠습니다.

Spring Security 란

Spring Security는 인증(authentication)과 인가(authorization) 그리고 일반적인 서비스 공격을 막아낼 수 있는 프레임워크입니다. 이 프레임워크는 servlet 서비스 뿐만 아니라 reactive 서비스 역시 사용 가능하며 Spring 기반 어플리케이션 보안의 사실상 표준입니다. 최고의 장점으로는 커스터마이징의 용이함이 있습니다.

Spring Security의 기본 메커니즘

Spring Security을 Servlet에서 사용할때 가장 기본적으로 사용하는 Servlet의 개념은 Filter의 개념입니다. Filter란 요청이 Servlet으로 실제로 들어오기 전에 먼저 거치는 Component의 역할을 하게 됩니다. 이러한 Spring Security와 Filter에 대한 이야기는 이 아키텍처 문서에 잘 설명하고 있습니다. 추후에 별도로 해당 문서를 분석해보는 시간을 가져보도록 하고 이번 포스팅에서는 넘어가도록 하겠습니다.

위 이미지가 Spring Security에서 Filter를 사용하는 기본적인 구조입니다. 구성요소는 아래와 같습니다. 아래의 구성요소(Component)를 통해서 인증/인가를 구현하는 것입니다.

  • Authentication Filter
  • Authentication Manager
    • 다양한 Authentication Provider를 사용할 수 있도록 한다.
  • Authentication Provider
    • UserDetailsService와 PasswordEncoder를 통해서 인증과정을 제공한다.
  • UserDetailsService
    • 사용자에 대한 세부 정보를 저장한다.
  • PasswordEncoder
    • 암호를 인코딩합니다.
    • 암호가 기존 인코딩과 일치하는지 확인합니다.

위의 구성요소들은 아래와 같은 시나리오를 가지며 유기적으로 동작하게 됩니다.

  1. Authentication Filter는 Request를 받으면 이를 Authentication Manager에개 위임하고 응답을 대기합니다.
  2. Authentication Manager는 Authentication Provider를 이용해 인증을 처리합니다.
  3. Authentication Provider는 UserDetails와 PasswordEncoder를 이용해서 인증이 올바른지 확인합니다.
  4. Authentication Provider는 Manager에게 Manager는 Filter에 응답을 전달합니다.
  5. Authentication Filter는 응답을 바탕으로 Security Context를 구성합니다.

실습

위에서 간단하게 Spring Security의 인증 과정을 알아보았습니다. 이제 실습을 통해서 Spring Security를 사용해보도록 합시다. 실습은 kotlin, spring boot, servlet 환경에서 진행해보도록 하겠습니다. 오늘 실습할 내용은 Spring Security의 기본적인 사용방법입니다.

환경 설정

먼서 환경설정입니다. 오늘 실습은 kotlin, spring boot 환경에서 진행하도록 하겠습니다.

Spring Security을 Spring Boot 서블릿 환경에서 사용하기 위해서는 아래와 같은 의존성이 필요합니다. spring-boot-starter-web은 Spring Boot를 Servlet 환경에서 띄우는 의존성입니다. 그리고 spring-boot-starter-security는 Spring Security를 사용한다는 의존성입니다.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-web")
}

Controller 코드

권한에 대해서 사용할 예제코드는 아래와 같습니다.

@RestController
@RequestMapping("/example")
class ExampleController {

    @GetMapping
    fun example(): String {
        return "done"
    }

}

기본 설정 후 실행

이렇게 설정한 후 바로 실행하여 기본값을 확인해보도록 하겠습니다. Spring Boot 서비스가 올라오면서 아래와 같은 내용을 확인하실 수 있습니다. 이곳에 표시되는 비밀번호는 인증에 사용할 수 있는 password값 입니다. 해당 값은 서비스가 새롭게 뜰때마다 바뀌게되며 ID는 기본적으로 user를 사용하게 되며 인증 방식으로는 Basic Auth를 사용하게됩니다.

Using generated security password: 9bf551bf-79eb-4f0f-bafc-266477fb437f

This generated password is for development use only. Your security configuration must be updated before running your application in production.

Request 401

서비스를 실행한 후 위의 Controller 예제 interface에 Request를 한번 해보도록 하겠습니다. 응답으로 401 Unauthorized을 반환 받은것을 확인할 수 있었습니다. 이 이유는 Reuqest에 인증 정보가 없었기 때문입니다. 정상적으로 응답 받기 위해서는 Request에 인증 정보를 함께 전달하여야합니다.

➜  ~ curl -i -X GET http://localhost:8080/example

HTTP/1.1 401
Set-Cookie: JSESSIONID=2FEE2CA1768018BD6F4BEB213488B1EF; Path=/; HttpOnly
WWW-Authenticate: Basic realm="Realm"
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Sun, 04 Dec 2022 08:18:40 GMT

Request 200

위의 예제에서 요청에 인증 정보를 함께 실어 보내야한다는 것을 확인했습니다. 인증정보는 curl을 이용하면 -u {id}:{password} 옵션을 이용하면 적용하여 보낼 수 있습니다.

➜  ~ curl -i -u user:9bf551bf-79eb-4f0f-bafc-266477fb437f -X GET http://localhost:8080/example

HTTP/1.1 200
Set-Cookie: JSESSIONID=D58CD52CB611DC096FA5C0D39400D01C; Path=/; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 4
Date: Sun, 04 Dec 2022 08:21:24 GMT

done%

만약 curl이 익숙치 않으시다면 사이트에서 id와 password를 입력하면 입력해야할 해더값이 Base64로 변환되어 출력됩니다. 해당하는 값을 Authorization이라고 하는 헤더에 넣고 호출할 수도 있습니다. 아래와 같이 호출해도 동일한 결과값을 얻을 수 있습니다.

curl -i -H "Authorization: Basic dXNlcjpmZmE1MGJkZC00MWIxLTQ5NDEtYWY1YS0xNTA1MzAzOGZiMWI=" -X GET http://localhost:8080/example

default 코드 분석하기

우리는 위의 샘플에서 아무런 설정을 해주지 않았지만 기본적으로 Spring Security에서 설정해주는 기본 값이 있다는 사실을 알 수 있었습니다. 위에서 살펴보았던 아키텍처에서 UserDetailsService와 PasswordEncoder가 기본적으로 들어가는 부분이 있다는 것입니다. 그렇다면 어떠한 값들이 어떻게 설정되는지 코드로 알아보는 시간을 가져보겠습니다.

UserDetailsServiceAutoConfiguration

위 설정값은 이전에 자동으로 생성되던 Password Logging 을 찍어두던 클래스를 따라가는 것으로 시작해보겠습니다. 해당 클래스는 UserDetailsServiceAutoConfiguration입니다. 해당클래스는 아래와 같이 소개하고 있습니다. 즉, 아무런 설정을 하지 않았을 때 기본적으로 구현에 사용되는 Security 설정 값이라는 것입니다. 또한 서비스의 in-memory를 이용해서 구현하고 있다는 사실도 설명되어있습니다.

{@link EnableAutoConfiguration Auto-configuration} for a Spring Security in-memory
{@link AuthenticationManager}. Adds an {@link InMemoryUserDetailsManager} with a
default user and generated password. This can be disabled by providing a bean of type
{@link AuthenticationManager}, {@link AuthenticationProvider} or
{@link UserDetailsService}.

코드를 한번 보도록 하겠습니다. 코드는 UserDetailsService Interface에 대해서 InMemoryUserDetailsManager Bean을 구현하는 것으로 되어있습니다. 전체적인 코드를 보았을 때 properties에서 User 값을 가져와 사용한다는 것을 알수 있었습니다. Properties의 User의 값들이 default 값이라는 것을 유추할 수 있습니다.

@Bean
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
        ObjectProvider<PasswordEncoder> passwordEncoder) {
    SecurityProperties.User user = properties.getUser(); // properties에서 User를 가져오기
    List<String> roles = user.getRoles();
    return new InMemoryUserDetailsManager( // InMemory에 User 정보 저장
            User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
                    .roles(StringUtils.toStringArray(roles)).build());
}

private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
    String password = user.getPassword();
    if (user.isPasswordGenerated()) {
        logger.warn(String.format(
                "%n%nUsing generated security password: %s%n%nThis generated password is for development use only. "
                        + "Your security configuration must be updated before running your application in "
                        + "production.%n",
                user.getPassword()));
    }
    if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
        return password;
    }
    return NOOP_PASSWORD_PREFIX + password;
}

그리고 코드를 따라 들어갔을 때 User의 default 정보는 아래와 같습니다. name, password 모두 아래와 같은 default 정보값이 존재한다는 사실을 알 수 있었습니다.

public static class User {

    /**
        * Default user name.
        */
    private String name = "user";

    /**
        * Password for the default user name.
        */
    private String password = UUID.randomUUID().toString();

    /**
        * Granted roles for the default user name.
        */
    private List<String> roles = new ArrayList<>();

    private boolean passwordGenerated = true;
}

마무리

오늘은 이렇게 Spring Security의 의존성을 받아서 기본적으로 한번 실행해보았습니다.

그리고 마지막에는 해당 값들이 어떤 코드에서 오는지도 확인해보았습니다.

감사합니다.

참조

[1] Spring Security In Action

[2] https://docs.spring.io/spring-security/reference/servlet/architecture.html#requestcache

댓글