11-1 쿠키 · 세션 · 인증 · 권한 체크

박은서's avatar
Feb 06, 2026
11-1 쿠키 · 세션 · 인증 · 권한 체크

1. 쿠키와 세션

  • 저장 위치 : 브라우저(클라이언트)
  • 정체 : 그냥 문자열 저장소
  • 특징
    • 매 요청마다 자동으로 서버에 같이 전송됨
    • 보안에 취약 → 민감한 정보 저장 ❌
  • 주 용도
    • 서버가 발급한 세션 ID(키) 저장
➡️ 쿠키 자체는 인증도 아니고 권한도 아님
➡️ “서버가 나를 식별하기 위한 표식” 정도
notion image

2️⃣ 세션 (Session)

  • 저장 위치: 서버 메모리 (또는 Redis 같은 서버 저장소)
  • 정체: Map<세션ID, 값>
  • 특징
    • 클라이언트는 세션 내용을 절대 모름
    • 서버만 접근 가능
  • 주 용도
    • 로그인 상태 유지
    • 사용자 정보 저장
➡️ 세션은 “서버 쪽 상태(State)”

2. 인증(Authentication) vs 권한 체크(Authorization)

1️⃣ 🔐 인증 (Authentication)

1) 인증이란?

“너 로그인 한 사람 맞아?”
  • 기준 : 세션이 존재하느냐
  • 극단적으로 말하면
    • session.setAttribute("anything", true);
      이것만 있어도 인증은 됨
📌 인증은 값의 의미는 중요하지 않음
  • 세션에 값이 있다 → 인증됨
  • 누군지는 몰라도 “로그인된 사용자”임은 알 수 있음

2) 실습

package com.example.boardv1.board; import java.util.List; import org.springframework.stereotype.Controller; 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.ResponseBody; import com.example.boardv1.reply.ReplyRequest; import com.example.boardv1.user.User; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor // final이 적혀있는 애로 생성자 만들어줌 @Controller // @Controller 적어야 리턴값이 파일이 됨 (외부진입점) public class BoardController { private final BoardService boardService; private final HttpSession session; // title : title=title7&content=content7 (x-www-form) @PostMapping("/boards/save") public String save(BoardRequest.SaveOrUpdateDTO reqDTO){ // new해서 넣어줌! 필드가 많을 때 상태만 추가하면 되기 때문에 매우 편함! 재사용 가능!(필드명 잘 적어야 함) // 인증(v) 권한(x) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new RuntimeException("인증되지 않았습니다."); boardService.게시글쓰기(reqDTO.getTitle(), reqDTO.getContent(), sessionUser.getId()); return "redirect:/"; } // body : title=제목&content=내용 @PostMapping("/boards/{id}/update") public String update(@PathVariable("id") int id, BoardRequest.SaveOrUpdateDTO reqDTO){ // 인증(v) 권한(v) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new RuntimeException("인증되지 않았습니다."); boardService.게시글수정(id,reqDTO.getTitle(), reqDTO.getContent(), sessionUser.getId()); return "redirect:/boards/"+id; } @GetMapping("/") public String index(HttpServletRequest req){ List<Board> list = boardService.게시글목록(); req.setAttribute("models", list); return "index"; } @GetMapping("/boards/save-form") public String saveForm(){ // 인증(v) 권한(x) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new RuntimeException("인증되지 않았습니다."); return "board/save-form"; } @GetMapping("/boards/{id}/update-form") public String updateForm(@PathVariable("id") int id, HttpServletRequest req){ // 인증(v) 권한(v) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new RuntimeException("인증되지 않았습니다."); Board board = boardService.수정폼게시글정보(id, sessionUser.getId()); req.setAttribute("model", board); return "board/update-form"; } @GetMapping("/boards/{id}") public String detail(@PathVariable("id") int id, HttpServletRequest req){ User sessionUser = (User) session.getAttribute("sessionUser"); // Object타입으로 반환하기 때문에 다운캐스팅해야 함 Integer sessionUserId = sessionUser == null ? null : sessionUser.getId(); BoardResponse.DetailDTO dto = boardService.상세보기(id, sessionUserId); req.setAttribute("model", dto); return "board/detail"; // mustache 파일의 경로 } @PostMapping("/boards/{id}/delete") public String delete(@PathVariable("id") int id){ // 인증(v) 권한(v) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new RuntimeException("인증되지 않았습니다."); boardService.게시글삭제(id, sessionUser.getId()); return "redirect:/"; } @GetMapping("/api/boards/{id}") public @ResponseBody BoardResponse.DetailDTO apiDetail(@PathVariable("id") int id) { User sessionUser = (User) session.getAttribute("sessionUser"); Integer sessionUserId = sessionUser == null ? null : sessionUser.getId(); BoardResponse.DetailDTO dto = boardService.상세보기(id, sessionUserId); return dto; } }

