위의 대칭키, 비대칭키의 설명을 보고 오면 좋다.
암호화는 복호화가 불가능한 단방향, 복호화가 가능한 양방향 암호화가 있다. 클라이언트에게 받은 비밀번호는 서버도 확인이 불가능하기 때문에 복호화가 불가능한 단방향 암호화를 해 저장해야 한다.
1. 라이브러리 설치
비밀번호 암호화를 위해 단방향 해시 중 하나인 Bcrypt 를 사용한다.
// https://mvnrepository.com/artifact/org.mindrot/jbcrypt implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.4'
mvnrepository 에서 라이브러리를 설치한다.
2. JUnit 테스트
테스트 폴더에 그림과 같이 테스트 파일을 만든다.
test/java/com.example.springblog/_core.util/BCryptTest
package com.example.springblog._core.util; import org.junit.jupiter.api.Test; import org.mindrot.jbcrypt.BCrypt; public class BCryptTest { @Test // 솔트가 어떤 형태인지 확인하기 위해 테스트 public void gensalt_test(){ String salt =BCrypt.gensalt(); System.out.println(salt); } @Test public void hashpw_test(){ String rawPassword = "1234"; String encPassword = BCrypt.hashpw(rawPassword,BCrypt.gensalt()); System.out.println(encPassword); } }
jBCrypt는 솔트(salt)라는 임의의 데이터를 비밀번호에 추가하여 같은 비밀번호라도 다른 해시 값을 생성하는 기능을 제공한다. 이 기능을 사용하면 같은 비밀번호를 사용하는 사용자가 여럿 있더라도 각각 다른 해시 값을 저장할 수 있어 보안성을 높일 수 있다.
gensalt_test()
메서드를 만들어 솔트가 어떤 형태인지 확인해본다.솔트 값 확인
솔트는 실행 시마다 값이 달라진다.
hashpw_test()
를 사용해 비밀번호 1234 를 암호화한다.1234 암호화
insert into user_tb(username, password, email, created_at) values('ssar', '$2a$10$biACxO4imHNgIzJGxz/sZ.BPu/tTOzANv9cJ08kTppqalWxept8YO', 'ssar@nate.com', now()); insert into user_tb(username, password, email, created_at) values('cos', '$2a$10$biACxO4imHNgIzJGxz/sZ.BPu/tTOzANv9cJ08kTppqalWxept8YO', 'cos@nate.com', now());
더미 데이터의 값을 해시한 값으로 넣는다.
DB에 비밀번호가 암호화된 상태로 저장된다.
3. 클라이언트가 입력한 비밀번호 암호화
DB에 저장된 비밀번호는 암호화된 비밀번호기 때문에 로그인을 하려면 클라이언트가 입력한 비밀번호도 암호화되어 비교가 되어야 한다.
user/UserRepository
@PostMapping("/join") public String join(UserRequest.JoinDTO requestDTO){ //비밀번호 암호화 String rawPassword = requestDTO.getPassword(); String encPassword = BCrypt.hashpw(rawPassword,BCrypt.gensalt()); requestDTO.setPassword(encPassword); //유저네임 중복여부 확인 try{ userRepository.save(requestDTO); }catch (Exception e){ throw new RuntimeException("아이디가 중복되었어요"); } return "redirect:/loginForm"; }
회원가입을 할 때 비밀번호를 암호화한 상태로 DB에 저장한다.
user/userRepository
@Transactional public void save(UserRequest.JoinDTO requestDTO) { Query query = em.createNativeQuery("insert into user_tb(username,password,email) values (?,?,?)"); query.setParameter(1,requestDTO.getUsername()); query.setParameter(2,requestDTO.getPassword()); query.setParameter(3,requestDTO.getEmail()); query.executeUpdate(); }
4. 클라이언트가 입력한 비밀번호와 비교하기
@PostMapping("/login") public String login(UserRequest.LoginDTO requestDTO,HttpServletRequest request){ // 유효성 검사 if(requestDTO.getUsername().length()<3){ throw new RuntimeException("아이디는 3자리 이상이어야 합니다."); } // 유저네임으로 데이터를 가져옴 User user = userRepository.findByUsername(requestDTO.getUsername()); // 비밀번호 비교 if(!BCrypt.checkpw(requestDTO.getPassword(),user.getPassword())){ // !가 있으면 역치. throw new RuntimeException("패스워드가 틀렸습니다."); } session.setAttribute("sessionUser",user); return"redirect:/"; }
비밀번호 암호화 전에는
findByUsernameAndPassword
메서드를 통해 유저네임과 비밀번호를 DB에서 받았다. 하지만 암호화 이후에는 DB에서 유저네임만 받아서 비교할 수 있다.checkpw
는 jBCrypt 라이브러리에서 제공하는 메서드로, 해시된 비밀번호와 사용자가 입력한 비밀번호를 비교하는 데 사용된다.user/userRepository
public User findByUsername(String username) { Query query = em.createNativeQuery("select * from user_tb where username=?", User.class); // 알아서 매핑해줌 query.setParameter(1, username); try { User user = (User) query.getSingleResult(); return user; } catch (Exception e) { // DB에 입력한 유저네임이 없을 때 throw new RuntimeException("아이디를 찾을 수 없습니다"); } }
비밀번호 “12345” 를 입력한다
alert창이 뜬다.
비밀번호를 “1234”로 입력한다.
정상적으로 로그인이 완료된다.
package com.example.springblog.user; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.mindrot.jbcrypt.BCrypt; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseBody; import java.sql.JDBCType; @RequiredArgsConstructor @Controller public class Usercontroller { private final UserRepository userRepository ; private final HttpSession session; @PostMapping("/join") public String join(UserRequest.JoinDTO requestDTO){ String rawPassword = requestDTO.getPassword(); String encPassword = BCrypt.hashpw(rawPassword,BCrypt.gensalt()); requestDTO.setPassword(encPassword); try{ userRepository.save(requestDTO); }catch (Exception e){ throw new RuntimeException("아이디가 중복되었어요"); } return "redirect:/loginForm"; } @PostMapping("/login") public String login(UserRequest.LoginDTO requestDTO){ User user = userRepository.findByUsername(requestDTO.getUsername()); if(requestDTO.getUsername().length()<3){ throw new RuntimeException("유저네임이 너무 짧습니다"); } if(!BCrypt.checkpw(requestDTO.getPassword(),user.getPassword())){ // !가 있으면 역치. 패스워드 검증이 실패하면 throw new RuntimeException("패스워드가 틀렸습니다."); } session.setAttribute("sessionUser",user); // ! 안쓴 긍정문일 때 // if(BCrypt.checkpw(requestDTO.getPassword(),user.getPassword())){ // session.setAttribute("sessionUser",user); // }else{ // throw new RuntimeException("패스워드가 틀렸습니다."); // } return"redirect:/"; } @GetMapping("/joinForm") public String joinForm() { return "user/joinForm"; } @GetMapping("/loginForm") public String loginForm() { return "user/loginForm"; } @GetMapping("/user/updateForm") public String updateForm(HttpServletRequest request) { User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { return "redirect:/loginForm"; } User user = userRepository.findByUsernameAndEmail(sessionUser.getId()); request.setAttribute("sessionUser",user); return "user/updateForm"; } @PostMapping("/user/update") public String update(UserRequest.passwordUpdateDTO requstDTO,HttpServletRequest request){ User sessionUser = (User) session.getAttribute("sessionUser"); if(sessionUser==null){ return "redirect:/loginForm"; } if(sessionUser.getPassword().equals(requstDTO.getPassword())){ request.setAttribute("msg","비밀번호가 동일합니다."); request.setAttribute("status",400); return "error/40x"; } userRepository.passwordUpdate(requstDTO,sessionUser.getId()); return "redirect:/"; } @GetMapping("/logout") public String logout() { session.invalidate(); return "redirect:/";} }
Share article