31. Spring Security : 비밀번호 암호화하기

yuzu sim's avatar
Feb 12, 2024
31. Spring Security :  비밀번호 암호화하기

  • 단방향 암호화 : 복구화가 안됨
  • hash : 동일한 hash값이 나옴
  • crack이 안된 안전한 hash코드를 써야 함
 
notion image

1. 인증 절차

1) 들어온 주소 확인하기
/login → loadByUsername() 실행
DB에 ssar이라는 username이 있는지 확인
있으면 UserDetails로 응답(user) → 컴포지션이 필요함
2) password 검증하기
getPassword() : hash 값
사용자가 입력한 비밀번호 hash화
DB에 있는 비밀번호와 사용자가 입력한 비밀번호 비교
- 같은 hash를 사용해야 함
기본 해쉬 값 : Bycript
다른 hash를 하고 싶으면 security 필터를 커스터 마이징 해야 함
- 값이 같으면 session 만듦
UserDetails가 authentication에 당김
내가 키 값을 모르면 getattribute()할 수 없음
 
  • 머스태취에서 session에 접근 가능함
  • 머스태취 헤더 파일에서 쓸 수 있는 이유 : session에 저장 → 이제 안 먹힐 것
 
notion image

2. 순서 정리

  1. 사용자가 로그인 요청을 보냄
  1. 이 요청을 가로채어 사용자가 제공한 인증 정보(예: 사용자 이름과 비밀번호)를 검증
  1. 인증이 성공하면, 보안 컨텍스트에 사용자의 정보를 저장하고, 인증된 사용자로써의 권한을 부여
  1. session에 저장되어 후속 요청에서 사용
session에 저장된 인증 정보를 이용해 요청이 들어올 때마다 사용자를 식별하고 권한을 확인
  1. 새로운 요청이 들어오면, 이 요청에 대한 인증 헤더, 쿠키 또는 다른 인증 수단을 확인
보안 컨텍스트를 복원하고, 사용자를 인증
  1. 만약 인증에 실패하면, 해당 요청은 보호된 자원에 접근할 수 없음
 

3. 비밀번호 암호화해서 DB에 담기

  • 비밀번호 hash 값으로 구하기
  • 컴퓨터 시스템마다 해쉬 값이 다름
package shop.mtcoding.blog.util; import org.junit.jupiter.api.Test; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class EncodeTest { @Test public void encoded_test() { BCryptPasswordEncoder en = new BCryptPasswordEncoder(); String rawPassword = "1234"; String encPassword = en.encode(rawPassword); System.out.println(encPassword); } }
notion image
 
  • DB에 hash값으로 비번 변경하기
insert into user_tb(username, password, email, created_at) values('ssar', '$2a$10$z18Td.MxcWGbVbITabM83.FGlvN/P5F.taTF1oLoNeddA.HvRQoLa', 'ssar@nate.com', now()); insert into user_tb(username, password, email, created_at) values('cos', '$2a$10$z18Td.MxcWGbVbITabM83.FGlvN/P5F.taTF1oLoNeddA.HvRQoLa', 'cos@nate.com', now()); insert into board_tb(title, content, user_id, created_at) values('제목1','내용1', 1, now()); insert into board_tb(title, content, user_id, created_at) values('제목2','내용2', 1, now()); insert into board_tb(title, content, user_id, created_at) values('제목3','내용3', 1, now()); insert into board_tb(title, content, user_id, created_at) values('제목4','내용4', 2, now());
 

4. user 찾기 테스트 하기

