
- 단방향 암호화 : 복구화가 안됨
- 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