2️⃣ 🛂 권한 체크 (Authorization)

1) 권한 체크란?

“너 이 작업 해도 돼?”
  • 기준 : 세션 안의 정보
  • 예 :
    • session.setAttribute("userId", 3); session.setAttribute("role", "ADMIN");
📌 권한 체크는 세션에 ‘의미 있는 정보’가 있어야 가능
  • userId → DB 조회 후 권한 확인 가능
  • role → 바로 권한 판단 가능
➡️ 그래서
“아무 값이나 세션에 넣으면 안 된다”
는 말이 나오는 것

2) 실습

① 서버에서 차단
package com.example.boardv1.board; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.example.boardv1.reply.Reply; import com.example.boardv1.user.User; import com.example.boardv1.user.UserRepository; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; // 책임 : 트랜잭션 관리 + DTO 만들기 + 권한 체크(DB정보가 필요하기 때문) @RequiredArgsConstructor @Service public class BoardService { private final BoardRepository boardRepository; private final EntityManager em; public List<Board> 게시글목록() { return boardRepository.findAll(); } public Board 수정폼게시글정보(int id, int sessionUserId) { Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요")); // 권한 if (sessionUserId != board.getUser().getId()) throw new RuntimeException("수정할 권한이 없습니다"); return board; } public BoardResponse.DetailDTO 상세보기(int id, Integer sessionUserId) { Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요")); return new BoardResponse.DetailDTO(board, sessionUserId); } @Transactional // update, delete, insert 할때 붙이세요!! public void 게시글수정(int id, String title, String content, int sessionUserId) { Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("수정할 게시글을 찾을 수 없어요")); // 권한 if (sessionUserId != board.getUser().getId()) throw new RuntimeException("수정할 권한이 없습니다"); board.setTitle(title); board.setContent(content); } // 원자성(모든게 다되면 commit, 하나라도 실패하면 rollback) // 트랜잭션 종료시 flush 됨. @Transactional public void 게시글쓰기(String title, String content, int sessionUserId) { User user = em.getReference(User.class, sessionUserId); // 1. 비영속 객체 Board board = new Board(); board.setTitle(title); board.setContent(content); board.setUser(user); System.out.println("before persist " + board.getId()); // 2. persist boardRepository.save(board); System.out.println("after persist " + board.getId()); } @Transactional public void 게시글삭제(int id, int sessionUserId) { // 영속화 Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("삭제할 게시글을 찾을 수 없어요")); // 권한 if (sessionUserId != board.getUser().getId()) throw new RuntimeException("삭제할 권한이 없습니다"); boardRepository.delete(board); } // 자동 flush }
② 프론트엔드에서 차단(수정/삭제버튼 숨김)
package com.example.boardv1.board; import lombok.Data; public class BoardResponse { @Data public static class DetailDTO { // 화면에 보이지 않는 것 (PK는 화면에 안보여도 무조건 적어야 함!) private int id; private int userId; // 화면에 보이는 것 private String title; private String contetn; private String username; // 연산해서 만들어야 되는 것 private boolean isOwner; // 게시글의 주인인가? public DetailDTO(Board board, Integer sessionUserId) { this.id = board.getId(); this.userId = board.getUser().getId(); this.title = board.getTitle(); this.contetn = board.getContent(); this.username = board.getUser().getUsername(); this.isOwner = board.getUser().getId() == sessionUserId; } } }
{{> header}} <div class="container p-5"> {{#model.isOwner}} <!-- 수정삭제버튼 --> <div class="d-flex justify-content-end"> <a href = "/boards/{{model.id}}/update-form" class="btn btn-secondary me-1">수정</a> <form action = "/boards/{{model.id}}/delete" method="post"> <button class="btn btn-outline-secondary">삭제</button> </form> </div> {{/model.isOwner}} <!-- 게시글내용 --> <div> <h2><b>{{model.title}}</b></h2> <hr /> <div class="d-flex justify-content-end"> 작성자 : {{model.username}} </div> <div class="m-4 p-2"> {{model.content}} </div> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form action="/replies/save" method="post"> <input type="hidden" name="boardId" value="{{model.id}}"> <textarea id="comment" class="form-control" rows="2" name="comment"></textarea> <div class="d-flex justify-content-end"> <button class="btn btn-secondary mt-1"> 댓글등록 </button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> {{#model.replies}} <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-secondary text-white rounded">{{replyUsername}}</div> <div>{{comment}}</div> </div> {{#isReplyOwner}} <form action="/replies/{{id}}/delete?boardId={{model.id}}" method="post"> <input type="hidden" name="boardId" value="{{model.id}}"> <button class="btn">🗑</button> </form> {{/isReplyOwner}} </div> {{/model.replies}} </div> </div> </div> {{^sessionUser}} <script> // 1. 조건 (querySelector 사용, textArea에 comment라는 id 하나 추가하기) const textArea = document.querySelector("#comment"); // 2. textArea에 마우스로 클릭했을 때의 이벤트 리스너가 필요함 textArea.addEventListener("click", function () { // 3. 이 때, 행위가 하나 필요함 -> 알림창을 띄워서 '로그인하세요'라고 알려줘야 함 alert("로그인하세요"); }); </script> {{/sessionUser}} </body> </html>
detail.mustache

3. request / session / cookie에 무엇을 넣나?

1️⃣ request

  • 수명: 요청 1번
  • 용도
    • 컨트롤러 → 서비스 → 뷰로 전달할 데이터
  • 특징
    • 요청 끝나면 즉시 사라짐
  • ❌ 로그인 정보 저장하면 안 됨

2️⃣ session

  • 수명: 브라우저 닫거나 만료 전까지
  • 넣는 것
    • userId
    • 사용자 역할(role)
    • 로그인 여부
  • 목적
    • 인증 유지
    • 권한 체크
  • 넣는 것
    • 세션 ID 하나
  • ❌ userId, role, password 절대 금지

4. 세션 기반 인증의 핵심 구조

1️⃣ 왜 세션을 쓰는가?

  • HTTP는 무상태(stateless)
  • 하지만 로그인은 상태(stateful) 가 필요
    • → 세션으로 상태를 서버에 저장

2️⃣ 서버가 여러 대일 때 문제

1) 문제점

서버 A에서 로그인 → 서버 B로 가면 세션 없음 ❌

2) 해결책

  1. Sticky Session
      • 항상 같은 서버로 요청
  1. 세션 동기화
      • 서버끼리 세션 복제
  1. 공유 세션 저장소
      • Redis (가장 일반적)

5. 최종 정리 (클라이언트 ↔ 서버 흐름)

🔁 로그인 흐름

1) 클라이언트

ID / PW 입력 → 로그인 요청

2) 서버

ID / PW 검증 세션 생성 세션에 사용자 정보 저장 - userId - role 세션 ID 발급

3) 서버 → 클라이언트

Set-Cookie: JSESSIONID=abc123

4) 브라우저

쿠키에 세션 ID 자동 저장

🔁 로그인 이후 요청 (글쓰기 등)

5) 클라이언트

요청 + 쿠키(JSESSIONID)

6) 서버

세션 ID로 세션 조회 ✔ 세션 있으면 → 인증 성공 ✔ 세션 값으로 → 권한 체크

브라우저에서 확인하기

notion image
notion image
notion image
notion image

6. 한 문장 요약

  • 쿠키: 세션 ID를 들고 다니는 운반책
  • 세션: 서버에 저장된 로그인 상태
  • 인증: 세션이 있냐 없냐
  • 권한 체크: 세션 안에 뭐가 들어 있냐
  • request: 잠깐 쓰고 버리는 데이터
Share article