회원 가입 패스워드 인코딩

Yuri Lee·2020년 11월 6일
0

절대로 패스워드를 평문으로 저장해서는 X

  • Account 엔티티를 저장할 때 패스워드 인코딩하기!
  • 패스워드처럼 보안에 민감한 정보는 양방향으로 암호화, 복호화 할 필요가 없다. 해싱을 하면 된다. 로그인할때 입력한 평문이 해쉬값과 일치하는지 확인하면 된다.
  • 양방향이 아닌 단방향

스프링 시큐리티 권장 PasswordEncoder

  • PasswordEncoderFactories.createDelegatingPasswordEncoder()
  • 여러 해시 알고리듬을 지원하는 패스워드 인코더
  • 기본 알고리듬 bcrypt

해싱 알고리듬(bcrypt)과 솔트(salt)

- 해싱 알고리즘을 쓰는 이유?

우리가 만약 데이터베이스에다가 평문 그대로 leee@gmail.com / 12345678 => aaaabbbb
만약 해커한테 12345678 비밀번호가 털렸다고 하자. 굉장히 큰 문제가 된다. (비밀번호가 은행 계좌일 수도 있고, 주식거래 하는 번호일 수도 있고) 그래서 해싱해야 한다. 특정한 알고리즘을 따라서 (SHA , bcrypt 등등) 해싱한다. 스프링 시큐리티가 제공하는 PasswordEncoderFactories.createDelegatingPasswordEncoder() 사용시 기본적으로 bcrypt 알고리즘을 따른다.

- salt(솔트)를 쓰는 이유?

해커가 12345678 => aaaabbbb 에서 aaaabbbb 를 보고 얘의 비밀번호가 12345678 이라는 것을 알아낼 수 있다. 그래서 디비에 저장을 할때 12345678 + salt => aaaabbbb, 비밀번호에다가 솔트값을 넣어 전혀 다른 값을 가지도록 한다. 해싱을 할때마다 랜덤한 값을 사용해도 동작한다.

  • 솔트값이 애플리케이션에서 고정된 값이 아니여도 동작하는 이유는 ? bcrypt 는 솔트를 매번 해싱할 때 랜덤한 값을 사용해도 괜찮다. 동일한 비밀번호지만 솔트값이 매번 바뀌기 때문에 해싱된 결과가 매번 바뀐다. 솔트값은 패스워드를 인코딩 할때만 사용한다.

AppConfig class 생성하기

AppConfig.java

package com.goodmoim.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration  //빈등록(IoC관리)
public class AppConfig {
 
    @Bean //스프링 컨테이너에서 객체를 관리할 수 있게 하는 것
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

@Bean 어노테이션과 @Component 어노테이션 둘다 Spring(IoC) container 에 Bean 을 등록하도록 하는 메타 데이터를 기입하는 어노테이션이다. 그렇다면 왜 두개나 만들어 놓았을까? 둘의 용도가 다르기 때문이다. Bean 어노테이션의 경우 개발자가 직접 제어가 불가능한 외부 라이브러리등을 Bean으로 만들려 할때 사용된다. 반면 Component 어노테이션의 경우 개발자가 직접 작성한 class를 Bean으로 등록하기 위한 어노테이션이다.

BCryptPasswordEncoder

BCryptPasswordEncoder.java

    public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength, SecureRandom random) {
        this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
        this.logger = LogFactory.getLog(this.getClass());
        if (strength == -1 || strength >= 4 && strength <= 31) {
            this.version = version;
            this.strength = strength == -1 ? 10 : strength;
            this.random = random;
        } else {
            throw new IllegalArgumentException("Bad strength");
        }
    }

    

