우리가 만약 데이터베이스에다가 평문 그대로 leee@gmail.com / 12345678 => aaaabbbb
만약 해커한테 12345678 비밀번호가 털렸다고 하자. 굉장히 큰 문제가 된다. (비밀번호가 은행 계좌일 수도 있고, 주식거래 하는 번호일 수도 있고) 그래서 해싱해야 한다. 특정한 알고리즘을 따라서 (SHA , bcrypt 등등) 해싱한다. 스프링 시큐리티가 제공하는 PasswordEncoderFactories.createDelegatingPasswordEncoder() 사용시 기본적으로 bcrypt 알고리즘을 따른다.
해커가 12345678 => aaaabbbb 에서 aaaabbbb 를 보고 얘의 비밀번호가 12345678 이라는 것을 알아낼 수 있다. 그래서 디비에 저장을 할때 12345678 + salt => aaaabbbb, 비밀번호에다가 솔트값을 넣어 전혀 다른 값을 가지도록 한다. 해싱을 할때마다 랜덤한 값을 사용해도 동작한다.
AppConfig.java
package com.goodmoim.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration //빈등록(IoC관리)
public class AppConfig {
@Bean //스프링 컨테이너에서 객체를 관리할 수 있게 하는 것
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
@Bean 어노테이션과 @Component 어노테이션 둘다 Spring(IoC) container 에 Bean 을 등록하도록 하는 메타 데이터를 기입하는 어노테이션이다. 그렇다면 왜 두개나 만들어 놓았을까? 둘의 용도가 다르기 때문이다. Bean 어노테이션의 경우 개발자가 직접 제어가 불가능한 외부 라이브러리등을 Bean으로 만들려 할때 사용된다. 반면 Component 어노테이션의 경우 개발자가 직접 작성한 class를 Bean으로 등록하기 위한 어노테이션이다.
BCryptPasswordEncoder.java
public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength, SecureRandom random) {
this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
this.logger = LogFactory.getLog(this.getClass());
if (strength == -1 || strength >= 4 && strength <= 31) {
this.version = version;
this.strength = strength == -1 ? 10 : strength;
this.random = random;
} else {
throw new IllegalArgumentException("Bad strength");
}
}
-1이면 strength 10, 강도를 설정할 수 있다. bcrypt 는 다른 해싱 알고리즘에 비해서 의도적으로 시간이 더 걸린다. 해커들이 여러번 시도할 수 없도록 한 것임. 약간 느리다는 게 장점 중 하나임.
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else {
String salt = this.getSalt();
return BCrypt.hashpw(rawPassword.toString(), salt);
}
}
private String getSalt() {
return this.random != null ? BCrypt.gensalt(this.version.getVersion(), this.strength, this.random) : BCrypt.gensalt(this.version.getVersion(), this.strength);
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
} else {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}
패스워드를 encode 할때 getSalt 값을 받아와서 (랜덤한 값) hashpw 해싱을 진행한다. rawPassword 내가 입력한 1234578과 salt를 갖고 해싱한다. 실제로 매칭할 때 볼때는 rawPassword (로그인할 때 입력한 1234578), encodedPassword (db에 저장된 해싱 되어있는 값) 이 둘을 매칭되는지 볼때는
public static boolean checkpw(String plaintext, String hashed) {
return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
}
내가 입력한 plaintext (12345678) 값과 hashed 값으로 다시 한번 해싱을 한다. 이 값이 다시 hashed 과 일치하는지 확인한다. true 이면 값을 잘 입력한 것이다. 만약 12345789를 입력했을 때 이전에 12345678 + salt => aaa3333f 일 때 9를 추가 입력했으므로 다시 해시해도 같은 값이 나오지 않는다. 따라서 salt 값이 바뀌어도 상관이 없다.
@DisplayName("회원 가입 처리 - 입력값 정상")
@Test
void signUpSubmit_with_correct_input() throws Exception {
mockMvc.perform(post("/sign-up")
.param("nickname", "lee")
.param("email", "leeee@email.com")
.param("password", "12345678")
.with(csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/")); // 리턴하는 뷰 이름 검증
Account account = accountRepository.findByEmail("leeee@email.com");
assertNotNull(account);
assertNotEquals(account.getPassword(), "12345678");
//leeee@email.com 계정이 실제로 있는지
//assertTrue(accountRepository.existsByEmail("leeee@email.com"));
then(javaMailSender).should().send(any(SimpleMailMessage.class)); // 아무런 타입, sender가 호출되었는지 확인하는 것, 그럼 메일을 보내겠다고 확인할 수 있음
}
다음의 테스트는 진행이 된다. 즉 평문 그대로 저장을 안한다는 것이 입증된다. 패스워드 인코더를 적용해서 회원가입 할때 패스워드 인코더를 적용했기 때문에 더 이상 입력된 값 그대로 저장되지 않았다.
sts 내부의 terminal을 이용해서 git 에 push 를 했더니 test 폴더 밑의 파일이 반영이 안되더라.. 오늘 두번 커밋을 했는데, 커밋 단위가 제대로 반영되지 않아서 보고 놀랬따 😥😥 왜지? 윈도우..;; 맥은 되는 것 같은데 .. 추가 플러그인을 설치해야 하나. ... 그래서 git reset --hard HEAD 이 명령어를 사용해서 돌아가긴 했는데 .. 뭔가 이상하게 꼬여서 ㅎㅎ 다 날릴 뻔 하다가 그냥 원상복귀 했다. git은 어려워 🤧🤧🤧🤧
출처 : 인프런 백기선님의 스프링과 JPA 기반 웹 애플리케이션 개발
https://jaddong.tistory.com/entry/%EC%9B%90%EA%B2%A9%EC%A0%80%EC%9E%A5%EC%86%8C%EC%97%90-%EC%98%AC%EB%9D%BC%EA%B0%84-%EC%BB%A4%EB%B0%8B-%EB%90%98%EB%8F%8C%EB%A6%AC%EA%B8%B0