새로운 프로젝트를 시작하면서 스프링 시큐리티 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}가 붙는다고 해서 크게 문제가 되지는 않는다고 생각한다…​ 유출되었을 때는 문제가 될 수도 있으려나?


Spring은 사용하려는 프로파일을 정의하여 상황에 따라 컴포넌트에 대한 등록 및 제외를 결정할 수 있다.

@Profile({"dev", "!kr"})

이라고 프로파일을 정의하면 조건식은 dev or !kr 이 되어 dev 혹은 kr 에 대해서 선언되어 있지 않으면 반드시 실행되는 상황이 발생한다.

이런 상황을 피할 수 있는 방법으로 Spring 4.0에서 추가된 @@Conditional 을 사용하는 방법이 있다. 이와 관련한 질문은 How to conditionally declare Bean when multiple profiles are not active? 를 살펴보면 고민하고 있는 유사한 내용과 답변을 볼 수 있다.

간단한 해결책은 Condition을 구현하는 것이다. 다음과 같이 간단하게 dev and !kr을 만족하는 조건식을 작성해보자.

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class PrdAndIgnoreKrProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().acceptsProfiles("prd") && !context.getEnvironment().acceptsProfiles("kr");
}
}

위의 클래스를 사용하여

@Profile({"dev", "!kr"})
//을 대신하여
@Conditional(PrdAndIgnoreKrProfileCondition.class)
// 으로 정의하면 dev and !kr 조건식이 적용가능해진다.


현재 회사에서는 Spring Boot + AWS Beanstalk 조합으로 서비스를 운영하고 있다. 빈즈톡을 이용하여 로드밸런싱 처리를 할 때 nginx 를 사용하고자 하는 경우 빈즈톡에서 사용하는 인스턴스 내에서 nginx 설정을 변경해도 반영되지 않는 문제가 발생한다.

AWS 의 개발가이드가 완전히 한국어화가 되지 않았기 때문에 찾아보기도 이해하기도 쉽지 않다.

이에 프로젝트 내에 빈즈톡과 관련된 설정파일을 두어 배포시 빈즈톡 내에 위치한 nginx 설정 파일 /etc/nginx/conf.d 에 위치하도록 할 수 있다.

적용방법

  1. 프로젝트 내에 aws 설정파일(.conf) 를 둘 디렉토리를 지정하라.

    • ex) $ mkdir boot-spring-boot/aws

  2. boot-spring-boot/aws 하위에 .ebextensions/nginx/conf.d/elasticbeanstalk 디렉토리를 생성한다.

엘라스틱 빈즈톡의 기본 nginx 를 확장하기 위해서는 .ebextensions/nginx/conf.d/ 폴더에 .conf 설정파일을 추가하면 된다. 그러면 자동으로 엘라스틱 빈즈톡의 nginx 구성에 .ebextensions/nginx/conf.d/ 에 있는 .conf 파일들이 추가된다.

~/workspace/boot-spring-boot/aws/
`-- .ebextensions
    `-- nginx
        `-- conf.d
            |-- elasticbeanstalk
            |   `-- 00_server.conf
            `-- boot-spring-boot.conf

conf.d 폴더에 있는 .conf 파일들은 기본 구성의 http 블럭에 포함되며, conf.d/elasticbeanstalk 폴더에 있는 파일의 내용은 http 블록 내에 server 블록에 포함된다.

엘라스틱 빈즈톡의 기본 nginx 구성을 완전히 덮어쓰기 위해서는 .ebextensions/nginx/nginx.conf 으로 구성을 추가하면 된다.

~/workspace/boot-spring-boot/aws/
`-- .ebextensions
    `-- nginx
        `-- nginx.conf

nginx 에서 파일업로드 사이즈를 10M로 올려보자.

  1. 프로젝트 내에 aws/.ebextensions/nginx/conf.d/elasticbeanstalk 폴더를 생성

  2. 00_server.conf 파일 생성

  3. 00_server.conf 내에 client_max_body_size 10M; 를 추가한다.

  4. 스프링 부트 패키징 시 .ebextensions/nginx/conf.d/elasticbeanstalk 폴더가 포함되어 배포되도록 만든다.

build.gradle 에 다음과 같은 태스크를 만들어 실행하면 손쉽게 가능해진다.

task awsEBZip(type: Zip, dependsOn: 'bootRepackage') {
from '../aws' // .ebextensions 위치
from 'build/libs/' + jar.archiveName bootRepackage 된 파일 위치
baseName = 'boot-spring-boot-' + jar.version // 재생성한 파일명
}



Thymeleaf 에서 스프링 환경변수 사용하기

Thymeleaf 에서 스프링 프로퍼티 값 사용하는 방법

  • @ 뒤에 빈(Bean) 이름을 사용하면 그 빈에 접근할 수 있다.

타임리프에서 스프링의 property 파일(application.property 혹은 application.yml 등)에 기술되어 있는 변수를 이용하려는 경우

${@environment.getProperty('property.key')}

프로파일 환경에 따라 표시를 하려면

<div th : if = $ { @environment .acceptsProfiles ( 'production' )}>
This is the production profile
</ div>
or
<div th : if = "$ {# arrays.contains (@ environment.getActiveProfiles () 'production')} " >
     This is the production profile
</ div>

시스템 환경변수를 이용하는 경우

$ { @systemProperties [ 'property.key' ]}



"Spring MVC 4 익히기" 라는 이름으로 번역한 책이 2016/08/25 에 출간되었습니다. ^^

종이책은 9월 중순 이후에 나올 예정입니다.


쉽게쉽게할 수 있을거라고 생각하고 시작했는데 쉽지 않았습니다. ㅎ.

처음 번역할 때보다는 절반이상 시간을 줄였습니다.


@_@); 이제 스프링부트 책을 한번 써보지 않겠냐는 제의가 주변에서 쏟아지고 있는데... 부담이 되네요.

써야지 하고 뼈대는 잡아놨는데...


다음 책의 이름은 "Boot Spring Boot" 입니다. 뒤집어 읽어도 "Boot Spring Boot!"


과연 완료할 수 있을지!!


+ Recent posts