출처
www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1/dashboard
절대로 패스워드를 평문으로 저장해서는 안 됩니다.
- Account 엔티티를 저장할 때 패스워드 인코딩하기
스프링 시큐리티 권장 PasswordEncoder
- PasswordEncoderFactories.createDelegatingPasswordEncoder()
- 여러 해시 알고리즘을 지원하는 패스워드 인코더
- 기본 알고리즘 bcrypt
해싱 알고리즘과 솔트(salt)
- 해싱 알고리즘을 쓰는 이유?
- 솔트를 쓰는 이유
절대로 패스워드를 평문으로 저장해서는 안 됩니다.
- Account 엔티티를 저장할 때 패스워드 인코딩하기
패스워드 등의 보안에 민감한 정보 & 양방향 암호화 복호화도 필요 없다면 해싱을 하면 된다.
평문 해싱값이 저장된 해싱값과 일치하는지로 로그인
해싱 알고리즘(bcrypt)과 솔트(salt)
- 해싱 알고리즘을 쓰는 이유?
abcde@email / 12345678 ->
다른 웹사이트에서도 동일한 아이디와 비밀번호를 사용한다
다른 보안에 민감한 곳 (은행, 주식 등등) 에서도 같은 조합으로 사용하는 경우가 많다.
해싱이란?
문자열을 일정한 알고리즘에 따라 adfjo12j908ja0sdfj2dna902mkl같이 변경한다.
- 솔트를 쓰는 이유
해커가 dictionary attack으로 이미 12345678 -> adfjo12j908ja0sdfj2dna902mkl로 해싱결과가 정해짐을 알고있다면
역 추적으로 12345678을 추정할 수 있다. (미리 비밀번호와 해싱값 테이블을 가지고 넣어가면서 해킹시도)
그래서 해싱 시에 12345678 + (salt) ->f2ijf1j3io9ahjds0io9fj9013rfj9asdjf 등으로 이전과는 다른 값이 도출된다.
그래서 해싱 테이블 대조만으로는 원래 비밀번호를 찾기가 매우 어려워진다.
해싱시 매번 솔트를 고정 값이 아닌 다른 값을 넣어도 전혀 무관하다.
bcrypt는 솔트를 매번 다른 값을 넣어도 무관하다.
bcrypt는 다른 해싱 알고리즘에 비해 의도적으로 해싱에 시간이 좀 더 걸린다.
이유는 시도 자체를 여러번 할 수 없도록 설정했기 때문이다.
org.springframework.security.crypto.factory의 createDelegatingPasswordEncoder()함수에서 bcrypt 생성 object
BCryptPasswordEncoder (org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder)
// 강도 설정
public BCryptPasswordEncoder(BCryptVersion version, int strength, SecureRandom random) {
if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {
throw new IllegalArgumentException("Bad strength");
}
this.version = version;
this.strength = strength == -1 ? 10 : strength; // strength가 높을 수록 높은 강도
this.random = random;
}
// 인코딩
public String encode(CharSequence rawPassword) {
String salt;
if (random != null) {
salt = BCrypt.gensalt(version.getVersion(), strength, random);
} else {
// random값을 지정하지 않았을 경우
//salt에 임의값을 생성해서
salt = BCrypt.gensalt(version.getVersion(), strength);
}
//hashing한 후 리턴한다.
return BCrypt.hashpw(rawPassword.toString(), salt);
// 매칭
// rawPassword => 로그인 입력 값
// encodedPassword => 해싱되어있는 값
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
logger.warn("Empty encoded password");
return false;
}
if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
logger.warn("Encoded password does not look like BCrypt");
return false;
}
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
public static boolean checkpw(String plaintext, String hashed) {
return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
// 입력 값과 해시 된 값을 다시 해싱을 한 값을 리턴한다.
// ex) 12345678(맞는 비밀번호) + 1jioadsjiojioajdoif => 1jioadsjiojioajdoif
// ex) 12345566(틀린 비밀번호) + 1jioadsjiojioajdoif => 8ajidof890fnahhzcsd
}
}
AppConfig.Class
package com.studyolle.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
public class AppConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
package com.studyolle.account;
import com.studyolle.domain.Account;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.validation.Valid;
@Service
@RequiredArgsConstructor
public class AccountService {
private final AccountRepository accountRepository;
private final JavaMailSender javaMailSender;
private final PasswordEncoder passwordEncoder; // 객체 생성
public void processNewAccount(SignUpForm signUpForm) {
Account newAccount = saveNewAccount(signUpForm);
newAccount.generateEmailCheckToken(); //UUID
sendSignUpConfirmEmail(newAccount);
}
private Account saveNewAccount(@Valid SignUpForm signUpForm) {
Account account = Account.builder()
.email(signUpForm.getEmail())
.nickname(signUpForm.getNickname())
.password(passwordEncoder.encode(signUpForm.getPassword())) // 해싱
.studyCreateByWeb(true)
.studyEnrollmentResultByWeb(true)
.studyUpdateByWeb(true)
.build();
return accountRepository.save(account);
}
private void sendSignUpConfirmEmail(Account newAccount) {
SimpleMailMessage mailMessage = new SimpleMailMessage();
//Subject -> 제목 설정 | setText -> 본문설정
mailMessage.setTo(newAccount.getEmail());
mailMessage.setSubject("스터디올래, 회원 가입 인증");
mailMessage.setText("/check-email-token?token=" +
newAccount.getEmailCheckToken() + "&email=" + newAccount.getEmail());
javaMailSender.send(mailMessage);
}
}
'프로젝트 정리 > 스프링과 JPA 기반 웹 애플리케이션 개발' 카테고리의 다른 글
11. 회원 가입: 인증 메일 확인 테스트 및 리팩토링 (0) | 2021.02.25 |
---|---|
10. 회원 가입: 인증 메일 확인 (0) | 2021.02.24 |
7. 회원 가입 : 리팩토링 및 테스트 (0) | 2021.02.19 |
6 - 2 . 회원 가입 폼 서브밋 처리 (0) | 2021.02.17 |
6 - 1 . 회원 가입 : 폼 서브밋 검증 (0) | 2021.02.16 |