왜 사용하는지, 어떻게 돌아가는지는 하는 법 이후에 알아보도록 하자.
gradle 에 spring-security-crypto 추가
dependencies {
implementation 'org.springframework.security:spring-security-crypto:5.7.1'
}
maven을 쓴다면 아래처럼 추가
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>5.7.1</version>
</dependency>
PasswordEncoder Bean 등록
@Configuration
public class CommonConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
비밀번호 encoding 하는법 (passwordEncoder.encode)
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootTest
class PwdEncodeTest {
@Autowired
PasswordEncoder passwordEncoder; // DI
@Test
void pwdEnc() {
String pwd = "kedric123";
String encodedPwd = passwordEncoder.encode(pwd); //암호화 하는부분
System.out.println(encodedPwd);
}
}
결과
{bcrypt}$2a$10$xO99cg0RupsQY4PNvdPJe.neRL7JSplM8t/NQUgBRGnOM19/FbstS
PasswordEncoder를 di 받아서
passwordEncoder.encode(비밀번호) 를 해주면 알아서 암호화된다.
이렇게 암호화된 비밀번호를 사용자 정보를 저장할때 저장해주면 된다.
여기서 몇 번 테스트 해본 사람이라면 결과값이 항상 똑같지 않다는걸 알 수 있다.
그럼 어떻게 해당 사용자의 비밀 번호가 맞는지 아닌지 알 수 있을까?
기존 비밀번호와 비교하여 맞는지 확인하는 법 (passwordEncoder. matches)
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootTest
class PwdEncodeTest {
@Autowired
PasswordEncoder passwordEncoder; // DI
@Test
void pwdEnc() {
String pwd = "kedric123";
String encodedPwd = passwordEncoder.encode(pwd); //암호화 하는부분
System.out.println(encodedPwd);
}
@Test
void pwdMatch(){
// 기존 저장해두었던 암호화된 비밀번호
String encodedPwd = "{bcrypt}$2a$10$xO99cg0RupsQY4PNvdPJe.neRL7JSplM8t/NQUgBRGnOM19/FbstS";
// 검증할 비밀번호
String newPwd = "kedric123";
if(passwordEncoder.matches(newPwd, originPwd){
System.out.println("true");
}else{
System.out.println("false");
}
}
}
DB에 암호화된 비밀번호를 저장해두고
해당 사용자가 로그인 할 경우, 기존 암호화된 비밀번호와 입력한 비밀번호를 비교하여 결과를 얻을 수 있다.
passwordEncoder.matches(검증할 비밀번호, 기존 암호화된 비밀번호) 를 해주면
boolean 값으로
비밀번호가 일치하면 true
일치하지 않다면 false값을 return한다.
사용방법이라면 이렇게나 간단하다.
============================================================================================
그럼 이제
왜 이런 암호화를 쓰고 어떤식으로 암호화 되는지 알아보자.
우선 암호화는 단방향 암호화와 양방향 암호화가 있다.
간단히 말하자면
단방향(MD5, SHA 등)은 암호화(encoding)는 가능해도 복호화(decoding)는 안되는 것을 말하고,
양방향(AES,RSA,DES 등) 은 암호화 및 복호화가 가능한 것이다.
더 자세한 내용은 검색해서 알아보도록 하자.
두 방식을 놓고 보면
단방향으로 암호화된 경우는 복호화가 불가능하기 때문에 암호를 알아내기 어려울 것처럼 보인다.
하지만 여기서 우리가 Spring 이 제공하는 PasswordEncoder를 쓰는 이유가 나온다.
위 암호화 예시에서 언급했지만
PasswordEncoder 를 이용하여 encode하는 경우 암호화된 결과값이 항상 다르게 나온다.
기존 단방향 암호화의 경우,
암호를 알아내려는 공격자가 rainbow table을 이용하여 rainbow attack을 하는경우, 결국 비밀번호를 알아낼 수 있다.
간단히 말하면
kedric1234를 암호화 하는데, 항상 똑같이 암호화된 string이 asdqwe123이라고 한다면
공격자는 암호를 알수 없지만 반대로 asdqwe123가 kedric1234라는 것은 알 수 있다는 것이다.
이런 데이터 목록들을 rainbow table이라고 한다.
그럼 PasswordEncoder는 뭐가 다른가?
public interface PasswordEncoder {
/**
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
* greater hash combined with an 8-byte or greater randomly generated salt.
*/
String encode(CharSequence rawPassword);
/**
* Verify the encoded password obtained from storage matches the submitted raw
* password after it too is encoded. Returns true if the passwords match, false if
* they do not. The stored password itself is never decoded.
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
/**
* Returns true if the encoded password should be encoded again for better security,
* else false. The default implementation always returns false.
* @param encodedPassword the encoded password to check
* @return true if the encoded password should be encoded again for better security,
* else false.
*/
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
encode 부분을 보면
단순히 해시는 하는것 뿐만 아니라, salt(바이트 단위의 임의의 문자열) 를 추가하기 때문에
입력값이 다르더라도 매번 다른 encode값이 나오게 된다.
처음에 우리가 등록했던 bean을 보면
PasswordEncoderFactories.createDelegatingPasswordEncoder();
위와 같은 부분이 있다.
PasswordEncoder가 제공하는 방식으로 encode 하겠다는 것이다.
이 부분을 살펴보면
PasswordEncoder는 기본적으로 bcrypt 방식을 추천한다.
물론 다른방식으로도 암호화는 가능하다.
필자는 그대로 bcrypt방식을 사용했다.
PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
마치며
이렇게 Spring 에서 제공하는 PasswordEncoder 로 암호화하는 방식과
왜 이러한 방식을 써야하는지를 알아보았다.
여기서 bcrypt 외에 방식이라거나 key stretching, 속도 등 알아야 할 것들이 여전히 꽤나 많다.
늘 공부하면서도 느끼는 것이지만 개발 공부는 끝이 없는 것 같다.
하나를 알았다고 생각되면 늘 새로운 것이 있는 것 같다.
이런 하나하나가 조금씩 쌓여 내 지식이 되길 바란다.
**참조
https://d2.naver.com/helloworld/318732
https://gompangs.tistory.com/entry/Spring-Password-Encoder
https://mvnrepository.com/artifact/org.springframework.security/spring-security-crypto/5.7.1
'개발자의 삶 > Spring' 카테고리의 다른 글
[Spring] Message (feat, 다국어 처리) (0) | 2022.02.16 |
---|---|
[Spring boot] 프로젝트 생성 및 Hello world (feat, spring security) (0) | 2021.02.25 |