스프링 부트 2.1.0 에는 스프링 5.1.0 프레임워크가 반영되었다.

스프링 5.1.0 은 컴포넌트 탐색과정에서 발생하는 오버헤드를 감소시키기 위한 여러가지 정책이 반영되었는데, 그 중에 하나가 생성한 빈을 덮어쓰는 상황을 강제적으로 제한한다.

그래서 동일한 이름을 가진 스프링 빈이 등록되려고 하면 BeanDefinitionOverrideException 이 발생한다.

DefaultListableBeanFactory 중 일부
BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
if (existingDefinition != null) {
    if (!isAllowBeanDefinitionOverriding()) {
        throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
    }
    else if (existingDefinition.getRole() < beanDefinition.getRole()) {
        // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
        if (logger.isInfoEnabled()) {
            logger.info("Overriding user-defined bean definition for bean '" + beanName +
                    "' with a framework-generated bean definition: replacing [" +
                    existingDefinition + "] with [" + beanDefinition + "]");
        }
    }
    else if (!beanDefinition.equals(existingDefinition)) {
        if (logger.isDebugEnabled()) {
            logger.debug("Overriding bean definition for bean '" + beanName +
                    "' with a different definition: replacing [" + existingDefinition +
                    "] with [" + beanDefinition + "]");
        }
    }
    else {
        if (logger.isTraceEnabled()) {
            logger.trace("Overriding bean definition for bean '" + beanName +
                    "' with an equivalent definition: replacing [" + existingDefinition +
                    "] with [" + beanDefinition + "]");
        }
    }
    this.beanDefinitionMap.put(beanName, beanDefinition);
}

이는 스프링 부트의 설정이 아닌 스프링 프레임워크에서 빈을 등록하는 과정에서 발생하는 것으로 이에 대한 속성을 비활성화할 수 있는 기능을 제공한다.

spring.main.allow-bean-definition-overriding: true

위와 같이 spring.main.allow-bean-definition-overriding 속성을 true 로 선언하면 문제가 해결될 것이다. 이후에는 중복되는 빈을 찾아서 중복이 발생하지 않도록 조치해야 한다.



스프링 부트 2.0 정식 출시

스프링 부트 2.0 새로운 점:

  • A Java 8 baseline, and Java 9 support.
  • Reactive web programming support with Spring WebFlux/WebFlux.fn.
  • Auto-configuration and starter POMs for reactive Spring Data Cassandra, MongoDB, Couchbase and Redis.
  • Support for embedded Netty.
  • HTTP/2 for Tomcat, Undertow and Jetty.
  • A brand new actuator architecture, with support for Spring MVC, WebFlux and Jersey.
  • Micrometer based metrics with exporters for Atlas, Datadog, Ganglia, Graphite, Influx, JMX, New Relic, Prometheus, SignalFx, StatsD and Wavefront.
  • Quartz scheduler support.
  • Greatly simplified security auto-configuration.

정리

기다리고 기다리던 스프링 부트 2.0이 정식으로 출시되었다.

스프링 부트 2.0 출시에 맞춰서 쓰고 있는 책은 내일 교정본을 받고 최대한 빨리 출간할 예정이다.

스프링 부트 2.0에서 크게 달라진 점은 스프링 프레임워크 5를 기반으로 하여 리액티브 지원이 가장 큰 부분이라고 생각된다.

아직까지는 리액티브 웹 프로그래밍을 적극적으로 활용한 경험은 없는 상태라서 살펴봐야할 부분이 많다.

살펴볼 수 있다.

관심가는 변화 중 하나는 JDBC 라이브러리가 Tomcat JDBC에서 HikariCP 로 변경되었다.

기존에는 Tomcat JDBC를 제외(exclude)하고 HikariCP 의존성을 추가해야했지만 지금은 굳이 그럴 필요가 없어졌다.

그리고 운영과 관련된 액츄에이터(Actuator)에서 DropWizard에 맞춰 지원하던 형식이 Micrometer에서 쉽게 사용할 수 있도록 개편되었다. 더불어서 액츄에이터 기본경로에 /actuator가 접두사로 붙게 되었다.

그레이들 관련한 플러그인도 조금 더 개선이 될 것으로 보인다.

참고


새로운 프로젝트를 시작하면서 스프링 시큐리티 5.0을 적용하고 있다. 어떤 새로운 기능이 있는지 확인하기 위해서 참고문서(What’s New in Spring Security 5.0)를 살펴봤다. 새로운 기능이 추가되었는데 대략 다음과 같다.

