토리맘의 한글라이즈 프로젝트 logo 토리맘의 한글라이즈 프로젝트

스프링 부트 공식 레퍼런스를 한글로 번역한 문서입니다.

전체 목차는 여기에 있습니다.

목차


7.10. Security

클래스패스에 스프링 시큐리티가 있으면 디폴트로 웹 애플리케이션을 보호해준다. 스프링 부트는 스프링 시큐리티의 content-negotiation 전략에 따라 httpBasic이나 formLogin 사용 여부를 결정한다. 메소드 레벨 보안을 사용하고 싶을 땐 애플리케이션에 원하는 설정으로 @EnableGlobalMethodSecurity를 추가해도 된다. 추가 정보는 스프링 시큐리티 레퍼런스 가이드에서 확인할 수 있다.

디폴트 UserDetailsService는 단일 사용자를 갖는다. user name은 user이고, password는 랜덤으로 결정되며, 아래 보이는 것처럼 애플리케이션이 시작할 때 INFO 레벨로 password를 출력한다:

Using generated security password: 78fa095d-3f4c-48b1-ad50-e24c31d5cf35

로그 설정을 세분화해놨다면, org.springframework.boot.autoconfigure.security 카테고리는 INFO 레벨 메세지를 출력하도록 설정돼 있는지 확인해봐야 한다. INFO 레벨을 기록하지 않게 되어 있다면 디폴트 패스워드를 출력하지 않는다.

spring.security.user.namespring.security.user.password를 제공하면 username과 password를 변경할 수 있다.

웹 애플리케이션에 기본으로 제공하는 기능은 다음과 같다:

다른 AuthenticationEventPublisher를 사용하고 싶다면 전용 빈을 추가하면 된다.

7.10.1. MVC Security

디폴트 시큐리티 설정은 SecurityAutoConfigurationUserDetailsServiceAutoConfiguration에서 구현한다. SecurityAutoConfiguration은 웹 보안을 위해 SpringBootWebSecurityConfiguration을 임포트하며, UserDetailsServiceAutoConfiguration은 웹 애플리케이션이 아니더라도 관련 있는 인증 설정을 구성해준다. 디폴트 웹 애플리케이션 시큐리티 설정을 완전히 꺼버리거나, OAuth2 클라이언트나 리소스 서버같은 스프링 시큐리티 컴포넌트를 여러 개 조합하려면 SecurityFilterChain 타입 빈을 추가해라 (이렇게 해도 UserDetailsService 설정이나 액추에이터의 보안은 비활성화되지 않는다).

UserDetailsService 설정도 꺼버리려면 UserDetailsService나, AuthenticationProvider, AuthenticationManager 타입 빈을 추가하면 된다.

커스텀 SecurityFilterChain이나 WebSecurityConfigurerAdapter 빈을 추가해도 액세스 규칙을 재정의할 수 있다. 스프링 부트에선 좀 더 편하게 액추에이터 엔드포인트와 스태틱 리소스에 대한 액세스 규칙을 재정의할 수 있다. management.endpoints.web.base-path 프로퍼티 기반 RequestMatcher를 생성할 때는 EndpointRequest를 활용할 수 있다. 공통으로 사용하는 위치에 있는 리소스에 대한 RequestMatcher를 생성할 땐 PathRequest를 사용하면 된다.

7.10.2. WebFlux Security

웹플럭스에서도 스프링 MVC와 유사하게 spring-boot-starter-security 의존성을 추가해서 애플리케이션을 보호할 수 있다. 디폴트 시큐리티 설정은 ReactiveSecurityAutoConfigurationUserDetailsServiceAutoConfiguration에서 구현한다. ReactiveSecurityAutoConfiguration은 웹 보안을 위해 WebFluxSecurityConfiguration을 임포트하며, UserDetailsServiceAutoConfiguration은 웹 애플리케이션이 아니더라도 관련 있는 인증 설정을 구성해준다. 디폴트 웹 애플리케이션 시큐리티 설정을 완전히 꺼버리려면 WebFilterChainProxy 타입 빈을 추가하면 된다 (이렇게 해도 UserDetailsService 설정이나 액추에이터의 보안은 비활성화되지 않는다).

UserDetailsService 설정도 꺼버리려면 ReactiveUserDetailsServiceReactiveAuthenticationManager 타입 빈을 추가하면 된다.

액세스 규칙을 바꾸거나, OAuth 2 클라이언트나 리소스 서버같은 스프링 시큐리티 컴포넌트를 여러 개 사용하고 싶을 땐 커스텀 SecurityWebFilterChain 빈을 추가하면 된다. 스프링 부트에선 좀 더 편하게 액추에이터 엔드포인트와 스태틱 리소스에 대한 액세스 규칙을 재정의할 수 있다. management.endpoints.web.base-path 프로퍼티 기반 ServerWebExchangeMatcher를 생성할 때는 EndpointRequest를 활용할 수 있다.

