패스워드 암호화 구현 정리(Bcrypt 암호화, Hash)

devdo·2022년 11월 25일
0

SpringBoot

목록 보기
24/34
post-thumbnail

사실 비밀번호 암호화는 인증과 깊이 관련이 있다.
아래와 같은 사진으로 회원가입, 로그인 등 인증처리가 필요한 view가 있는데 이런 요청에서 비밀번호는 필수이고 db에 들어가기 때문에 사용자의 신용정보로서 암호화가 필수이다.

암호화 정리

평문을 알아볼 수 없게 바꿔 놓음
=> 단방향, 양방향의 개념이 있다.

  • 단방향 - hash 방식, 단뱡향은 복호화가 안된다는 뜻!
  • 양방향 - 복호화 가능 (대칭키, 공개키 방식)

✅ 패스워드 암호화 요구조건

  • 복호화 불가능
  • 해시함수 사용 (단방향)
  • 특정 패스워드의 해시값이 노출되어도, 같은 패스워드인 다른 계정도 탈취당하면
    안된다.
  • salt 필요
  • Brute-force 공격에 대비가 가능해야 한다. (연산속도가 너무 빠르면 안된다.)

즉, hash && salt를 쓰는 이유를 살펴보면 된다!
: 똑같은 문자열의 패스워드라도 hash 값이 다르게 할 수 있기 때문이다!

현재는 Bcrypt 암호화 알고리즘을 많이 사용하는 추세이다.
: hash, 단방향이기 때문에!

대칭키 vs 비대칭키(공개키) 암호화 차이

1) 대칭키

암복호화에 사용하는 키가 동일! -> private 키

  • 장점: 속도가 빠르다.

  • 단점: 키가 탈취될 염려가 있어 인터넷이라는 공간안에서 쓰기 힘들다.

  • 대칭키 알고리즘 : DES, AES 등

2) 비대칭키(공개키)

암복호화에 사용하는 키가 2개! -> private(개인) 키 & public(공개) 키

public키: 모든 사람이 접근 가능
private키: 각 사용자만이 가지고 있는 키

  • 장점: 안정성!

예를 들어, A가 B에게 데이터를 보낸다고 할 때, A는 B의 공개키로 암호화한 데이터를 보내고 B는 본인의 개인키로 해당 암호화된 데이터를 복호화해서 보기 때문에 암호화된 데이터는 B의 공개키에 대응되는 개인키를 갖고 있는 B만이 볼 수 있게 되는 것이다.

1) B 공개키/개인키 쌍 생성
2) 공개키 공개(등록), 개인키는 본인이 소유
3) A가 B의 공개키를 받아옴
4) A가 B의 공개키를 사용해 데이터를 암호화
5) 암호화된 데이터를 B에게 전송
6) B는 암호화된 데이터를 B의 개인키로 복호화 (개인키는 B만 가지고 있기 때문에 B만 볼 수 있음)

중간 공격자가 B의 공개키를 얻는다고 해도 B의 개인키로만 복호화가 가능하기 때문에 기밀성을 제공하며 개인키를 가지고있는 수신자만이 암호화된 데이터를 복호화할 수 있으므로 일종의 인증기능도 제공한다

  • 단점 : 느리다.
  • 비대칭키 알고리즘 :
    디피-헬만(Diffie-Hellman)
    타원곡선암호(Elliptic Curve Cryptosystem, ECC)
    * 전자서명(digital signature)

구현소스1

설정

// crypto
implementation group: 'org.springframework.security', name: 'spring-security-crypto', version: '5.7.3'
implementation group: 'commons-logging', name: 'commons-logging', version: '1.2'

Utils

public class EncodePasswordUtils {
    /* 순환참조 안될려면 이렇게 */
    public static PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Service

    @Override
    @Transactional
    public void register(BoardVO board) {
        validateEntity(board);

        // 비밀번호 암호화 처리
        String encodedPassword = passwordEncoder().encode(board.getPassword());
        board.setPassword(encodedPassword);

        long result = boardMapper.registerWithSelectKey(board);
        log.info("register result: {}", result);

        if (board.getAttachList() != null && board.getAttachList().size() != 0) {
            board.getAttachList().forEach(attach -> {
                attach.setBoardId(board.getId());
                attachMapper.insert(attach);
            });
            boardMapper.registerFileYN(board.getId());
        }
    }

구현소스2

설정

// BCrypt
implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.3m'

1) 인터페이스 주입 방식

public interface Encryptor {
    String encrypt(String origin);
    boolean isMatch(String origin, String hashed);
}
public class BCryptEncryptor implements Encryptor {
    @Override
    public String encrypt(String origin) {
        return BCrypt.hashpw(origin, BCrypt.gensalt());
    }

    @Override
    public boolean isMatch(String origin, String hashed) {
        try {
            return BCrypt.checkpw(origin, hashed);
        } catch (Exception e) { // 여러 예외가 있다.
            return false;
        }
    }
}

2) static 메서드 주입방식 : 내가 자주 사용하는 방식

public class Encryptor {

    public static String encrypt(String origin) {
        return BCrypt.hashpw(origin, BCrypt.gensalt());
    }

    public static boolean isMatch(String origin, String hashed) {
        try {
            return BCrypt.checkpw(origin, hashed);
        } catch (Exception e) { // 여러 예외가 있다.
            return false;
        }
    }
}

User

...
    public static User of(SignUpRequest signUpRequest) {
        return User.builder()
                .email(signUpRequest.getEmail())
                .name(signUpRequest.getName())
                .nickname(signUpRequest.getNickname())
                // User 엔티티에 패스워드 암호화하면 테스트시 편해짐!
                .password(encrypt(signUpRequest.getPassword()))
                .build();
    }

Service

    @Override
    @Transactional(readOnly = true)
    public LoginDto getByEmailAndPassword(String email, String password) {
        // 파라미터 password가 hash값이다.
        User user = userRepository.findByEmail(email)
                .map(u -> Encryptor.isMatch(u.getPassword(), password) ? u : null)
                .orElseThrow(() -> new WSApiException(ErrorCode.NOT_FOUND_USER));
        log.info("getByEmailAndPassword user : {}", user);

        return LoginDto.mapToDto(user);
    }    


참고

profile
배운 것을 기록합니다.

0개의 댓글