Spring Security 5 새로운점

이 중에서 크게 관심을 끄는 항목은 '현대화된 비밀번호 인코딩' 항목이었다. 이전까지는 BcryptPasswordEncoder를 기본으로 단방향 암호화인코더로 사용해왔다.

PasswordEncoder passwordEncoder = new BcryptPasswordEncoder();

Spring Security’s PasswordEncoder interface is used to perform a one way transformation of a password to allow the password to be stored securely. Given PasswordEncoder is a one way transformation, it is not intended when the password transformation needs to be two way (i.e. storing credentials used to authenticate to a database). Typically PasswordEncoder is used for storing a password that needs to be compared to a user provided password at the time of authentication.

위의 내용을 구글번역기로 돌려보면

스프링 시큐리티의 PasswordEncoder 인터페이스는 패스워드를 단방향으로 변환하여 패스워드를 안전하게 저장할 수있게 해준다. PasswordEncoder는 편도 변환이며, 암호 변환이 양방향 (즉, 데이터베이스 인증에 사용되는 자격 증명 저장) 일 필요가있는 경우에는 제공되지 않습니다. 일반적으로 PasswordEncoder는 인증시 사용자가 제공한 암호와 비교해야하는 암호를 저장하는 데 사용됩니다.

대충 정리하면, 스프링 시큐리티에서 제공하는 PasswordEncoder는 사용자가 등록한 비밀번호를 단방향으로 변환하여 저장하는 용도로 사용된다. 그리고 시대적인 흐름에 따라서 점점 고도화된 암호화 알고리즘 구현체가 적용되어간다. 이런 과정에서 서비스에 저장된 비밀번호에 대한 암호화 알고리즘을 변경하는 일은 상당히 많은 노력을 요구하게 된다.

단방향의 변환된 암호를 풀어서 다시 암호화해야 하는데 그게 말처럼 쉬운 일은 아니다.

그래서 스프링시큐리티에서 내놓은 해결책이 DelegatingPasswordEncoder 다.

사용방법은 간단하다.

public class PasswordEncoderTest {
private PasswordEncoder passwordEncoder;
@Before
public void setUp() throws Exception {
passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Test
public void encode() {
String password = "password";
String encPassword = passwordEncoder.encode(password);
assertThat(passwordEncoder.matches(password, encPassword)).isTrue();
assertThat(encPassword).contains("{bcrypt}"); (1)
}
}
PasswordEncoderFactories.createDelegatingPasswordEncoder()로 생성한 PasswordEncoder는 BCryptPasswordEncoder가 사용되며 앞에 {id} PasswordEncoder 유형이 정의된다.

PasswordEncoderFactories.createDelegatingPasswordEncoder에 정의되어 있는 PasswordEncoder 종류를 살펴보면 다음과 같다.

public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}

생성되는 암호화코드의 종류는 대략 다음과 같다.

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

{id}가 없는 비밀번호의 경우에는 다음과 같이 선언해서 확인작업이 가능하다.

@Test
public void 암호변환기ID가_없는경우는_다음과같이() {
String password = "password";
String encPassword = "$2a$10$Ot44NE6k1kO5bfNHTP0m8ejdpGr8ooHGT90lOD2/LpGIzfiS3p6oq"; // bcrypt
DelegatingPasswordEncoder delegatingPasswordEncoder = (DelegatingPasswordEncoder) PasswordEncoderFactories.createDelegatingPasswordEncoder();
delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());
assertThat(delegatingPasswordEncoder.matches(password, encPassword)).isTrue();
}

DelegatingPasswordEncoder를 이용하면 암호화 알고리즘 변경에 대한 걱정은 크게 하지 않아도 되겠다. 사용 전략에 대해서는 코드를 살펴보고 각자가 판가름하기 바란다.

테스트 코드는 다음과 같다.


기존에 저장되어 있는 암호화된 비밀번호를 DelegatingPasswordEncoder에서 사용할 수 있도록 이전하는 작업은 간단하다.

$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

처럼 앞에 {bcrypt}만 넣어주면 된다.

그 다음 단계에 대해서는 각자 고민해보자.

부연설명

PasswordEncoder 자체가 단방향 암호화를 목적으로 생성되었다. 보안상의 원인으로 DB에 저장된 비밀번호가 유출되지 않는다면 변환된 비밀번호 앞에 {id}가 붙는다고 해서 크게 문제가 되지는 않는다고 생각한다…​ 유출되었을 때는 문제가 될 수도 있으려나?


+ Recent posts