공통으로 사용하는 위치에 있는 리소스에 대한 ServerWebExchangeMatcher를 생성할 땐 PathRequest를 사용하면 된다.

예를 들면, 아래와 같은 코드로 시큐리티 설정을 커스텀할 수 있다:

@Configuration(proxyBeanMethods = false)
public class MyWebFluxSecurityConfiguration {

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.authorizeExchange((spec) -> {
            spec.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
            spec.pathMatchers("/foo", "/bar").authenticated();
        });
        http.formLogin();
        return http.build();
    }

}

7.10.3. OAuth2

OAuth2는 널리 사용하는 인증 프레임워크로, 스프링에서도 지원한다.

Client

클래스패스에 spring-security-oauth2-client가 있으면 몇 가지 자동 설정을 통해 OAuth2/Open ID Connect 클라이언트를 활용할 수 있다. 이 설정에선 OAuth2ClientProperties 밑에 있는 프로퍼티를 사용한다. 서블릿과 리액티브 애플리케이션 모두 이 프로퍼티를 사용한다.

다음 예제처럼 spring.security.oauth2.client 프리픽스로 여러 가지 OAuth2 클라이언트와 provider를 등록할 수 있다:

properties yaml
spring.security.oauth2.client.registration.my-client-1.client-id=abcd
spring.security.oauth2.client.registration.my-client-1.client-secret=password
spring.security.oauth2.client.registration.my-client-1.client-name=Client for user scope
spring.security.oauth2.client.registration.my-client-1.provider=my-oauth-provider
spring.security.oauth2.client.registration.my-client-1.scope=user
spring.security.oauth2.client.registration.my-client-1.redirect-uri=https://my-redirect-uri.com
spring.security.oauth2.client.registration.my-client-1.client-authentication-method=basic
spring.security.oauth2.client.registration.my-client-1.authorization-grant-type=authorization-code

spring.security.oauth2.client.registration.my-client-2.client-id=abcd
spring.security.oauth2.client.registration.my-client-2.client-secret=password
spring.security.oauth2.client.registration.my-client-2.client-name=Client for email scope
spring.security.oauth2.client.registration.my-client-2.provider=my-oauth-provider
spring.security.oauth2.client.registration.my-client-2.scope=email
spring.security.oauth2.client.registration.my-client-2.redirect-uri=https://my-redirect-uri.com
spring.security.oauth2.client.registration.my-client-2.client-authentication-method=basic
spring.security.oauth2.client.registration.my-client-2.authorization-grant-type=authorization_code

spring.security.oauth2.client.provider.my-oauth-provider.authorization-uri=https://my-auth-server/oauth/authorize
spring.security.oauth2.client.provider.my-oauth-provider.token-uri=https://my-auth-server/oauth/token
spring.security.oauth2.client.provider.my-oauth-provider.user-info-uri=https://my-auth-server/userinfo
spring.security.oauth2.client.provider.my-oauth-provider.user-info-authentication-method=header
spring.security.oauth2.client.provider.my-oauth-provider.jwk-set-uri=https://my-auth-server/token_keys
spring.security.oauth2.client.provider.my-oauth-provider.user-name-attribute=name
spring:
  security:
    oauth2:
      client:
        registration:
          my-client-1:
            client-id: "abcd"
            client-secret: "password"
            client-name: "Client for user scope"
            provider: "my-oauth-provider"
            scope: "user"
            redirect-uri: "https://my-redirect-uri.com"
            client-authentication-method: "basic"
            authorization-grant-type: "authorization-code"

          my-client-2:
            client-id: "abcd"
            client-secret: "password"
            client-name: "Client for email scope"
            provider: "my-oauth-provider"
            scope: "email"
            redirect-uri: "https://my-redirect-uri.com"
            client-authentication-method: "basic"
            authorization-grant-type: "authorization_code"

        provider:
          my-oauth-provider:
            authorization-uri: "https://my-auth-server/oauth/authorize"
            token-uri: "https://my-auth-server/oauth/token"
            user-info-uri: "https://my-auth-server/userinfo"
            user-info-authentication-method: "header"
            jwk-set-uri: "https://my-auth-server/token_keys"
            user-name-attribute: "name"

OpenID Connect 디스커버리를 지원하는 OpenID Connect provider는 설정을 더 간소화할 수 있다. 이땐 issuer-uri에 provider의 Issuer 식별자를 URI로 설정해야 한다. 예를 들어서 issuer-uri를 “https://example.com”으로 설정하면, “https://example.com/.well-known/openid-configuration”으로 OpenID Provider Configuration Request를 보낸다. 응답으론 OpenID Provider Configuration Response를 받을 것으로 기대한다. 다음은 issuer-uri로 OpenID Connect provider를 설정하는 예시다:

