Spring Security | 인증 | BCryptPasswordEncoder : 암호 해시


왜 해시해야 하는가?

만일 데이터가 누설된 경우, 패스워드가 그대로 보존되어 있다면 매우 위험하다.

따라서 일반적으로 암호는 원래의 문자열을 식별할 수없는 형태로 데이터베이스 등에 저장하여 둔다. 또한 암호 해독이 불필요한 경우는 해시 함수를 사용하여 암호를 변환한다.

BCrypt

해시 함수라고 하면 MD5나 SHA 같은 것이 있지만, 암호를 해시시키는 경우 BCrypt라는 것을 사용하면 편리하다.

단순하게 해시 함수를 1회만 적용했다면, 무차별 랜덤으로 대입해 보는 공격 및 사전 공격, 레인보우 테이블 등의 암호 공격에 취약하게 된다.

BCrypt는 단순히 입력을 1회 해시하는 것이 아니라 ,랜덤의 소트(salt)을 부여하여 여러번 해시를 적용하여 원래의 암호를 추측하기 어럽게 한다.

Spring Security에도 이 BCrypt을 이용하는 것을 권장하고 있다.

구현

암호 해시는 PasswordEncoder을 DaoAuthenticationProvider로 설정함으로써 구현할 수 있다. BCrypt를 사용하는 경우에는 BCryptPasswordEncoder라는 PasswordEncoder의 구현을 사용한다.

또한, 해시 값은 BCrypt 해시 값을 계산하여 미리 계산 해놓은 것을 기재한다.

XML Configuration

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>

    ...

    <bean id="passwordEncoder"
          class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <sec:user
                    name="hoge"
                    password="$2a$08$CekzJRYhb5bzp5mx/eZmX.grG92fRXo267QVVyRs0IE.V.zeCIw8S"
                    authorities="ROLE_USER" />
            </sec:user-service>

            <sec:password-encoder ref="passwordEncoder" />
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>
  • BCryptPasswordEncoder를 Bean으로 정의하고, <password-encoder>의 ref 속성에 지정한다.
  • <password-encoder><authentication-provider>의 자식 요소로 설정한다.

Java Configuration

MySpringSecurityConfig.java

package sample.spring.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    ...

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    public void configure(AuthenticationManagerBuilder auth, PasswordEncoder passwordEncoder) throws Exception {
        auth.inMemoryAuthentication()
            .passwordEncoder(passwordEncoder)
            .withUser("hoge")
            .password("$2a$08$CekzJRYhb5bzp5mx/eZmX.grG92fRXo267QVVyRs0IE.V.zeCIw8S")
            .roles("USER");
    }
}
  • AbstractDaoAuthenticationConfigurer의 passwordEncoder() 메소드에 PasswordEncoder를 지정한다.
  • AbstractDaoAuthenticationConfigurer는 AuthenticationManagerBuilder의 inMemoryAuthentication() 메소드의 반환 값의 형태로 InMemoryUserDetailsManagerConfigurer 부모 클래스이다.

비밀번호 인코딩

임의의 비밀번호를 암호화할 때 PasswordEncoder을 컨테이너에서 가져와 encode() 메소드를 사용하면 된다.

EncodePasswordServlet.java

package sample.spring.security.servlet;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/encode-password")
public class EncodePasswordServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(req.getServletContext());
        PasswordEncoder passwordEncoder = context.getBean(PasswordEncoder.class);

        String password = req.getParameter("password");
        String encode = passwordEncoder.encode(password);
        System.out.println(encode);
    }
}
  • 설정은 앞에서 설명한 것과 같다(BCryptPasswordEncoder를 Bean으로 등록하고 있다).
  • 동작 확인을 위해 간단히 구현한 컨테이너에서 명시적으로 PasswordEncoder을 얻고 있지만, 실제로는 컨테이너 관리의 Bean에 PasswordEncoder을 DI하여 참조를 얻게 된다.
  • 구현 후에 애 URL에 접속해 본다.

서버 콘솔 출력

$2a$10$2qVDkAqxp8eDrxR8Be2ZpubYGOCVZ7Qy9uK/XzOIY1ZoxpChtrWDK