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

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에 저장 → 이제 안 먹힐 것

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

- 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에 저장됨 } }


- 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:/"; } }


- 로그인 없이 바로 /에 접근하면 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