properties yaml
spring.security.oauth2.client.provider.oidc-provider.issuer-uri=https://dev-123456.oktapreview.com/oauth2/default/
spring:
  security:
    oauth2:
      client:
        provider:
          oidc-provider:
            issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/"

스프링 시큐리티의 OAuth2LoginAuthenticationFilter는 기본적으로 /login/oauth2/code/*와 매칭되는 URL만 처리한다. redirect-uri에서 사용할 패턴을 바꾸고 싶으면 커스텀 패턴을 처리하기 위한 설정을 제공해야 한다. 예를 들어 서블릿 애플리케이션에선 다음과 유사한 자체 SecurityFilterChain을 추가할 수 있다:

@Configuration(proxyBeanMethods = false)
public class MyOAuthClientConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated();
        http.oauth2Login().redirectionEndpoint().baseUri("custom-callback");
        return http.build();
    }

}

스프링 부트는 스프링 시큐리티에서 클라이언트 registration을 관리하기 위해 사용하는 InMemoryOAuth2AuthorizedClientService를 자동으로 설정한다. InMemoryOAuth2AuthorizedClientService는 기능에 제한이 있으므로 개발 환경에서만 사용하길 권장한다. 프로덕션 환경에선 JdbcOAuth2AuthorizedClientService를 사용하거나 자체 OAuth2AuthorizedClientService 구현체를 생성하는 게 좋다.

OAuth2 client registration for common providers

구글, 깃허브, 페이스북, 옥타 등, 많이 사용하는 OAuth2, OpenID provider를 위한 기본값들을 제공하고 있다 (각각 google, github, facebook, okta).

이런 provider를 커스텀할 필요가 없다면, provider 속성을 이 중 하나로 설정하면 되며, 이때 기본값들은 직접 유추해야 한다. 스프링 부트는 클라이언트 registration 키가 지원하는 디폴트 provider와 일치할 때도 유추해낸다.

즉, 아래 예제에 있는 두 설정은 Google provider를 사용한다:

properties yaml
spring.security.oauth2.client.registration.my-client.client-id=abcd
spring.security.oauth2.client.registration.my-client.client-secret=password
spring.security.oauth2.client.registration.my-client.provider=google
spring.security.oauth2.client.registration.google.client-id=abcd
spring.security.oauth2.client.registration.google.client-secret=password
spring:
  security:
    oauth2:
      client:
        registration:
          my-client:
            client-id: "abcd"
            client-secret: "password"
            provider: "google"
          google:
            client-id: "abcd"
            client-secret: "password"

Resource Server

클래스패스에 spring-security-oauth2-resource-server가 있으면 스프링 부트에서 OAuth2 리소스 서버를 설정할 수 있다. JWT 설정에선 다음 예시처럼 JWK Set URI나 Issuer URI를 지정해야 한다:

properties yaml
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://example.com/oauth2/default/v1/keys
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: "https://example.com/oauth2/default/v1/keys"
properties yaml
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://dev-123456.oktapreview.com/oauth2/default/
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/"

인증 서버가 JWK Set URI를 지원하지 않을 때는 JWT 서명을 확인할 때 사용하는 공개 키로 리소스 서버를 구성할 수 있다. 이땐 spring.security.oauth2.resourceserver.jwt.public-key-location 프로퍼티를 사용하면 된다. 이 프로퍼티는 PEM으로 인코딩된 x509 형식의 공개 키를 가지고 있는 파일을 가리켜야 한다.

서블릿, 리액티브 애플리케이션 모두 같은 프로퍼티를 사용한다.

아니면 서블릿 애플리케이션에선 JwtDecoder 빈을, 리액티브 애플리케이션에선 ReactiveJwtDecoder를 정의할 수도 있다.

JWT 대신 opaque 토큰을 사용할 때는, 아래 프로퍼티를 설정하면 introspection을 통해 토큰의 유효성을 검증할 수 있다:

properties yaml
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://example.com/check-token
spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id
spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret
spring:
  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: "https://example.com/check-token"
          client-id: "my-client-id"
          client-secret: "my-client-secret"

다시 말하지만, 서블릿과 리액티브 애플리케이션 모두 같은 프로퍼티를 사용한다.

아니면 서블릿 애플리케이션에선 자체 OpaqueTokenIntrospector 빈을, 리액티브 애플리케이션에선 ReactiveOpaqueTokenIntrospector를 정의해도 된다.

Authorization Server

현재 스프링 시큐리티는 OAuth 2.0 Authorization Server 구현을 지원하지 않는다. 하지만 이 기능은 Spring Security OAuth 프로젝트에서 이용할 수 있으며, 궁극적으로는 스프링 시큐리티로 완전히 대체할 예정이다. 그때까지는 spring-security-oauth2-autoconfigure 모듈을 사용해서 OAuth 2.0 인증 서버를 간단히 설정할 수 있다. 상세 가이드는 문서를 참고해라.

7.10.4. SAML 2.0

Relying Party

클래스패스에 spring-security-saml2-service-provider가 있으면 몇 가지 자동 설정을 통해 SAML 2.0 Relying Party를 활용할 수 있다. 이 설정에선 Saml2RelyingPartyProperties 밑에 있는 프로퍼티를 사용한다.

relying party registration에선 Identity ProviderIDP와 Service ProviderSP 설정이 쌍을 이룬다. 다음 예제처럼 spring.security.saml2.relyingparty 프리픽스로 여러 가지 relying party를 등록할 수 있다:

properties yaml
spring.security.saml2.relyingparty.registration.my-relying-party1.signing.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party1.signing.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party1.decryption.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party1.decryption.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party1.identityprovider.verification.credentials[0].certificate-location=path-to-verification-cert
spring.security.saml2.relyingparty.registration.my-relying-party1.identityprovider.entity-id=remote-idp-entity-id1
spring.security.saml2.relyingparty.registration.my-relying-party1.identityprovider.sso-url=https://remoteidp1.sso.url

spring.security.saml2.relyingparty.registration.my-relying-party2.signing.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party2.signing.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party2.decryption.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party2.decryption.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party2.identityprovider.verification.credentials[0].certificate-location=path-to-other-verification-cert
spring.security.saml2.relyingparty.registration.my-relying-party2.identityprovider.entity-id=remote-idp-entity-id2
spring.security.saml2.relyingparty.registration.my-relying-party2.identityprovider.sso-url=https://remoteidp2.sso.url
spring:
  security:
    saml2:
      relyingparty:
        registration:
          my-relying-party1:
            signing:
              credentials:
              - private-key-location: "path-to-private-key"
                certificate-location: "path-to-certificate"
            decryption:
              credentials:
              - private-key-location: "path-to-private-key"
                certificate-location: "path-to-certificate"
            identityprovider:
              verification:
                credentials:
                - certificate-location: "path-to-verification-cert"
              entity-id: "remote-idp-entity-id1"
              sso-url: "https://remoteidp1.sso.url"

          my-relying-party2:
            signing:
              credentials:
              - private-key-location: "path-to-private-key"
                certificate-location: "path-to-certificate"
            decryption:
              credentials:
              - private-key-location: "path-to-private-key"
                certificate-location: "path-to-certificate"
            identityprovider:
              verification:
                credentials:
                - certificate-location: "path-to-other-verification-cert"
              entity-id: "remote-idp-entity-id2"
              sso-url: "https://remoteidp2.sso.url"

7.10.5. Actuator Security

보안 상의 이유로 /health를 제외한 모든 액추에이터는 기본적으로 비활성화된다. 액추에이터는 management.endpoints.web.exposure.include 프로퍼티로 활성화할 수 있다.

클래스패스에 스프링 시큐리티가 있으면서 별 다른 WebSecurityConfigurerAdapterSecurityFilterChain 빈이 없으면, /health를 제외한 모든 액추에이터를 스프링 부트 자동 설정으로 보호한다. 커스텀 WebSecurityConfigurerAdapterSecurityFilterChain 빈을 정의하면 스프링 부트 자동 설정은 취소되며, 직접 액추에이터 액세스 규칙을 완전히 제어할 수 있다.

management.endpoints.web.exposure.include를 설정하기 전에 먼저, 노출하는 액추에이터에 민감한 정보가 포함되어 있지는 않은지 확인해봐라. 앞에 방화벽을 두거나 스프링 시큐리티 등을 활용해 보호하는 방법도 있다.

Cross Site Request Forgery Protection

스프링 부트는 스프링 시큐리티의 기본값을 사용하기 때문에, 디폴트로 CSRF 보호를 활성화한다. 따라서 디폴트 시큐리티 설정을 사용하면, POST나 (shutdown, loggers 엔드포인트), PUT, DELETE가 필요한 액추에이터 엔드포인트에선 403 forbidden 에러가 발생한다.

개발 중인 서비스를 사용하는 클라이언트가 브라우저가 아닐 때만 CSRF 보호를 완전히 비활성화하는 게 좋다.

CSRF 보호에 관한 추가 정보는 스프링 시큐리티 레퍼런스 가이드에서 확인할 수 있다.


전체 목차는 여기에 있습니다.

<< >>

TOP