ep.09 비밀번호 암호화 (Bcrypt)
보안은 언제든 뚫릴 수 있기에 항상 대비를 해야 한다. 개인정보를 이용해 사기 범죄를 일삼는 조직도 있고, 보이스피싱, 스미싱 등 많은 범죄가 개인정보를 이용한 것이기에 항상 대비해야 한다. 이번 작업을 통해 보안에 대해 더 공부하게 되어 좋았고 백엔드 개발자로서 항상 경각심을 가지고 작업을 해야겠다는 생각이 들었다. 이번 포스팅에서는 회원가입 시 유저가 입력한 비밀번호를 암호화하는 작업을 다뤄보겠다.
암호화
암호화란 평문을 암호문으로 바꾸는 행위이다. 반대로 암호문을 평문으로 바꾸는 행위는 복호화라고 한다.
암호화는 아주 기본적인 정보 보안 방법으로, 데이터가 유출되는 것 자체를 막지는 못하지만 데이터가 어떤 정보를 담고 있는지 모르게하는데에 의미가 있다. 사용자의 비밀번호와 같은 중요한 정보는 다른 사람이 알지 못하게 하기 위해 암호화 과정이 필요하다.
1. 암호화의 필요성
ㅁ 유저의 비밀번호는 절대 그대로 DB에 저장하지 않는다.
-> 데이터베이스가 해킹당하면 유저의 비밀번호가 그대로 노출된다.
-> 외부 해킹이 아니더라도 내부 개발자나 인력이 유저들의 비밀번호를 볼 수 있다.
ㅁ 암호화를 적용한다면 해킹을 당할지언정 평문의 비밀번호가 외부로 노출되는것을 막을 수 있고, 내부 인력이 비밀번호를 알 수 없다.
2. 암호화의 종류
2-1. 단방향 암호화 (Hash, Mac 방식)
- 암호화는 할 수 있어도, 복호화 하여 원래의 비밀번호를 알 수 없다. 말 그대로 한번 암호화하면 복호화가 불가능하다.
- Hash 알고리즘은 임의의 문자열을 고정된 길이의 다른 문자열로 변환시킨다.
- Mac(Message Authentication Code) 알고리즘은 임의 길이의 메시지와 송수신자가 공유하는 키를 기반으로 고정 길이의 출력값을 계산한다.
2-2. 양방향 암호화 (대칭키, 공개키 암호화 방식)
암호화된 비밀번호를 다시 복호화하여 평문의 비밀번호를 알 수 있다.
- 대칭키 암호화 방식은 암호화, 복호화에 대한 키가 동일하며 해당 키를 아는 사람만이 문서를 복호화 할 수 있게 한다. 공개키 암호화 방식에 비해 속도가 빠른 것이 장점이지만 키를 교환하는 문제, 키를 교환하는 중에 탈취될 수 있는 문제, 관리해야 하는 키가 방대해진다는 단점이 있다.
- 공개키 암호화 방식은모든 사람이 접근 가능한 키이고 개인키는 각 사용자만이 가지고 있는 키이다. 공개되어 있기 때문에 키를 교환할 필요가 없고 개인키를 가지고 있는 수신자만이 암호화된 데이터를 복호화할 수 있으므로 일종의 인증기능도 제공한다. 대칭키 암호화 방식에 비해 속도가 느리다는 것이 단점이다.
이 외에 salting, Key Stretching등 여러 보완법이 있는데 이는 이 포스팅을 참고하기 바란다.
Bcrypt를 이용한 암호화
Bcrypt 알고리즘은 복호화가 불가능한 단방향 알고리즘이다. 현재 사용되고 있는 Hash 알고리즘 중 가장 강력한 암호화 방식으로 널리 사용되며 Spring Security에서도 채택하여 사용한다. 암호화의 반복 횟수를 설정할 수 있어서 처리되는 시간을 임의로 늘려 무차별적인 대입을 통한 해킹 방식을 방어할 수 있다. 이제 암호화를 진행해 보자.
1. 의존성 주입 (build.gradle.kts)
implementation("org.springframework.boot:spring-boot-starter-security")
2. Spring Security 설정
2-1. Security User 설정
> .env (큰따옴표 안에 값을 입력해 준다)
SECURITY_USER=""
SECURITY_PASSWORD=""
> application.properties
# SpringSecurity
spring.security.user.name=${SECURITY_USER}
spring.security.user.password=${SECURITY_PASSWORD}
SpringApplication을 실행시키고 localhos:8080에 접속하였을 때 보이는 로그인창에 user.name, user.password를 각각 입력하면 로그인이 된다.
2-2. SecurityConfig 파일 생성
일단 초기 설정이기에 모든 페이지에 대한 접근 권한을 설정하고 csrf(사이트 위변조 방지)를 해제하였다. 로그인 기능을 구현하면서 각 페이지 별 접근 권한을 설정할 예정이다. 이와 같이 설정해 주면 로그인 절차 없이 바로 localhos:8080에 접속된다.
package kr.co.vibevillage.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig{
// springframework.security에서 제공하는 패스워드인코더 인터페이스를 이용하여 객체 생성 후 Bean 등록
// 회원가입 처리되는 비즈니스 로직에서 이 객체를 이용하여 비밀번호 암호화 진행
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 모든 페이지에 대한 접근권한 설정, 사이트 위변조 방지 해제
http.authorizeHttpRequests(auth -> auth
.requestMatchers(new AntPathRequestMatcher("/**"))
.permitAll())
.csrf((csrf) -> csrf.disable());
return http.build();
}
}
2-3. RegisterServiceImpl 파일 수정
유저가 입력한 비밀번호를 passwordEncoder 객체를 이용하여 비밀번호 암호화를 진행하고 userDTO객체에 초기화한다. 일단 암호화가 잘 되는지 테스트하기 위해 회원가입에 실패했을 경우에 대한 코딩은 콘솔에 문자열을 띄우는 정도로 작업했다.
package kr.co.vibevillage.user.model.service;
import kr.co.vibevillage.user.model.dto.UserDTO;
import kr.co.vibevillage.user.model.mapper.RegisterMapper;
import kr.co.vibevillage.user.model.util.CertificationUtil;
import kr.co.vibevillage.user.redis.dao.RedisRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor // 초기화 되지 않은 final 필드나 @NonNull이 붙은 필드에 대한 생성자를 만들어줌
public class RegisterServiceImpl implements RegisterService{
private final RegisterMapper registerMapper;
// 비밀번호 암호화를 위한 passwordEncoder 객체 생성
private final PasswordEncoder passwordEncoder;
...(생략)
@Override
public int register(UserDTO userDTO) {
// 유저가 입력한 비밀번호를 passwordEncoder객체를 이용하여 암호화를 진행한다.
String bcryptPassword = passwordEncoder.encode(userDTO.getUserPassword());
// 암호화된 비밀번호를 userDTO갹채에 초기화한다.
userDTO.setUserPassword(bcryptPassword);
// 회원가입 진행
int result = registerMapper.register(userDTO);
if (result == 1) {
// 회원가입이 오류없이 진행되면 회원 등급을 생성
int result2 = registerMapper.registerLevel(userDTO.getUserNo());
return result2;
} else {
System.out.println("회원가입 에러발생");
return 0;
}
}
...(생략)
}
이와 같이 설정하고 회원가입을 진행해 보면... 데이터 베이스에 저장된 암호는 평문이 아니라 암호화된 패스워드인 것을 확인할 수 있다.
이렇게 비밀번호 암호화를 마쳤다. 이제 로그인 기능을 구현할 건데 기본적인 로그인 기능 구현부터 SpringSecurity, JWT 적용까지 하나씩 진행해 보겠다.
> 참고 자료 <
1. 제 컴에서는 되는데요?
2. JIN_CODER
3. hxyxneee.log