| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- 별찍기
- JAVA 변수
- 스프링시큐리티 로그아웃
- 인텔리제이 Web 애플리케이션
- JSP 실습
- 접근제어자
- SQL import
- 중첩for
- Node.js 설치
- MySQL workbench dump
- SpringBoot
- @PreAuthorize("isAuthenticated()")
- Springsecurity
- Scanner 시간구하기
- if else
- SpringSecurity 로그아웃
- 회원정보 수정
- StringBuilder
- StringBuffer
- SpringSecurity 로그인
- 스프링부트 로그인
- 중첩 if
- D2Coding
- SQL dump
- 클래스 형변환
- System클래스
- 이클립스 설치
- 증감 연산자
- jdk 설정
- if else if
- Today
- Total
gi_dor
Spring Security와 BCrypt 해싱 본문
사이드 프로젝트를 진행하면서 입력한 비밀번호 값이 데이터베이스에 화끈하게 다 보이고있는 상태다
암호화 되지 않은 야생의 비밀번호 그 자체이기 때문에 데이터베이스를 누군가 탈취한다면 비밀번호가 그대로 저장되어있어
DB가 유출된다면 모든 사용자의 비밀번호가 유출되는 상황이 올수 있다
(과거 프로젝트 만들면서 DB 접속 호스트랑 비밀번호를 깃허브에 올려서 화끈하게 해킹 당한적이있음)
자바 스프링을 놓고 솔루션만 개발한지 시간이 지나 대부분 다 까먹어서 하나씩 알아가면서 하려고 한다
현재 HttpSession 을 통해 로그인 로그아웃을 만들어놓은 상태이다
Spring Security
Spring으로 만든 웹 애플리케이션의 인증(Authentication)과 권한 부여(Authorization)를 전문적으로 담당하는 프레임워크
- 역할
- 인증 (Authentication) : 사용자가 누구인지 확인하는 과정입니다. (예: 아이디/비밀번호로 로그인)
- 권한 부여 , 인가 (Authorization) : 인증된 사용자가 어떤 작업을 수행할 수 있는지 허가해주는 과정.
(예: '사용자 USER'는 사용자페이지만 접근 가능)
- 왜 쓰는지 ? 우리가 직접 로그인/로그아웃, 세션 관리, 페이지 접근 제어 같은 복잡하고 실수하기 쉬운 보안 로직을 만들 필요 없이, Spring Security가 제공하는 표준적이고 안전한 방법을 그대로 사용하기 위해
BCrypt
비밀번호를 안전하게 암호화하기 위해 사용하는 해시(Hash) 함수 중 하나이다. Spring Security에서 기본으로 권장하는 방식
- 단방향 해시 : 비밀번호를 암호화된 문자열(해시 값)로 바꿀 수는 있지만, 암호화된 문자열을 다시 원래 비밀번호로 되돌릴 수는 절대 없다. (관리자도 모름 , ** 가장 중요 **)
해커가 DB를 탈취해도 원래 비밀번호를 알아낼 수 없 - 솔트 (Salt) 사용: 같은 비밀번호를 입력해도, 암호화할 때마다 매번 다른 결과가 나온다.
- 왜? 암호화 시 '솔트' 라는 임의의 데이터를 비밀번호에 섞어서 암호화하기 때문
해커들이 미리 계산된 해시 값 목록(레인보우 테이블)을 사용한 공격을 할 수 없게 된다.
- 왜? 암호화 시 '솔트' 라는 임의의 데이터를 비밀번호에 섞어서 암호화하기 때문
- 어떻게 로그인 처리해?:
- 사용자가 로그인 시 입력한 '날것'의 비밀번호와 DB에 저장된 '암호화된' 비밀번호를 BCrypt에게 함께 전달.
그러면 BCrypt가 알아서 "이 두 개가 같은 비밀번호가 맞는지" 검증하여 true 또는 false를 알려준다.
우리는 그 결과만 믿고 사용하면 된다 - 검증(Verification) : 두 문자열이 같은지 ==로 비교하는 것이 아니라, BCrypt가 제공하는 matches() 기능을 통해 원본 비밀번호와 저장된 해시값이 수학적으로 일치하는지 확인
- 사용자가 로그인 시 입력한 '날것'의 비밀번호와 DB에 저장된 '암호화된' 비밀번호를 BCrypt에게 함께 전달.
SecurityConfig 파일
일단 개발 초기이니 전부다 허가 해놓는다
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// @Bean: 이 메서드가 반환하는 객체(PasswordEncoder)를 Spring 컨테이너가 관리하는 Bean으로 등록
// BCrypt 암호화 도구를 생성해서 Spring에 등록.
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// http 객체를 사용하여 메서드 체이닝 방식으로 보안 설정을 구성합니다.
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorize -> authorize.requestMatchers("/**").permitAll());
// 개발 편의상 일단 로그인을 안 한 사람이든 한 사람이든 상관없이 검사하지 말고 들여보낸다
return http.build();
}
}
MemberService 변경전
public class MemberService {
@Transactional
public Long join(String email, String password, String name ,String nickname) {
validateDuplicateEmail(email);
validateDuplicateNickName(nickname);
// member 객체 생성할때 createAt은 null로 들어감
// repository.save 호출
// DB 넣기 전에 @EntityListeners 가 작동해서 저장할때 현재 시간을 주입해줌
Member member = Member.builder()
.email(email)
.password(password)
.name(name)
.nickname(nickname)
.memberRole(MemberRole.USER)
.build();
Member savedMember = memberRepository.save(member);
return savedMember.getId();
}
public Member login(String email, String password) {
Member member = memberRepository.findByEmail(email);
if(member == null) {
return null;
}
if(!member.getPassword().equals(password)) {
return null;
}
return member;
}
/**
* 이메일 중복 체크
*/
private void validateDuplicateEmail(String email) {
if (memberRepository.existsByEmail(email)) {
throw new DuplicateFieldException("email","이미 존재하는 회원(이메일)입니다.");
}
}
/**
* 닉네임 중복 체크
*/
private void validateDuplicateNickName(String nickname) {
if (memberRepository.existsByNickname(nickname)) {
throw new DuplicateFieldException("nickname","이미 존재하는 닉네임 입니다.");
}
}
}
MemberService 변경후
public class MemberService {
@Transactional // 이 메서드는 데이터베이스에 정보를 저장(쓰기)하므로, @Transactional을 붙여서 쓰기 모드로 설정합니다.
public Long join(String email, String password, String name ,String nickname) {
// 호옥시 이미 가입된 이메일이나 닉네임인지 확인하기 (중복 체크)
validateDuplicateEmail(email);
validateDuplicateNickName(nickname);
// Member(회원) 객체를 만듬
Member member = Member.builder()
.email(email)
// 사용자가 입력한 비밀번호를 그대로 저장하면 해킹 시 위험
// 비밀번호를 아무도 알아볼 수 없는 암호문으로 바꿔준다 개꿀. (예: $2a$10$...)
.password(passwordEncoder.encode(password))
.name(name)
.nickname(nickname)
.memberRole(MemberRole.USER) // 처음 가입하는 사람은 모두 일반 사용자(USER)
.build();
// 완성된 회원 정보를 데이터베이스에 저장(save)
Member savedMember = memberRepository.save(member);
// 데이터베이스에 저장된 회원의 ID 번호를 알려준다
return savedMember.getId();
}
public Member login(String email, String password) {
// 사용자가 입력한 이메일로 데이터베이스에서 회원 찾기
Member member = memberRepository.findByEmail(email);
// 만약 그런 이메일을 가진 회원이 없다면, 당연히 로그인 실패이므로 null을 반환하고 끝낸다
if(member == null) {
return null;
}
// 데이터베이스에 저장된 암호문(member.getPassword())과
// 사용자가 방금 입력한 날것의 비밀번호(password)가 서로 짝이 맞는지 확인
// equals() , == 이딴거 쓰기 XXXX passwordEncoder.matches() 사용
if(!passwordEncoder.matches(password, member.getPassword())) {
// 만약 짝이 맞지 않는다면, 비밀번호가 틀린 것이므로 null을 반환하고 끝낸다
return null;
}
// 이메일도 존재하고 비밀번호도 짝이 맞는다면 로그인 성공
// 찾았던 회원 정보를 통째로 반환
return member;
}
}
왜 PasswordEncoder가 필요한가
비밀번호는 절대 DB에 그대로 저장하면 안 된다.
DB가 유출되는 순간, 모든 사용자의 계정이 즉시 위험해진다
그래서 Spring Security에서는 비밀번호를 저장할 때
단방향 해시(Hash) 함수를 사용하도록 강제하고 있고 그 역할을 담당하는 인터페이스가 바로 PasswordEncoder
PasswordEncoder는 다음 두 가지 역할을 가진다.
- 사용자가 입력한 평문 비밀번호를 해시값으로 변환
- 로그인 시 입력한 비밀번호가 저장된 해시값과 일치하는지 검증
즉, 애플리케이션 코드에서
“어떤 해시 알고리즘을 쓸지”, “salt를 어떻게 처리할지”를 직접 고민하지 않도록
Spring Security가 제공하는 보안 추상화 계층이라고 볼 수 있다.
BCryptPasswordEncoder를 사용하는 이유
BCryptPasswordEncoder는 PasswordEncoder의 구현체 중 하나로,
비밀번호 저장에 특화된 단방향 해시 알고리즘 BCrypt를 사용
BCrypt의 특징은 다음과 같다.
- 내부적으로 랜덤 salt를 자동 생성
- 동일한 비밀번호라도 매번 다른 해시 결과 생성
- 연산 비용(cost)을 조절할 수 있어 무차별 대입 공격에 강함
따라서 Spring Security 환경에서는 별도의 이유가 없다면 BCryptPasswordEncoder를 사용하는 것이 표준
비밀번호 저장과정
String encodedPassword = passwordEncoder.encode(rawPassword);
encode() 메서드는
- 평문 비밀번호
- 랜덤 salt
- Bcrypt 알고리즘 들을 사용해 복호화 불가능한 해시 문자열을 생성한다
matches()는 어떻게 동작하는가
로그인 시에는 비밀번호를 다시 복호화해서 비교하지 않는다.
애초에 BCrypt는 복호화 자체가 불가능한 알고리즘이기 때문
passwordEncoder.matches(rawPassword, encodedPassword);
- DB에 저장된 encodedPassword에서 salt 값과 cost 값 추출
- 사용자가 입력한 rawPassword에 동일한 salt + 동일한 cost로 다시 해시 수행
- 새로 계산된 해시결과와 DB에 저장된 해시값 비교
- 같으면 true , 틀리면 false 반환
- 단순 문자열 비교 XXXXXXXX
왜 equals()로 비교하면 안 되는가
passwordEncoder.encode(rawPassword).equals(encodedPassword);
BCrypt 는 랜덤 salt 를 사용하므로 같은 비밀번호 여도 equals() 결과는 매번 다르다
그러므로 항상 실패 , 반드시 matches() 메서드 사용하기
정리
- PasswordEncoder는 비밀번호 해시와 검증을 책임지는 보안 인터페이스다
- BCrypt는 복호화가 불가능한 단방향 해시 알고리즘이다
- 로그인 시에는 복호화가 아니라 재해시 후 비교를 수행한다
- 비밀번호 비교는 반드시 matches()를 사용해야 한다


