![[Spring-Security] 인코딩(Encoding), 암호화(Encyrption), 해싱(Hashing), 그리고 Spring Security에 적용하기](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVXDrm%2FbtsGY6evFgX%2FMQNc8iA4thzDk5jeubIZsK%2Fimg.png)
인코딩, 암호화, 해싱은 모두 데이터 보안을 위한 기법이지만 각기 다른 목적과 방식을 갖습니다.
애플리케이션에서는 사용자의 비밀번호를 그대로 저장하는 것이 아닌 해싱을 거친 후에 저장합니다. 셋 다 데이터 보안을 위한 기법이라면 인코딩과 암호화를 사용하여 비밀번호를 저장해도 될 것 같은데 그러면 안 되는 이유에 대해서도 알아보겠습니다.
인코딩
인코딩의 목적은 데이터를 다른 형식으로 변환하여 표현하는 것입니다. 이는 데이터를 저장하거나 전송할 때 형식을 표준화하거나 압축하는 등의 용도로 사용됩니다. 즉, 누군가를 못 알아보게 하는 목적이 아닌 사용성을 위해 사용되는 방식입니다.
예를 들어, 이미지를 인코딩하면 데이터를 효율적으로 전송할 수 있습니다. 이미지의 사이즈가 크기 때문에 이를 인코딩을 하여 압축하면 네트워크 대역폭을 절약할 수 있습니다.
그렇다면 암호화를 위해 사용되지 않는 이유는 무엇일까요? 결론부터 말하자면 인코딩한 데이터를 디코딩하면 원본 데이터로 바꿀 수 있습니다.
사용자가 예를 들어 "내 비밀번호는 아무도 모를 거야!" 라고 비밀번호를 사용하고 있습니다. 이를 인코딩하면 "64K0IOu5hOuwgOuyiO2YuOuKlCDslYTrrLTrj4Qg66qo66W8IOqxsOyVvCE="로 변경됩니다.
인코딩 된 데이터를 그대로 디코딩하면 원본 데이터인 "내 비밀번호는 아무도 모를 거야!" 로 변환됩니다.
따라서 인코딩 방식으로 사용자의 비밀번호를 데이터베이스에 저장하면 안 됩니다.
암호화
암호화의 목적은 데이터를 안전하게 보호하기 위해 사용됩니다. 오직 특정한 키를 가진 사람만이 데이터를 해독할 수 있습니다. 여기서 특정한 키란 공개키/개인키를 의미합니다.
암호화에는 대칭 암호화 방식과 비대칭 암호화 방식 2가지가 존재합니다.
대칭 암호화 방식
이름에서도 알 수 있듯이 암호화와 복화에 사용되는 키가 동일합니다. 즉, 송신자가 데이터를 암호화할 때 사용한 키와 수신자가 데이터를 복화 할 때 사용하는 키가 동일해야합니다. 송신자와 수신자가 동일한 키를 사용하기 때문에 속도가 빠르다는 장점이 있습니다. 대표적인 예로는 AES와 DES가 있습니다.
비대칭 암호화 방식
비대칭 암호화 방식은 암호화에 사용되는 키와 복호화에 사용되는 키가 다릅니다. 대칭 암호화 방식과 달리 송신자와 수신자가 다른 키를 사용하게 됩니다. 누구나 사용이 가능한 공캐키와, 특정 사용자만이 가지고 있는 개인키가 존재합니다.
암호화/복호화 예시는 다음과 같습니다.
- 송신자
- 수신자의 공개키를 받아와서 데이터를 암호화할 때 사용한다.
- 암호화된 데이터를 수신자에게 전송한다.
- 수신자
- 자신의 개인키를 사용하여 송신자가 보낸 데이터를 복호화한다.
송신자는 데이터를 전송할 때 공개키를 사용하여 암호화하고, 수신자는 개인키를 사용하여 복호화를 합니다. 즉, 데이터를 전송받더라도 개인키가 존재하지 않는다면 복호화를 진행할 수 없습니다. 이는 제3자가 공개키를 알고 있더라도 이를 복호화하기 위한 개인키가 없으면 원본 데이터를 알 수 없다는 장점이 있습니다.
그러나 암호화 방식 또한 사용자의 비밀번호를 저장하기에는 적합하지 않습니다. 암호화에 사용된 키를 탈취할 수만 있다면 복화화를 할 수 있기 때문입니다.
해싱
해싱은 데이터의 무결성을 보호하기 위하여 사용됩니다. 비밀번호를 가장 안전하게 저장하는 방식으로도 알려져 있습니다. 해싱은 단방향 함수를 사용하여 비밀번호를 저장하기 때문에, 해싱된 값으로부터 원본 데이터를 복호화할 수 없습니다. 따라서 해싱된 값을 얻더라도 원본 데이터를 알 수 없습니다.
해싱의 종류는 매우 다양합니다.
- MD5
- SHA-1
- SHA-256
- Bcrypt
- Argon2
- ...
Bcrypt 방식을 사용하여 예시를 들어보겠습니다.
사용자가 "내 비밀번호는 아무도 모를 거야!" 라는 비밀번호를 사용하고자 할 때, Bcrypt 해싱 기법을 사용하여 "$2a$12$WzW0n7ljyhUNko5QgVoWjOk2Li2rdrb1CwM7oWo3PXNIWTOn7DpSa" 값을 얻었습니다.
해싱은 단방향 함수이기 때문에 위에서 구한 해싱값으로 원본데이터를 구할 수 없습니다. 또한 원본 데이터를 해싱을 수행할 때마다 다른 값을 반환하게 됩니다.
해싱을 수행할 때마다 다른 값을 반환하는데 어떻게 원본 데이터와 일치하는지 확인하지? 라는 의문이 들 수 있습니다. 사용자가 로그인을 할 때 어떠한 과정이 수행되는지 알아보겠습니다.
- 로그인을 시도할 때 입력한 비밀번호를 해시 함수를 사용하여 해시값을 생성한다.
- 생성된 해시값과 DB에 저장된 해시값을 비교하여 일치하는지 확인한다.
- 해시값이 일치하면 인증이 성공하고, 그렇지 않다면 인증이 실패한다.
위 방식을 통해 원본 데이터와 해시 값이 일치하는지 여부를 확인할 수 있습니다.
Spring Security 적용하기
BCryptPasswordEncoder
비밀번호를 안전하게 저장 및 비교하기 위해 해싱 알고리즘으로 BCrpyt 알고리즘을 사용합니다.
@Configuration
public class ProjectSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests((requests) -> {
requests.requestMatchers("/myAccount", "/myBalance", "/myLoans", "/myCards")
.authenticated()
.requestMatchers("/notices", "/contact", "/register").permitAll();
}).formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
/**
* NoOpPasswordEncoder() : 비밀번호를 일반 텍스트로 저장한다. 비밀번호 암호화 및 해싱을 수행하지 않기 때문에 prod 환경에서 사용 금지.
* BCrypt() : 해싱 알고리즘
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
// return NoOpPasswordEncoder.getInstance();
}
}
Controller
사용자가 회원가입을 할 때 입력한 password를 Bycrpt 해싱 기법을 사용하여 변경합니다.
@RestController
@RequiredArgsConstructor
public class LoginController {
private final CustomerRepository customerRepository;
private final PasswordEncoder passwordEncoder;
@PostMapping("/register")
public ResponseEntity<String> registerUser(@RequestBody Customer customer) {
String hashPwd = passwordEncoder.encode(customer.getPwd());
customer.setPwd(hashPwd);
customerRepository.save(customer);
return ResponseEntity.status(HttpStatus.CREATED)
.body("Given user details are successfully registered");
}
}
사용자가 입력한 비밀번호가 "내 비밀번호는 아무도 모를 거야!" 이지만, DB에 저장된 비밀번호는 해싱이 적용되었음을 알 수 있습니다.