본문 바로가기

프로그래밍/Spring Security

Spring Security에서의 비밀번호를 암호화시켜 적용해보자

지난 글에서는 Spring Security가 로그인 작업을 성공한 후 또는 로그인 작업을 실패한 후의 부가작업 설정하는 부분에 대해 설명했다. 이번 글에서는 암호화된 패스워드를 Spring Security에서 사용하는 방법에 대해 알아보도록 하겠다.

 

지금까지 로그인 테스트를 진행했다면 아마 이 부분에 대해 의아심을 가졌던 분들이 많을 것이다. 흔히 로그인 하는 과정을 보면 암호화된 비밀번호 값을 DB에 저장한뒤 사용자가 입력한 암호화 되지 않은 비밀번호를 지정된 암호화 방식으로 비교해서 확인하거나 또는 DB에 저장되어 있는 암호화된 비밀번호 값을 복호화한뒤에 사용자가 입력한 패스워드와 비교하는 식으로 아이디와 패스워드 인증을 거칠텐데, 지금까지 Spring Security를 설명하면서 이런 내용에 대해 언급한 것이 전혀 없기 때문이다. 이번글은 이런 부분에 대한 설명을 할려고 한다.

 

데이터를 암호화하는데는 여러가지 암호화 방식이 존재한다. 그러나 이러한 방식이 모두 안전하다고 말하는데는 무리가 있다. md5 방식만 봐도 md5로 암호화 된 값은 복호화 할 수 없다고 얘기했었지만 이젠 md5도 복호화가 되기 때문에 이 방식을 선택할수도 없게 되었다. 그만큼 어떤 암호화 방식을 선택해야 할지는 정말 신중한 고민을 해야 할 필요성이 있다. Spring Security는 plaintext, sha, sha256, md4, md5 이렇게 5가지 방식을 제공해주고 있다. 그러나 Spring Security는 장기적인 측면에서 기존의 암호화 방식을 그대로 사용하지 않을 것으로 보인다.(개인적인 의견이므로 정확하지 않음을 미리 밝혀둔다) 이렇게 얘기하는 이유를 설명할려면 PasswordEncoder 인터페이스에 대한 설명을 먼저 해야 하지 싶다.

 

Spring Security는 Password의 암호화와 비교 작업을 하기 위한 인터페이스를 제공하는데 그것이 org.springframework.security.authentication.encoding.PasswordEncoder 인터페이스 이다. 이 인터페이스에는 2개의 메소드가 있는데 다음과 같다

 

String encodePassword(String rawPass, Object salt);

boolean isPasswordValid(String encPass, String rawPass, Object salt);

 

encodePassword는 입력된 문자열(rawPass)를 암호화 작업을 거쳐 결과물인 암호화 된 값을 return 하고, isPasswordValid는 암호화 된 문자열과 암호화 되지 않은 문자열을 입력받아 서로 match가 되는지 그 결과를 return 하게 된다. 각각의 메소드 모두 salt라고 하는 파라미터가 들어간다. 문자열을 암호화 할 때 특정 데이터를 같이 포함시켜서 암호화 시킬수 있다. 이렇게 하는 이유는 사람들에게 단어를 무작위로 입력해서 패스워드가 통과되는 것을 방지하기 위함이다. 예를 들어 비밀번호를 사람들이 누구나 아는 단어인 "mother"라고 했다고 가정하자. 로그인 해킹 프로그램에서 사람들에게 친숙한 단어 사전을 이용해서 해킹을 시도할때 mother가 들어가면 뚫려버리게 되는 것이다. 이런 이유로 mother 같은 친숙한 단어를 사용해도 특정 데이터(예를 들면 가입자의 생년월일)를 섞어서 암호화 된 문자열을 만들면 로그인 해킹 프로그램에서 mother로 넣어도 특정 데이터를 모르면 해킹할 수가 없게 된다. 바로 이런 특정 데이터를 salt 라고 하는 것이다. salt를 넣어서 encodePassword 메소드를 실행했다면 isPasswordValid 메소드에서도 encodePassword 메소드 실행시 사용했던 salt를 넣어야 올바른 비교를 할 수 있을 것이다. 

 

그러나 Spring Security 3.1로 넘어와서는 salt 조차 노출이 되면 해킹이 될 수 있어서 그런지 이 PasswordEncoder 인터페이스를 deprecated 시켰다. 이 salt가 되는 데이터는 흔히 회원과 연관되는 데이터(ex : 회원 생년월일이나 회원 이름 등)을 설정하게 되는데 회원 정보가 외부로 노출이 되면 salt 되는 데이터도 같이 노출이 되기 때문에 해킹이 될 소지는 분명 있다. 그런 이유에서인지는 모르겠으나 Spring Security는 3.1로 넘어와서는 기존의 PasswordEncoder를 deprecated 시키고 새로이 org.springframework.security.crypto.password.PasswordEncoder 인터페이스를 다시 재정의했다. 이 인터페이스 또한 2개의 메소드가 있다.

 

