spring 일기 2번째
회원의 정보로는 아이디, 비밀번호, 이름 등등.. 다양한 정보를 가지고 있다.
이 때 회원의 정보보호를 위해서, 필수적으로 비밀번호를 암호화해야한다는 사실! (다양한 법적 문제에 의해..)
오늘은 암호화를 위해 공부한 내용을 작성해두고자 한다.
User 회원가입의 설계
1. User 1이 회원가입을 실행한다. (암호화 하기 이전에 동일 회원이 없는지 확인한다!)
2. User 1이 설정한 비밀번호(평문)를 DB에 저장하기 전에 암호화를 진행한다. (어떠한 알고리즘에 의해서)
3. 암호화된 비밀번호를 DB에 저장한다.
그러면 로그인을 할 때, 유저가 암호화된 비밀번호를 기억해야하나요?
당연히 아니다!
이 때, 유저가 로그인을 실행할 경우마다 암호화를 진행하는데 여기서 암호화된 문장을 복호화할수 있냐 없냐에 따라 단방향 암호화, 양방향 암호화로 나뉜다.
- 단방향 암호화 : 암호화된 문장을 복호화할 수 없는 상태.
이 떄문에 비밀번호를 찾을 때는, 유저에게 간단한 확인정보를 받은 뒤, 비밀번호를 재설정하도록 한다.
(찾기는 불가능) - 양방향 암호화 : 암호화된 문장을 복호화할 수 있는 상태.
비밀번호 평문을 잠구고 해제하는 자물쇠를 암호화라 한다면, Key라는 열쇠의 개념이 존재한다고 생각하면 쉽다.
잠구고 해제할 수 있는 열쇠를 따로 만들어야 하기에, 유저가 많아지면 서버의 성능부하가 발생할 수 있다.
나는 성능을 위해 단방향 암호화를 사용할 것이다! (이 개념은 생각보다 복잡하므로 더 공부해야하는 부분 중 하나이다.)
단방향 암호화 중 Hash기법을 활용하고자 한다.
1. 의존성 추가
먼저 암호화를 위해 dependency에 새로운 implement를 추가해야한다. spring security에서 제공하는 PasswordEncoder Interface를 이용하며, BCryptPasswordEncoder를 사용하고자 한다.
build gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
2. 암호화 알고리즘 구성하기
이제 암호화를 구성할 알고리즘 중 하나인, BCrypt알고리즘을 구현체로 등록하고자 한다. SecurityConfig파일을 구성하여 BCrypt알고리즘을 Bean으로 등록한다.
(중요한 점으로, dependency에 spring security를 추가하면 Httpsecurity에 대한 인증, 인가에 대한 설정을 해두지 않은 채, http request를 할 경우 401(unauthorized) 오류가 발생할 수 있다. 그러니 요청에 대한 인증 문제를 미리 해제해두자!)
SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
} // passwordEncoder를 Beam으로 등록함.
// 인증 or 인가에 대한 설정 진행...
}
3. 암호화를 위한 method 구현
암호화할 알고리즘을 설정했으니, 비밀번호 평문에 대해 직접적으로 알고리즘을 적용시커 비밀번호를 암호화하는 method를 작성하고자 한다. 코드의 가독성을 위해 Entity의 method로 만들었으며, PasswordEncoder interface의 기존 method를 활용했다. 주요 2가지 method를 소개하고자 한다.
String encode(CharSequence rawPassword); : 평문password를 파라미터로 받아서 암호화시켜주는 method
Boolean matches(CharSequence rawPassword, String encodedPassword);: 평문 비밀번호와 암호화된 비밀번호를 비교해주는 method
UserEntity
@Entity
@Getter
@Builder
@DynamicInsert
@NoArgsConstructor @AllArgsConstructor
@Table(name = "user_table")
public class UserEntity {
// 이런 저런 Column들...
/*
* param : 암호화할 인코더 클레스
* return : 변경된 유저의 entity(비밀번호)
*/
public UserEntity hashPassword(PasswordEncoder passwordEncoder) {
this.password = passwordEncoder.encode(this.password);
return this;
} // 비밀번호 암호화 과정. entity의 기존 비밀번호를 암호화시킴.
/*
* param1 : 암호화전 비밀번호
* param2 : 암호화에 사용된 class
* return : 같은지 다른지. true false
* */
public Boolean checkPassword(String password, PasswordEncoder passwordEncoder) {
return passwordEncoder.matches(password, this.password);
} // 비밀번호 확인
}
4. Service method에 암호화 method 추가
이제 Entity의 method를 활용해서 유저가 입력한 비밀번호를 바꿔주는 service부분의 구현을 작성한다. 이전에 이미 회원가입과 로그인에 대한 method를 작성했기에 암호화하는 부분만 추가하였다.
코드 특징으로 본래 method바깥에 private으로 PasswordEncoder를 참조한 뒤 재사용하는 방식이 훨씬 좋다. 현재 코드는 method마다 BCryptPasswordEncoder를 생성하여 사용하고 있다.
=> 이유가 SecurityConfig에 이미 Spring Security의 설정과 jwt 발급을 위해 이미 Service를 참조하고 있다. 이에 Service에서 PasswordEncoder를 참조할 경우, Bean 간의 순환 참조가 발생하여 어플리케이션에 문제가 발생한다. (순환참조에 대한 공부도 필요...)
UserService
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
// private final PasswordEncoder bCryptPasswordEncoder
// 위와 같이 선언하면 굳이 method마다 생성자를 만들 필요 x
public void userSave(UserDTO userDTO) throws Exception {
Optional<UserEntity> studentId = userRepository.findByStudentId(userDTO.getStudentId());
if(studentId.isPresent())
throw new Exception("This studentId already exist.");
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
UserEntity userEntity = UserDTO.toUserEntity(userDTO);
userEntity.hashPassword(bCryptPasswordEncoder); // 유저 비밀번호 암호화 과정
userRepository.save(userEntity); // 그대로 저장함
} // 회원 정보 저장
public String login(LogInDTO logInDTO){
Optional<UserEntity> userEntity = userRepository.findByStudentId(logInDTO.getStudentId());
UserEntity user = userEntity.get();
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
if(userEntity.isPresent()) { // 학번을 통해 찾은 user의 정보가 존재한다면
if(user.checkPassword(logInDTO.getPassword(), bCryptPasswordEncoder)) {
return JwtUtil.createJwt(user.getName(), user.getStudentId(), secretKey);
}
else { // password 일치하지 않을 경우
return null;
}
}
else { // user의 학번이 Entity에 없는 경우
return null;
}
}// login. null일 경우 회원정보 불일치함. 아닐 경우, 회원정보 일치. 회원 정보 return.
}
5. Controller 구성
이제 controller에 service의 동작을 적용시켜 url의 요청을 받으면 완성된다!
(아직 jwt는 token만 발급되는 상황인 것을 참조할 것..! 개발 열심히 해야지..ㅎ)
UserController
@RestController
@RequestMapping("/tgwing.kr")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@CrossOrigin
@PostMapping("/register")
public ResponseEntity<Void> register(@RequestBody UserDTO userDTO) throws Exception {
System.out.println("User Register");
System.out.println("userDTO = " + userDTO);
userService.userSave(userDTO); // 회원 저장
return ResponseEntity.ok().build();
} // 회원 저장
@CrossOrigin
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LogInDTO logInDTO) {
String key = userService.login(logInDTO);
if(key == null) { // null값일 경우 회원 정보를 못찾은 것임.
System.out.println("로그인 실패. 회원정보 불일치");
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
System.out.println("로그인 성공.");
return ResponseEntity.ok().body(key);
} // 회원 로그인
}
동작 확인
아래 사진처럼 register를 통해 내 정보를 입력하면 비밀번호가 평문이 아닌 암호화된 모습을 확인할 수 있다!
그리고 로그인을 시도하면 아래와 같이 token이 발급된다!
개발 일지이기에 부족한 점이나 잘못된 부분은 댓글로 말씀 부탁드려요!
'spring' 카테고리의 다른 글
[EP 1-4] Spring 소셜로그인(OAuth2) 써보기1: 소셜로그인 완벽 이해하기 (0) | 2025.03.13 |
---|---|
[EP 1-3] spring jpa json serialize 문제 해결과정 (부제: JPA 지연 로딩) (0) | 2024.10.15 |
[EP 1-1] Spring Entity Default 설정 방법 (1) | 2023.11.16 |