-1이면 strength 10, 강도를 설정할 수 있다. bcrypt 는 다른 해싱 알고리즘에 비해서 의도적으로 시간이 더 걸린다. 해커들이 여러번 시도할 수 없도록 한 것임. 약간 느리다는 게 장점 중 하나임.

    
     public String encode(CharSequence rawPassword) {
        if (rawPassword == null) {
            throw new IllegalArgumentException("rawPassword cannot be null");
        } else {
            String salt = this.getSalt();
            return BCrypt.hashpw(rawPassword.toString(), salt);
        }
    }
    private String getSalt() {
        return this.random != null ? BCrypt.gensalt(this.version.getVersion(), this.strength, this.random) : BCrypt.gensalt(this.version.getVersion(), this.strength);
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (rawPassword == null) {
            throw new IllegalArgumentException("rawPassword cannot be null");
        } else if (encodedPassword != null && encodedPassword.length() != 0) {
            if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                this.logger.warn("Encoded password does not look like BCrypt");
                return false;
            } else {
                return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
            }
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
    }

패스워드를 encode 할때 getSalt 값을 받아와서 (랜덤한 값) hashpw 해싱을 진행한다. rawPassword 내가 입력한 1234578과 salt를 갖고 해싱한다. 실제로 매칭할 때 볼때는 rawPassword (로그인할 때 입력한 1234578), encodedPassword (db에 저장된 해싱 되어있는 값) 이 둘을 매칭되는지 볼때는

    public static boolean checkpw(String plaintext, String hashed) {
        return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
    }

내가 입력한 plaintext (12345678) 값과 hashed 값으로 다시 한번 해싱을 한다. 이 값이 다시 hashed 과 일치하는지 확인한다. true 이면 값을 잘 입력한 것이다. 만약 12345789를 입력했을 때 이전에 12345678 + salt => aaa3333f 일 때 9를 추가 입력했으므로 다시 해시해도 같은 값이 나오지 않는다. 따라서 salt 값이 바뀌어도 상관이 없다.

Test

    @DisplayName("회원 가입 처리 - 입력값 정상")
    @Test
    void signUpSubmit_with_correct_input() throws Exception {
        mockMvc.perform(post("/sign-up")
                .param("nickname", "lee")
                .param("email", "leeee@email.com")
                .param("password", "12345678")
                .with(csrf()))
                .andExpect(status().is3xxRedirection())
                .andExpect(view().name("redirect:/")); // 리턴하는 뷰 이름 검증
        
        
        Account account = accountRepository.findByEmail("leeee@email.com");
        assertNotNull(account);
        assertNotEquals(account.getPassword(), "12345678");
        
        //leeee@email.com 계정이 실제로 있는지 
        //assertTrue(accountRepository.existsByEmail("leeee@email.com"));
        then(javaMailSender).should().send(any(SimpleMailMessage.class)); // 아무런 타입, sender가 호출되었는지 확인하는 것, 그럼 메일을 보내겠다고 확인할 수 있음
    }

다음의 테스트는 진행이 된다. 즉 평문 그대로 저장을 안한다는 것이 입증된다. 패스워드 인코더를 적용해서 회원가입 할때 패스워드 인코더를 적용했기 때문에 더 이상 입력된 값 그대로 저장되지 않았다.

Problem

sts 내부의 terminal을 이용해서 git 에 push 를 했더니 test 폴더 밑의 파일이 반영이 안되더라.. 오늘 두번 커밋을 했는데, 커밋 단위가 제대로 반영되지 않아서 보고 놀랬따 😥😥 왜지? 윈도우..;; 맥은 되는 것 같은데 .. 추가 플러그인을 설치해야 하나. ... 그래서 git reset --hard HEAD 이 명령어를 사용해서 돌아가긴 했는데 .. 뭔가 이상하게 꼬여서 ㅎㅎ 다 날릴 뻔 하다가 그냥 원상복귀 했다. git은 어려워 🤧🤧🤧🤧


출처 : 인프런 백기선님의 스프링과 JPA 기반 웹 애플리케이션 개발
https://jaddong.tistory.com/entry/%EC%9B%90%EA%B2%A9%EC%A0%80%EC%9E%A5%EC%86%8C%EC%97%90-%EC%98%AC%EB%9D%BC%EA%B0%84-%EC%BB%A4%EB%B0%8B-%EB%90%98%EB%8F%8C%EB%A6%AC%EA%B8%B0

profile
Step by step goes a long way ✨

0개의 댓글