String encode(CharSequence rawPass);

boolean matches(CharSequence rawPassword, String encodedPassword);

 

기존의 org.springframework.security.authentication.encoding.PasswordEncoder와 혼선이 없게끔 하기 위해 메소드 명도 바뀌고(encodePassword->encode, isPasswordValid->matches), 메소드의 파라미터가 들어가는 순서도 바뀌었다(isPasswordValid 메소드의 경우 첫번째 파라미터로 암호화 된 문자열, 두번째 파라미터로 암호화 되지 않은 문자열이 들어가지만 matches 메소드에서는 첫번째 파라미터로 암호화 되지 않은 문자열, 두번째 파라미터로 암호화 된 문자열이 들어간다. 그러나 여기에서는 이런 차이점 말고 한 가지 중대한 차이점이 존재한다. 바로 위에서 설명한 salt 역할을 하는 파라미터가 빠졌다는 것이다. 그럼 이 기능이 이제는 없는건가? 아니다. 이 salt를 외부에서 입력받는게 아니라 암호화 하는 시점에 랜덤하게 salt 역할을 하는 데이터를 만들어서 salt 기능을 수행하도록 바뀌었다. 이런 이유로 기존 Spring Security 사용자들중 org.springframework.security.authentication.encoding.PasswordEncoder 인터페이스를 사용하여 Password Encoder를 사용했다면 이제는 org.springframework.security.crypto.password.PasswordEncoder 인터페이스를 사용하는 방법으로 갈아타는 것이 좋다고 본다. 그리고 앞으로 PasswordEncoder 인터페이스를 얘기할 때는 org.springframework.security.crypto.password.PasswordEncoder 인터페이스를 이야기 하는 것으로 하겠다. 

 

그럼 이렇게 PasswordEncoder 인터페이스를 구현한 클래스는 무엇이 있는가? Spring Security는 이와 관련하여 3가지 클래스를 제공한다

 

● org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder

● org.springframework.security.crypto.password.NoOpPasswordEncoder

● org.springframework.security.crypto.password.StandardPasswordEncoder

 

NoOpPasswordEncoder 클래스는 암호화 기능을 수행하지 않는 암호화 클래스이다. 무슨 얘기냐면 암호화 시키기 위해 encode 메소드에 암호화 할 문자열을 넣어도 실제로는 암호화 기능을 수행하지 않고 입력받은 암호화 할 문자열을 그대로 return 해준다. 이 클래스는 암호화 기능을 수행하는지에 대한 테스트용으로 만들어진 클래스이니 실제 프로젝트에서는 테스트 용도가 아닌 용도로는 사용하면 안된다.

 

실제 암호화 작업까지 수행하는 클래스는 BCryptPasswordEncoder 클래스와 StandardPasswordEncoder 클래스이다. BCryptPasswordEncoder 클래스는 내부적으로 org.springframework.security.crypto.bcrypt.BCrypt 클래스를 이용하는데 이 BCrypt 클래스가 

bcrypt 해시 알고리즘을 이용하여 입력받은 데이터를 암호화하는 작업을 수행한다. StandardPasswordEncoder는 sha 해시 알고리즘을 이용하여 입력받은 데이터를 암호화한다(StandardPasswordEncoder 클래스 이용시 별도 설정이 없으면 sha-256으로 암호화한다). 또한 두 클래스 모두 내부적으로 salt 데이터를 랜덤하게 생성하여 적용하는 기능을 가지고 있다. Spring Security 측에서는 신규로 개발하는 시스템이라면 BCryptPasswordEncoder 클래스를 사용하는 bcrypt 해시 알고리즘 사용을 권장하고 있고, 기존 sha 해시 알고리즘을 적용한 상황이라면 StandardPasswordEncoder 사용을 권장하고 있다. 이 글에서는 신규니까 BCryptPasswordEncoder 클래스를 사용하는 것으로 패스워드 암호화를 하도록 하겠다.

 

Spring Security 에서 암호화 된 패스워드로 인증할 때 다음과 같은 설정을 한다. 

 

<authentication-manager>
	<authentication-provider user-service-ref="customJdbcDaoImpl">
		<password-encoder hash="bcrypt" />
	</authentication-provider>
</authentication-manager>

 

<authentication-provider> 태그에 하위 태그로 <password-encoder> 태그를 만들어서 설정하는 방법으로 한다. hash에는 어떤 암호화 알고리즘을 사용할 것인지를 지정하는 것으로 여기에 들어가는 값은 {sha}, {ssha}, bcrypt, md4, md5, plaintext, sha-256, sha 중 하나를 지정할 수 있다. 또한 base64 속성을 사용하면 암호화된 문자열이 base64 인코딩 과정까지 거쳐두었는지를 지정할 수 있다. 미리 얘기해두지만 {sha}, {ssha}가 무엇을 의미하는지는 이 글을 작성하는 시점에서는 모르겠다. Spring Security 레퍼런스 문서에서도 값에 대한 의미를 분명히 밝혀두질 않아서..ㅠㅠ..또한 sha-256과 sha의 차이도 모르겠다.

 

그러나 개인적으로 이런 설정 방식을 권장하지는 않는다. 권장하지 않는데는 이유가 있다. 위에서 언급했던 PasswordEncoder 인터페이스를 보면 두 가지 기능을 제공하는데 하나는 기존 문자열을 암호화된 문자열로 바꾸는 기능암호화 된 문자열과 원래 문자열을 입력받아 일치하는지 비교하는 기능이 그것이다. 암호화 된 문자열과 원래 문자열을 입력받아 일치하는지 비교하는 기능은 로그인 때 사용하는 것이므로 반드시 필요하다. 그러나 이 기능 못지 않게 기존 문자열을 암호화된 문자열로 바꾸는 기능 또한 필요한 시점이 있다. 어느 때 일것이라고 생각하는가? 로그인 페이지가 있다는건 어떤 형식으로든 회원 정보를 관리한다는것이고 그럴 경우 회원 정보에 대한 조회, 등록, 수정, 삭제가 있다는 것이다. 회원 정보를 등록하거나 수정할때 사용자가 자신이 사용해야 할 패스워드 문자열을 입력하면 이를 암호화 시켜서 DB에 보관해야 할 것이다. 바로 패스워드 문자열을 암호화 시킬때 이 기존 문자열을 암호화된 문자열로 바꾸는 기능을 사용하게 되는 것이다. 그럼 위와 같이 설정하면 그 기능을 써 먹을수 없는 것인가? 현재로썬 그렇다. <authentication-provider> 태그가 설정되면 Spring Security는 내부적으로 org.springframework.security.authentication.dao.DaoAuthenticationProvider 클래스가 bean으로 등록이 되는데 이 bean에 있는 property 중 userDetailService 에는 우리가 이전 글에서 만든 로그인 시 DB에서 사용자 조회하는 CustomJdbcDaoImpl 클래스가 설정이 되고 또 다른 property인 passwordEncoder에는 우리가 <password-encoder> 태그의 hash 속성에서 설정한 암호화 알고리즘에 따른 PasswordEncoder 인터페이스를 구현한 클래스가 셋팅이 된다. 위에서 설정한 예를 따르자면 bcrypt로 설정했기 때문에 위의 분홍색 글박스에 언급했던 BCryptPasswordEncoder 클래스가 설정이 된다. 문제는 이 설정 과정에서 BCryptPasswordEncoder를 DaoAuthenticationProvider 클래스 바깥에서 bean을 생성시키도록 설정한뒤 참조를 걸은것이아니라 DaoAuthenticationProvider 클래스의 passwordEncoder 프로퍼티 내부에 BCryptPasswordEncoder 클래스 객체를 만들어서 설정한 것이다. XML로 예를 들자면 <bean> 태그 설정시 <property> 태그의 하위 태그로 <bean> 태그를 설정했다고 보면 된다. 이렇게 할 경우 이 클래스를 외부의 다른 bean 클래스에서 참조할 방법이 없다. 위에서 들은 상황과 같이 회원 정보를 등록하거나 수정하는데 사용하는 bean 클래스에서 BCryptPasswordEncoder 클래스를 Injection 받아서 사용자가 입력한 문자열을 암호화 한 문자열로 바꿔주는 메소드를 사용할 방법이 없게 된다. 그래서 외부에서 PasswordEncoder 인터페이스를 구현한 클래스를 bean으로 등록한 뒤 <password-encoder> 태그에 등록된 bean을 참조로 걸어주는 방식으로 진행하는 것이 여러모로 좋다. 다음과 같이 말이다.

 

<beans:bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />	

<authentication-manager>
	<authentication-provider user-service-ref="customJdbcDaoImpl">
		<password-encoder ref="bcryptPasswordEncoder" />
	</authentication-provider>
</authentication-manager>

 

BCryptPasswordEncoder 클래스를 등록하는 <bean> 태그의 위치는 <authentication-manager> 태그보다 먼저 선언되어야 하는 그런건 없다(<bean> 태그의 위치는 중요하지 않다는 얘기) 위의 설정을 보면 <bean> 태그를 이용해 BCryptPasswordEncoder 클래스를 등록한 뒤 이를 <password-encoder> 태그에 ref 속성을 이용해서 참조를 걸어두었다. 이렇게 설정해두면 로그인 할 때 사용자가 입력한 문자열을 암호화 시켜서 DB에 등록되어 있는 암호화 된 문자열과 비교하는 기능 뿐만 아니라 다음의 코드와 같이 사용자가 입력한 문자열을 암호화 된 문자열로 바꾸는 기능도 사용할 수 있게 된다. 다음의 코드는 사용자가 입력한 문자열을 입력받아 이를 암호화 된 문자열로 화면에 내려주는 Controller 클래스 소스 코드의 일부이다.

 

@Autowired
BCryptPasswordEncoder passwordEncoder;

@RequestMapping(value="passwordEncoder", method={RequestMethod.GET, RequestMethod.POST})
public String passwordEncoder(@RequestParam(value="targetStr", required=false, defaultValue="") String targetStr, Model model){
	if(StringUtils.hasText(targetStr)){
		// 암호화 작업
		String bCryptString = passwordEncoder.encode(targetStr);
		model.addAttribute("targetStr", targetStr);
		model.addAttribute("bCryptString", bCryptString);
	}
	return "/common/showBCryptString";
}

 

<bean> 태그로 등록된 BCryptPasswordEncoder 클래스를 @Autowired를 이용해 Injection 받은 뒤에 이를 Controller의 passwordEncoder 메소드에서 사용하고 있다. passwordEncoder 메소드에서는 <input> 태그의 name이 targetStr인 입력값을 받아서 이 값을 암호화 한 뒤 다시 화면에 내려보내주고 있다. 테스트 데이터를 만들때 특정 문자열을 입력하여 이 문자열의 암호화 된 값을 얻은뒤 DB에 입력할때 사용하는 용도로 이 passwordEncoder 메소드를 제작해봤다.

 

또한 위의 xml 설정 같이 <password-encoder> 태그의 ref를 이용해 참조를 걸어주는 방식으로 할 경우 Spring Security가 지원하지 않는 암호화 방식에 대한 구현도 가능하다. 예를 들어 Spring Security에 KISA에서 만든 SEED 알고리즘을 이용해서 암호화 작업을 하겠다고 가정해보자. SEED 알고리즘을 구현한 자바 코드는 구글에서 검색하면 나오니 별도로 언급하진 않겠다. PasswordEncoder 인터페이스를 구현한 클래스를 하나 만든뒤에 encode 메소드는 SEED 알고리즘을 이용하여 암호화된 문자열을 return 하도록 구현하면 되고, matches 메소드는 SEED로 암호화 된 문자열과 암호화 되지 않은 문자열을 입력받아 암호화 되지 않은 문자열을 SEED 알고리즘으로 암호화 된 문자열을 만든 뒤에 파라미터로 받은 SEED로 암호화 된 문자열과 equals 메소드로 비교한 결과값을 return 하면 된다(개인적으로 SEED 알고리즘을 적용한 PasswordEncoder 클래스를 테스트 삼아 만들어 본적이 있고 테스트도 성공했었다) 하지만 random salt 적용이 안되었기 때문에 이 부분에 대해서는 기존에 구현된 클래스인 BCryptPasswordEncoder 클래스와 StandardPasswordEncoder 클래스의 소스를 참조해서 random salt 적용을 해야 할 것이다.

 

지금까지 로그인 시 암호화 된 패스워드를 사용하는 방법을 설명했다. 이 글에서는 별도로 로그인 화면을 보여주질 않았다. 이전 글에서 늘 사용했던 로그인 화면 코드 그대로 이용하면 된다. 다만 이렇게 설정한 뒤 MEMBERINFO 테이블의 PASSWORD 컬럼의 값을 암호화 된 문자열로 바꿔야 할 것이다. 그 작업을 하기 위해 위의 Controller 메소드를 이용해서 원래 문자열을 입력하면 화면에 원래 문자열과 암호화된 문자열을 같이 보여주는 화면을 만들었던 것이다. 이번 글을 끝으로 Spring Security에서 인증, 즉 로그인과 관련된 글은 마무리 하도록 하겠다. 다음에는 권한과 관련된 내용으로 시작하도록 하겠다.