개발/Team Project

ep.09 비밀번호 암호화 (Bcrypt)

김현중 (keemhing) 2024. 8. 13. 14:06

보안은 언제든 뚫릴 수 있기에 항상 대비를 해야 한다. 개인정보를 이용해 사기 범죄를 일삼는 조직도 있고, 보이스피싱, 스미싱 등 많은 범죄가 개인정보를 이용한 것이기에 항상 대비해야 한다. 이번 작업을 통해 보안에 대해 더 공부하게 되어 좋았고 백엔드 개발자로서 항상 경각심을 가지고 작업을 해야겠다는 생각이 들었다. 이번 포스팅에서는 회원가입 시 유저가 입력한 비밀번호를 암호화하는 작업을 다뤄보겠다.


암호화

암호화란 평문을 암호문으로 바꾸는 행위이다. 반대로 암호문을 평문으로 바꾸는 행위는 복호화라고 한다.
암호화는 아주 기본적인 정보 보안 방법으로, 데이터가 유출되는 것 자체를 막지는 못하지만 데이터가 어떤 정보를 담고 있는지 모르게하는데에 의미가 있다. 사용자의 비밀번호와 같은 중요한 정보는 다른 사람이 알지 못하게 하기 위해 암호화 과정이 필요하다. 

 

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