풀이
$2a$ : 사용할 BCrypt 알고리즘의 버전
$10$ : 비용(Cost) 또는 작업량(Work Factor)을 의미한다고 함 , 10은 일반적 권장되는 기본값\
sDgalEi17wU7GzWCuy565utADxB8.Qvkd025H2iAH0dD37UjVYUEO
솔트(Salt) : 앞부분 22글자(sDgalEi17wU7GzWCuy565u)는 암호화 시 사용된 '임의의 소금' 값
솔트 덕분에 같은 비밀번호라도 매번 다른 암호문이 생성
해시 결과(Hashed Result) : 뒷부분 31글자(tADxB8.Qvkd025H2iAH0dD37UjVYUEO)가
사용자의 비밀번호와 솔트를 조합한 해시 계산을 한 최종 결과물
'Back_End > SpringBoot' 카테고리의 다른 글
| Spring에서 비동기 @Async (0) | 2024.08.29 |
|---|---|
| [Refactor] 카페인 캐싱으로 성능개선 , Ngrinder (1) | 2024.06.11 |
| 스프링부트 + MyBatis +MYSQL 페이징 처리 (0) | 2024.05.09 |
| 스프링부트 설정파일(application.properties) 암호화 (Jasypt) (0) | 2024.05.02 |
| 비밀번호 찾기 + 임시비밀번호 이메일전송 (0) | 2024.04.30 |