REST(REpresentational State Transfer) : 자원의 상태 전달
네트워크 아키텍처이다.
RestfulAPI 주소 설계 규칙이다.
https://chinggin.tistory.com/454
PostController와 UserController의 주소 설계를 고쳐주자.
package site.metacoding.dbproject.web;
import java.util.Optional;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import site.metacoding.dbproject.domain.user.User;
import site.metacoding.dbproject.domain.user.UserRepository;
@Controller
public class UserController {
// 컴포지션(의존성 연결) : 컨트롤러는 레파지토리에 의존해야해!
private UserRepository userRepository;
private HttpSession session;
// DI 받는 코드!!
public UserController(UserRepository userRepository, HttpSession session) {
this.userRepository = userRepository;
this.session = session;
}
// 회원가입 페이지 (정적) - 인증(로그인) X
@GetMapping("/joinForm")
public String joinForm() {
return "user/joinForm";
}
// username=ssar&password=1234&email=ssar@nate.com (x-www 타입)
// 회원가입 INSERT - 인증(로그인) X
@PostMapping("/join")
public String join(User user) { // 행위, 페이지 아님
// 1. username, password, email null 체크 required 속성 걸기(프론트 검사) -> 백엔드 검사
// username=ssar&email=ssar@nate.com 키 자체가 안들어오는게 null -> password null
// username=ssar&password=&email=ssar@nate.com 패스워드 null
// null 체크 -> 공백 체크 (순서 중요)
if (user.getUsername() == null || user.getPassword() == null || user.getEmail() == null) {
// 통신하다보면 물리적인 일이 날 수 있음 패킷이 유실되거나
return "redirect:/joinForm"; // -> 새로고침하면 적은 데이터 다날아감, 뒤로가기 해줘야해(자바스크립트로)
}
if (user.getUsername().equals("") || user.getPassword().equals("") || user.getEmail().equals("")) {
// 통신하다보면 물리적인 일이 날 수 있음 패킷이 유실되거나
return "redirect:/joinForm";
}
System.out.println("user : " + user);
// 2. 핵심로직
User userEntity = userRepository.save(user);
System.out.println("userEntity : " + userEntity);
// redirect는 GetMapping 주소!! redirect:매핑주소
return "redirect:/loginForm"; // 로그인페이지 이동해주는 컨트롤러 메서드를 재활용
}
// 로그인 페이지 (정적) - 인증(로그인) X
@GetMapping("/loginForm") // 브라우저가 쿠키를 가지고있으면 자동 전송함, 브라우저만!
public String loginForm(HttpServletRequest request, Model model) {
// request.getHeader("Cookie");
Cookie[] cookies = request.getCookies(); // 파싱해서 배열로 리턴해줌 jSessionId, remember 두개가 있음
for (Cookie cookie : cookies) {
System.out.println("쿠키값 : " + cookie.getName());
if (cookie.getName().equals("remember")) { // getName 키
model.addAttribute("remember", cookie.getValue()); // getValue 값
}
}
return "user/loginForm";
}
// 로그인 SELECT * FROM user WHERE username=? AND password=? -> 이런 메서드는 없으니까 직접
// 만들어!
// 원래 SELECT는 무조건 GET요청
// 근데 로그인만 예외! POST요청
// 이유 : 주소에 패스워드를 남길 수 없으니까!! 보안을 위해!!
// 로그인 - 인증(로그인) X
@PostMapping("/login")
public String login(User user, HttpServletResponse response) {
// HttpSession session = request.getSession(); // 쿠키에 JSESSIONID를 85로 가져오면
// session의 자기 공간을 가리킴
// 1. DB연결해서 username, password 있는지 확인
User userEntity = userRepository.mLogin(user.getUsername(), user.getPassword());
// 2. 있으면 session 영역에 인증됨이라고 메시지 하나 넣어두자
if (userEntity == null) {
System.out.println("아이디 혹은 패스워드가 틀렸습니다.");
} else {
System.out.println("로그인 되었습니다.");
// 세션에 옮겨담자, request는 사라졌지만 세션영역에 보관
session.setAttribute("principal", userEntity); // principal 인증된 주체 -> 로그인
if (user.getRemember() != null && user.getRemember().equals("on")) {
// F12 Application Cookies 프로토콜이라서 저장한거임!! redirection과 상관 없음 ! 브라우저가 저장시킨다
response.addHeader("Set-Cookie", "remember=" + userEntity.getUsername()); // 프로토콜에 없는 http 헤더 키값 만들어낸것
// response.addHeader("Set-Cookie", "hi=hihihihi;"); // 프로토콜에 없는 http 헤더 키값
// 만들어낸것
// response.addCookie(cookie); // 프로토콜에 없는 http 헤더 키값 만들어낸것
// response.setHeader("hello", "안녕");
// F12 Network Header responseheader에 남는데 얘는 redirect되어서 request 사라짐
}
}
return "redirect:/";
}
// 로그아웃 - 인증(로그인) O
@GetMapping("/logout")
public String logout() {
session.invalidate(); // 해당 JSESSIONID 영역 전체 날리기 -> 이게 로그아웃
// session.removeAttribute("principal"); // 해당 JSESSIONID 영역의 principal 키값만 날아가는
// 것
return "redirect:/loginForm"; // PostController 만들고 수정하자
}
// http://localhost:8080/user/1
// 유저 상세 페이지 (동적 -> DB연동 필요) - 인증(로그인) O
@GetMapping("/s/user/{id}")
public String detail(@PathVariable int id, Model model) {
// 유효성 검사 하기(수십개... 엄청 많겠지?)
User principal = (User) session.getAttribute("principal");
// 1. 인증 체크 (로그인하지 않고 주소로 접근 막기)
if (principal == null) {
return "error/page1";
}
// 2. 권한 체크
if (principal.getId() != id) {
return "error/page1";
}
Optional<User> userOp = userRepository.findById(id); // 유저정보
// 3. 핵심 로직
if (userOp.isPresent()) { // 박스안에 뭐가 있으면
User userEntity = userOp.get();
model.addAttribute("user", userEntity);
return "user/detail";
} else { // 없으면 == isEmpty
// 누군가 고의로 DELETE 하지 않는 이상 거의 타지 않는 오류
return "error/page1";
}
// DB에 로그 남기기 (로그인 한 아이디도 남기기)
// Heidi SQL에서도 남겨야해 근데 디비는 알아서 로그가 남음
}
// 유저 수정 페이지 - 인증(로그인) O
@GetMapping("/s/user/updateForm")
public String updateForm() {
return "user/updateForm";
}
// 유저 수정 - 인증(로그인) O
@PutMapping("/s/user/{id}")
public String update(@PathVariable int id) {
return "redirect:/user/" + id;
}
}
유저 컨트롤러에는 현재
로그인폼, 로그인, 회원가입 폼, 회원가입, 로그아웃 => 인증이 필요 없는 페이지
유저 상세 페이지, 유저 수정 폼, 유저 수정 => 인증이 필요한 페이지
7개의 메서드가 만들어져 있다.
원래는 인증이 필요한 컨트롤러인 AuthController와
필요하지 않은 컨트롤러인 UserController로 나누어야 한다.
나중에 정리해주자!
유저 컨트롤러에서 민감한 정보(개인 정보)에 접근하는 페이지에는
/user가 붙어있다.
필터링 하기 위해 만들어 놓은 규칙이다.
/user/* 의 주소로 접근하면 인증 체크가 필요한 유저임을
스프링 성의 문지기에게 알려주어 필터링하는 것이다.
문지기는 세션에 유저의 값이 있는지 확인하여 문을 열어준다.
필터링 과정은 나중에 만들어줄 것이다.
지금 우리는 메서드 마다 유효성 검사와 인증 체크를 해주었는데
이는 너무 비효율적인 방식이다.
공통 로직을 따로 만들어두어 문지기에게 필터링을 맡긴 후
주소 방식으로 알려주는게 가장 편하다.
"/user로 들어오면 인증 체크해!"
URL 요청의 고질적인 문제는
a파일, b파일, c파일, d파일을 직접 요청하니까
파일에 일일이 인증체크를 해줘야 한다.
이 과정이 귀찮기 때문에 FrontController를 만든다.
a와 b파일의 공통 로직을 프론트 컨트롤러에,
c와 d파일의 공통로직을 프론트 컨트롤러에,
이 프론트 컨트롤러들의 공통로직을 디스패쳐 서블릿에 만들어둔다.
우리는 이미 만들어져 있는 디스패쳐 서블릿에 접근할 수 없기 때문에
디스패쳐 서블릿보다 더 앞에 필터에서 이 성의 전역적인 공통 로직 처리를 해준다.
이 처리를 문지기가 해주는 것이다.
(성의 입구 : 필터!!)
package site.metacoding.dbproject.web;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import lombok.RequiredArgsConstructor;
import site.metacoding.dbproject.domain.post.Post;
import site.metacoding.dbproject.domain.post.PostRepository;
import site.metacoding.dbproject.domain.user.User;
@RequiredArgsConstructor // final이 붙은 애들에 대한 생성자를 만들어준다.
@Controller
public class PostController {
private final HttpSession session;
private final PostRepository postRepository;
// GET 글쓰기 페이지 /post/writeForm - 인증 O
@GetMapping("/s/post/writeForm")
public String writeForm() {
if (session.getAttribute("principal") == null) {
return "redirect:/loginForm";
}
return "post/writeForm";
}
// 메인 페이지 - 인증 X
// GET 글 목록 페이지 /post/list/
@GetMapping({ "/", "post/list" }) // { "/", "post/list" }로 쓰면 두 가지 방법으로 들어올 수 있음
public String list(Model model, Post post) {
List<Post> posts = new ArrayList<>();
// 1. postRepository의 findAll() 호출
posts = postRepository.findAll();
// 2. model에 담기
model.addAttribute("posts", posts);
// 3. mustache 파일에 뿌리기
return "post/list";
}
// 글 상세보기 페이지 /post/{id} (삭제버튼 만들어두면 되니까 삭제페이지 필요 X)
@GetMapping("/post/{id}") // Get요청에 /post 제외시키기(인증 X)
public String detail(@PathVariable Integer id, Model model) { // int는 null이 없음, 초기값이 0
// Integer는 초기값이 null
Optional<Post> postOp = postRepository.findById(id); // 유저정보
// 핵심 로직
if (postOp.isPresent()) { // 박스안에 뭐가 있으면
Post postEntity = postOp.get();
model.addAttribute("post", postEntity);
return "post/detail";
} else { // 없으면 == isEmpty
// 누군가 고의로 DELETE 하지 않는 이상 거의 타지 않는 오류
return "error/page1";
}
}
// 글 수정 페이지 /post/{id}/updateForm - 인증 O
@GetMapping("/s/post/{id}/updateForm")
public String updateForm(@PathVariable Integer id) {
return "post/updateForm"; // ViewResolver 도움 받음
}
// DELETE 글 삭제 /post/{id} -> 글 목록으로 가기 - 인증 O
@DeleteMapping("/s/post/{id}")
public String delete(@PathVariable Integer id) {
return "redirect:/";
}
// UPDATE 글 수정 /post/{id} -> 글 상세보기 페이지 가기 - 인증 O
@PutMapping("/s/post/{id}")
public String update(@PathVariable Integer id) {
return "redirect:/post/" + id;
}
// POST 글 쓰기 /post -> 글 목록으로 가기 - 인증 O
@PostMapping("/s/post")
public String post(Post post) {
// title, content null검사, 공백검사, 길이검사 ,,,,
if (session.getAttribute("principal") == null) {
return "redirect:/loginForm";
}
// id가 필요한데 오브젝트를 넣어도 될까? 된다 -> 알아서 id(PK)만 뽑아간다
// 오브젝트 안넣으면 FK연결 안된다
User principal = (User) session.getAttribute("principal");
post.setUser(principal);
// insert into post(title, content, userId)
// values(입력 받기, 입력 받기, 세션오브젝트의 PK)
postRepository.save(post);
return "redirect:/"; // 다시 컨트롤러의 메서드를 찾아가는 것
}
}
포스트 컨트롤러는 인증된 사람만 접근하는 곳이다.
글쓰기는 아무나 할 수 있는 건 아니니까!
보통 주소의 앞에 테이블 명이 먼저 나오는 게
주소 설계 규칙에 맞다.
동사(GET, POST, PUT, DELETE) : /테이블명
/post로 요청하는 주소는 전역적으로 필터링 처리를 할 건데
글 상세 페이지를 볼 때
로그인(인증) 안 하면 글을 못 보게 만들면 안 된다.
원래 제일 앞에 테이블명이 오는 게 RestfulAPI 규칙에는 맞지만
그럼 어떻게 바꿔야 할까?
/post로 들어오는 규칙 중에 get요청만 빼고 인증 처리하라고
필터링을 해주면 되겠다?
/post로 들어오면 다 인증이 필요해
근데 /post요청 중에 GET 요청은 통과시켜줘
근데 /post/{id}/updateForm만 예외니까 Get이어도 인증 필요해!!
점점 필터링 해야할게 많아지면 코드가 지저분해진다.
간단하게 인증의 경로를 추가해주면 편해지겠다.
제일 앞에 테이블 이름이 나오지 않아서 주소 설계 규칙과 맞지 않지만
규칙은 참고만 할 뿐 반드시 지킬 필요는 없다.
프로토콜은 아니니까!
문지기에게 /s가 붙어있으면 인증 체크가 필요하다고 알려주면 된다!
(/secure의 약자)
이런 주소 설계에 정답은 없다.
내가 만든 주소 설계에 반대하는 사람에게
더 좋은 방법이 있다면 그에 따르면 된다!
[출처]
https://cafe.naver.com/metacoding
메타 코딩 유튜브
https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9