package shop.mtcoding.blog._core.config.security; import jakarta.servlet.http.HttpSession; import lombok.AllArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import shop.mtcoding.blog.user.User; import shop.mtcoding.blog.user.UserRepository; /* * 조건 * post 요청 * "/login"요청 * x-www-form-urlencoded * 키값이 username, password*/ @AllArgsConstructor @Service public class MyLoginService implements UserDetailsService { private final UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("loadUserByUsername: " + username); User user = userRepository.findByUsername(username); if (user == null) { System.out.println("user는 null"); return null; // 로그인 진행하던걸 취소하고 알아서 응답해줌 -> 반환할 페이지를 알려줘야함 } else { System.out.println("user를 찾았어요"); return new MyLoginUser(user); // securityContextHolder에 저장됨 } }
notion image
notion image
 
  • session에 있는 securityholder안에 값이 들어감
 

5. 로그인 확인하기

package shop.mtcoding.blog.board; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog._core.config.security.MyLoginUser; import shop.mtcoding.blog.user.User; import java.util.List; @RequiredArgsConstructor @Controller public class BoardController { private final HttpSession session; // DI private final BoardRepository boardRepository; // DI // ?title=제목1&content=내용1 // title=제목1&content=내용1 // 쿼리 스트링과 -x-www-form-urlencoded와 파싱 방법이 동일함 // http://localhost:8080?page=0 @GetMapping({"/"}) public String index(HttpServletRequest request, @AuthenticationPrincipal MyLoginUser myLoginUser) { System.out.println("로그인 되었나?: " + myLoginUser.getUsername()); List<Board> boardList = boardRepository.findAll(); request.setAttribute("boardList", boardList); return "index"; } // 상세보기시 호출 @GetMapping("/board/{id}") // 1이 프라이머리키 -> 뭐든 넣어도 실행시키려면 변수화시켜서 {} public String detail(@PathVariable int id, HttpServletRequest request) { System.out.println("id : " + id); // 1. 바로 모델 진입 -> 상세보기 데이터 가져오기 // body 데이터가 없으면 유효성 검사할 필요 없음 BoardResponse.DetailDTO reponseDTO = boardRepository.findByIdWithUser(id); //메서드 이름 변경 // user 객체를 가져와서 session 값 받기 : object라 다운 캐스팅 해야함 User sessionUser = (User) session.getAttribute("sessionUser"); //System.out.println("sessionUser: " + sessionUser); // 2. 페이지 주인 여부 체크(board의 userId와 sessionId의 값 비교) boolean pageOwner = false; if (reponseDTO.getUserId() == sessionUser.getId()) { //System.out.println("getUserId:" + reponseDTO.getUserId()); pageOwner = true; } request.setAttribute("board", reponseDTO); request.setAttribute("pageOwner", pageOwner); // 이 값을 mustache에게 줘야함! return "board/detail"; } @GetMapping("/board/saveForm") // /board/saveForm Get요청이 옴 public String saveForm() { // session 영역에 접근하기 위한 // 1. session 영역에 sessionUser 키 값에 user 객체가 있는지 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); // 2. 값이 null이면 로그인 페이지로 리다이렉션 // if (sessionUser == null) { // return "redirect:/loginForm"; // } // 3. null이 아니면 /board/saveForm으로 이동 return "board/saveForm"; } @PostMapping("/board/save") public String save(BoardRequest.SaveDTO requestDTO, HttpServletRequest request) { // 1. 인증 체크 User sessionUser = (User) session.getAttribute("sessionUser"); // System.out.println("sessionUser:" + sessionUser); // if (sessionUser == null) { // return "redirect:/loginForm"; // } // 2. 바디 데이터 확인 및 유효성 검사 System.out.println(requestDTO); if (requestDTO.getTitle().length() > 30) { request.setAttribute("status", 400); request.setAttribute("msg", "title의 길이가 30자를 초과해서는 안되요"); return "error/40x"; // BadRequest } // 3. 모델 위임 // insert into board_tb(title, content, user_id, created_at) values(?,?,?, now()); boardRepository.save(requestDTO, sessionUser.getId()); return "redirect:/"; } @GetMapping("/board/{id}/updateForm") // 보드에 해당 페이지 public String updateFormn(@PathVariable int id, HttpServletRequest request) { // 인증 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); // if (sessionUser == null) { // return "redirect:/loginForm"; // } // 권한 체크하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { request.setAttribute("status", 403); request.setAttribute("msg", "게시글을 수정할 권한이 없습니다"); return "error/40x"; // 리다이렉트 하면 데이터 사라지니까 하면 안됨 } // 가방에 담기 request.setAttribute("board", board); return "board/updateForm"; } @PostMapping("/board/{id}/update") public String update(@PathVariable int id, BoardRequest.UpdateDTO requestDTO) { // System.out.println(requestDTO); 정보 받기 확인 // 인증 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); // if (sessionUser == null) { // return "redirect:/loginForm"; // } // 권한 체크하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { return "error/403"; } // update board_tb set title =?, content =?, where id =? boardRepository.update(requestDTO, id); return "redirect:/board/" + id; // 수정한 게시글로 돌아가기 } @PostMapping("/board/{id}/delete") // body데이터가 없어서 유효성 검사 안해도 됨 public String delete(@PathVariable int id) { // 1. 인증 검사하기 User sessionUser = (User) session.getAttribute("sessionUser"); // if (sessionUser == null) { // return "redirect:/loginForm"; // } // 2. 권한 검사하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { return "error/403"; } boardRepository.deleteById(id); return "redirect:/"; } }
notion image
notion image
 
  • 로그인 없이 바로 /에 접근하면 nullPointException이 터짐!
package shop.mtcoding.blog._core.config.security; import jakarta.servlet.http.HttpSession; import lombok.AllArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import shop.mtcoding.blog.user.User; import shop.mtcoding.blog.user.UserRepository; /* * 조건 * post 요청 * "/login"요청 * x-www-form-urlencoded * 키값이 username, password*/ @AllArgsConstructor @Service public class MyLoginService implements UserDetailsService { private final UserRepository userRepository; private final HttpSession session; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("loadUserByUsername: " + username); User user = userRepository.findByUsername(username); if (user == null) { System.out.println("user는 null"); return null; // 로그인 진행하던걸 취소하고 알아서 응답해줌 -> 반환할 페이지를 알려줘야함 } else { System.out.println("user를 찾았어요"); session.setAttribute ("sessionUser", user); // 머스태치에서만 가져오기 return new MyLoginUser(user); // securityContextHolder에 저장됨 } } }
 

6. mustache에서 사용 가능한 session 정보 저장하기

  • 서랍마다 특정 키값이 “SPRING_SECURITY_CONTEXT”
  • 장점
 
  • 단점 : mustache에서 당겨오는게 복잡함 → session을 추가로 만들기
로그아웃 시 다 날려버리면 됨
package shop.mtcoding.blog._core.config.security; import jakarta.servlet.http.HttpSession; import lombok.AllArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import shop.mtcoding.blog.user.User; import shop.mtcoding.blog.user.UserRepository; /* * 조건 * post 요청 * "/login"요청 * x-www-form-urlencoded * 키값이 username, password*/ @AllArgsConstructor @Service public class MyLoginService implements UserDetailsService { private final UserRepository userRepository; private final HttpSession session; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("loadUserByUsername: " + username); User user = userRepository.findByUsername(username); if (user == null) { System.out.println("user는 null"); return null; // 로그인 진행하던걸 취소하고 알아서 응답해줌 -> 반환할 페이지를 알려줘야함 } else { System.out.println("user를 찾았어요"); session.setAttribute ("sessionUser", user); // 머스태치에서만 가져오기 return new MyLoginUser(user); // securityContextHolder에 저장됨 } } }
 

7. UserController에서 수정하기

package shop.mtcoding.blog.user; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.AllArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import shop.mtcoding.blog._core.config.security.MyLoginUser; import shop.mtcoding.blog.board.Board; import shop.mtcoding.blog.board.BoardRequest; @AllArgsConstructor @Controller public class UserController { // fianl 변수는 반드시 초기화 되어야 함 private final UserRepository userRepository; // null private final HttpSession session; @GetMapping("/loginForm") // view만 원함 public String loginForm() { return "user/loginForm"; } @GetMapping("/joinForm") // view만 원함 public String joinForm() { return "user/joinForm"; } @PostMapping("/join") public String join(UserRequest.JoinDTO requestDTO) { System.out.println(requestDTO); // 1. 유효성 검사 if (requestDTO.getUsername().length() < 3) { return "error/400"; } userRepository.save(requestDTO); // 모델에 위임하기 return "redirect:/loginForm"; //리다이렉션불러놓은게 있어서 다시부른거 } @GetMapping("/user/updateForm") public String updateForm(HttpServletRequest request, @AuthenticationPrincipal MyLoginUser myLoginUser) { User user = userRepository.findByUsername(myLoginUser.getUsername()); request.setAttribute("user", user); return "/user/updateForm"; } @PostMapping("/user/update") public String updateUser(UserRequest.UpdateDTO requestDTO, HttpServletRequest request) { // 세션에서 사용자 정보 가져오기 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { return "redirect:/loginForm"; // 로그인 페이지로 리다이렉트 } // 비밀번호 업데이트 userRepository.userUpdate(requestDTO, sessionUser.getId()); session.setAttribute("sessionUser", sessionUser); return "redirect:/"; // 홈 페이지로 리다이렉트 } @GetMapping("/logout") public String logout() { // 1번 서랍에 있는 uset를 삭제해야 로그아웃이 됨 session.invalidate(); // 서랍의 내용 삭제 return "redirect:/"; } }
 

8. BoardController에서 수정하기

package shop.mtcoding.blog.board; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog._core.config.security.MyLoginUser; import shop.mtcoding.blog.user.User; import java.util.List; @RequiredArgsConstructor @Controller public class BoardController { private final HttpSession session; // DI private final BoardRepository boardRepository; // DI // http://localhost:8080?page=0 @GetMapping({"/"}) public String index(HttpServletRequest request) { List<Board> boardList = boardRepository.findAll(); request.setAttribute("boardList", boardList); return "index"; } // 상세보기시 호출 @GetMapping("/board/{id}") // 1이 프라이머리키 -> 뭐든 넣어도 실행시키려면 변수화시켜서 {} public String detail(@PathVariable int id, HttpServletRequest request, @AuthenticationPrincipal MyLoginUser myLoginUser) { System.out.println("id : " + id); // 1. 바로 모델 진입 -> 상세보기 데이터 가져오기 // body 데이터가 없으면 유효성 검사할 필요 없음 BoardResponse.DetailDTO reponseDTO = boardRepository.findByIdWithUser(id); //메서드 이름 변경 // 2. 페이지 주인 여부 체크(board의 userId와 sessionId의 값 비교) boolean pageOwner = false; if(myLoginUser != null){ if (reponseDTO.getUserId() == myLoginUser.getUser().getId()) { //System.out.println("getUserId:" + reponseDTO.getUserId()); pageOwner = true; } } request.setAttribute("board", reponseDTO); request.setAttribute("pageOwner", pageOwner); // 이 값을 mustache에게 줘야함! return "board/detail"; } @GetMapping("/board/saveForm") // /board/saveForm Get요청이 옴 public String saveForm() { return "board/saveForm"; } @PostMapping("/board/save") public String save(BoardRequest.SaveDTO requestDTO, HttpServletRequest request, @AuthenticationPrincipal MyLoginUser myLoginUser) { // 2. 바디 데이터 확인 및 유효성 검사 System.out.println(requestDTO); if (requestDTO.getTitle().length() > 30) { request.setAttribute("status", 400); request.setAttribute("msg", "title의 길이가 30자를 초과해서는 안되요"); return "error/40x"; // BadRequest } // 3. 모델 위임 // insert into board_tb(title, content, user_id, created_at) values(?,?,?, now()); boardRepository.save(requestDTO, myLoginUser.getUser().getId()); return "redirect:/"; } @GetMapping("/board/{id}/updateForm") // 보드에 해당 페이지 public String updateFormn(@PathVariable int id, HttpServletRequest request, @AuthenticationPrincipal MyLoginUser myLoginUser) { // 권한 체크하기 Board board = boardRepository.findById(id); if (board.getUserId() != myLoginUser.getUser().getId()) { request.setAttribute("status", 403); request.setAttribute("msg", "게시글을 수정할 권한이 없습니다"); return "error/40x"; // 리다이렉트 하면 데이터 사라지니까 하면 안됨 } // 가방에 담기 request.setAttribute("board", board); return "board/updateForm"; } @PostMapping("/board/{id}/update") public String update(@PathVariable int id, BoardRequest.UpdateDTO requestDTO, @AuthenticationPrincipal MyLoginUser myLoginUser) { // System.out.println(requestDTO); 정보 받기 확인 // 권한 체크하기 Board board = boardRepository.findById(id); if (board.getUserId() != myLoginUser.getUser().getId()) { return "error/403"; } // update board_tb set title =?, content =?, where id =? boardRepository.update(requestDTO, id); return "redirect:/board/" + id; // 수정한 게시글로 돌아가기 } @PostMapping("/board/{id}/delete") // body데이터가 없어서 유효성 검사 안해도 됨 public String delete(@PathVariable int id) { // 1. 인증 검사하기 User sessionUser = (User) session.getAttribute("sessionUser"); // if (sessionUser == null) { // return "redirect:/loginForm"; // } // 2. 권한 검사하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { return "error/403"; } boardRepository.deleteById(id); return "redirect:/"; } }
  • 글쓰기 하려다가 로그인 화면으로 이동하면
로그인 후 바로 인덱스 페이지가 아니라 글쓰기 화면으로 바로 이동시켜 줌
 
  • 회원가입시 비밀번호는 encod해서 넣어야 함!
  • 내가 bean으로 등록해서 리턴되는 값이 IoC에 등록됨
→ security가 로그인할 때 어떤 hash로 로그인해야 하는지 알 수 있음
 
 
Share article

Coding_study