v6
★ MILESTONE · BOARD

안전한 게시판

Update / Delete + 본인 확인

학습 목표

  • v5 의 위험 — 인가 부재 — 를 직접 체험한다
  • 수정·삭제에 본인 확인을 추가한다
  • created_at 컬럼을 ALTER ADD 로 합류시킨다
  • JSP 에서도 본인이 아닐 때 버튼을 숨긴다
  • 인증(Authentication) 과 인가(Authorization) 의 차이를 안다

⚠️ v5 의 두 가지 답답함

① 인가 부재 — 다른 사람의 글을 수정해본다
  1. user1 로 로그인 → 글 작성
  2. 로그아웃 → user2 로 로그인
  3. user1 의 글에 들어가 「수정」 클릭
  4. 내용 바꾸고 저장 → user2 가 user1 의 글을 마음대로 수정
② 정렬이 num 만으로 — 작성일이 없다

v5 의 myboard 에는 created_at 이 없어서 「언제 쓴 글인지」 화면에 못 보여줌. 정렬도 num 역순뿐.

🛠️ 첫 도구 — ALTER TABLE ADD COLUMN

DEFAULT CURRENT_TIMESTAMP

운영 중인 테이블에 컬럼을 추가하는 SQL. DEFAULT 절을 쓰면 기존 row 도 채워짐.


ALTER TABLE myboard
  ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP;

「기존 row 는 어떻게?」 — MySQL 이 ALTER 시점의 현재 시각을 자동으로 채워 넣습니다. 데이터 유실 없음.

Board 도메인 — createdAt 필드 합류


package com.smhrd.domain;

import java.time.LocalDateTime;
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@Data @AllArgsConstructor @NoArgsConstructor
public class Board {
    private int           num;
    private String        title;
    private String        writer;
    private String        content;
    private LocalDateTime createdAt;   // ← v6 추가
}

MyBatis 의 mapUnderscoreToCamelCase 설정이 created_atcreatedAt 자동 매핑.

Mapper XML — SELECT 절에 created_at 합류


<mapper namespace="com.smhrd.mapper.BoardMapper">

  <select id="selectList" resultType="com.smhrd.domain.Board">
    SELECT num, title, writer, content, created_at
    FROM myboard
    ORDER BY num DESC
  </select>

  <select id="selectOne" parameterType="int"
          resultType="com.smhrd.domain.Board">
    SELECT num, title, writer, content, created_at
    FROM myboard
    WHERE num = #{num}
  </select>

인증 vs 인가

인증 (Authentication) — "당신이 누구인가" └─ 로그인. 세션에 사용자 정보 저장 └─ v3 에서 완성 인가 (Authorization) — "당신이 이걸 할 수 있는가" └─ 권한 체크. 본인 글만 수정/삭제 └─ v6 (오늘) 에서 추가

👉 두 단어가 비슷해 보이지만 다른 단계. 인증 통과해도 인가는 별도.

🛠️ 두 번째 도구 — 본인 확인 패턴

「작성자 ID == 세션 사용자 ID?」

수정·삭제 요청이 들어왔을 때:

  1. 대상 글의 writer 를 DB 에서 조회
  2. 현재 세션의 사용자 id 와 비교
  3. 같으면 진행, 다르면 거부

Mapper — Update / Delete 추가


  <update id="update" parameterType="com.smhrd.domain.Board">
    UPDATE myboard
    SET title = #{title}, content = #{content}
    WHERE num = #{num}
  </update>

  <delete id="delete" parameterType="int">
    DELETE FROM myboard WHERE num = #{num}
  </delete>

</mapper>

Service — 본인 확인 헬퍼


@Service
public class BoardService {

    @Autowired
    private BoardMapper mapper;

    public Board selectOne(int num) {
        return mapper.selectOne(num);
    }

    // 본인 확인 헬퍼 — 화면(JSP) 에서도 사용
    public boolean isOwner(int num, String userId) {
        Board b = mapper.selectOne(num);
        return b != null && b.getWriter().equals(userId);
    }

    // ... 다음 슬라이드: update / delete 에서도 검증
}

Service — 본인 확인 적용


    // 본인 확인 후 수정 (서비스 레벨에서도 한 번 더)
    public void update(Board b, String userId) {
        Board original = mapper.selectOne(b.getNum());
        if (!original.getWriter().equals(userId)) {
            throw new SecurityException("작성자가 아닙니다");
        }
        mapper.update(b);
    }

    public void delete(int num, String userId) {
        Board original = mapper.selectOne(num);
        if (!original.getWriter().equals(userId)) {
            throw new SecurityException("작성자가 아닙니다");
        }
        mapper.delete(num);
    }

왜 Service 와 Controller 모두에서?

Service 에서 한 번 더 체크하는 이유:

  • Controller 만 체크 → 다른 Controller 에서 호출 시 보호 안 됨
  • Service 에서도 체크 → 어디서 호출해도 안전
  • 이중 방어 가 보안의 기본

학생용 버전은 단순화해서 Controller 에서만 해도 OK. 이중은 권장.

Controller — 수정 요청


@PostMapping("/board/update")
public String update(Board b,
                     HttpSession session,
                     RedirectAttributes ra) {

    Member user = (Member) session.getAttribute("loginUser");
    if (user == null) {
        return "redirect:/login";
    }

    if (!service.isOwner(b.getNum(), user.getId())) {
        ra.addFlashAttribute("err", "작성자만 수정 가능합니다");
        return "redirect:/board/view?num=" + b.getNum();
    }

    service.update(b, user.getId());
    return "redirect:/board/view?num=" + b.getNum();
}

Controller — 삭제 요청


@PostMapping("/board/delete")
public String delete(@RequestParam int num,
                     HttpSession session,
                     RedirectAttributes ra) {

    Member user = (Member) session.getAttribute("loginUser");
    if (user == null) {
        return "redirect:/login";
    }

    if (!service.isOwner(num, user.getId())) {
        ra.addFlashAttribute("err", "작성자만 삭제 가능합니다");
        return "redirect:/board/view?num=" + num;
    }

    service.delete(num, user.getId());
    return "redirect:/board/list";
}

JSP — 본인일 때만 버튼 표시


<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<h1>${board.title}</h1>
<p>작성자: ${board.writer} | 작성일: ${board.createdAt}</p>
<div>${board.content}</div>

<c:if test="${board.writer == sessionScope.loginUser.id}">
    <div>
        <a href="/board/edit?num=${board.num}">수정</a>
        <form action="/board/delete" method="post" style="display:inline;">
            <input type="hidden" name="num" value="${board.num}" />
            <button onclick="return confirm('정말 삭제?')">삭제</button>
        </form>
    </div>
</c:if>

<c:if test="${not empty err}">
    <p style="color:red;">${err}</p>
</c:if>

UI / 서버 이중 방어

중요

JSP 의 c:if 로 버튼을 숨겨도 — 그게 보안이 아닙니다.

  • 버튼 숨김 = UI 편의 (실수로 누르지 않게)
  • 실제 보안 = 서버에서의 본인 확인
  • 해커는 버튼 없이도 직접 POST 요청을 보낼 수 있음

UI 는 친절, 서버는 엄격. 둘 다 필요하지만 서버가 진짜 보안.

v5 → v6 핵심 차이

v5 — 인가 없음

@PostMapping("/update")
public String update(Board b) {
    service.update(b);
    return "redirect:/board/view";
}
// 누구나 수정 가능
v6 — 본인 확인

@PostMapping("/update")
public String update(Board b,
        HttpSession session) {
    Member u = (Member)
       session.getAttribute("loginUser");
    if (!service.isOwner(
            b.getNum(), u.getId())) {
        return "redirect:/board/view";
    }
    service.update(b, u.getId());
    return "redirect:/board/view";
}

본인 확인 흐름도

POST /board/update?num=3 ↓ Controller : 세션 확인 ├─ 비로그인 → /login 으로 리다이렉트 ↓ 로그인됨 Service.isOwner(num=3, userId="user1") ↓ Mapper.selectOne(3) → DB ↓ Board 의 writer 확인 │ ├─ writer.equals("user1") → 본인. 수정 진행 └─ 그 외 → 거부. err 메시지 + 리다이렉트

실험 — 본인 확인 깨뜨리기

실험결과
본인 확인 빼고 다른 사람 글 수정 시도v5 처럼 동작 (위험)
F12 로 hidden num 값 변조 후 제출서버가 본인 확인 → err 메시지
Postman 으로 직접 POST 보내기UI 우회 시도 → 서버에서 차단

👉 「UI 만 잘 만들면 보안된다」는 착각을 깨는 실험.

발전 가능성

  • 관리자 권한 — 관리자는 누구의 글이든 수정 가능 (역할 기반)
  • 그룹 권한 — 같은 그룹 회원만 수정 가능
  • 댓글 권한 — 본인 댓글만 삭제

👉 인가 패턴은 게시판을 넘어 모든 보안 결정의 토대.

v6 의 의미

오늘이 우리 프로젝트의 여덟 번째 마일스톤:

  • v0~v4 — 인증 + 인터셉터까지 완성
  • v5 — 최소 게시판 (위험)
  • v6 — 안전한 게시판 (오늘)
  • v7 — 페이징
  • v8~v∞ — REST API

v6 부터 우리 게시판은 실제 운영 가능합니다.

🔄 Before / After

v5 끝

동작하지만 위험. 누구나 누구의 글이든 수정·삭제 가능.

v6 끝

본인 글만 수정·삭제. UI 와 서버 이중 방어. 인증과 인가의 차이를 코드로 안다.

이번 차시의 데이터 흐름

수정 요청
Controller
isOwner 체크
Service.update
myboard + created_at
인가 게이트 + ALTER 로 합류한 created_at 컬럼

정리

오늘 들고 가는 것

  • 인증 = 누구인가 / 인가 = 무엇을 할 수 있는가
  • ALTER TABLE myboard ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  • writer.equals(userId) 비교가 본인 확인의 핵심
  • Service 와 Controller 이중 체크
  • JSP 의 c:if 는 UI 편의일 뿐. 진짜 보안은 서버
  • v6 — 실제 운영 가능한 게시판

다음: v7 페이징 게시판 (보강) → Part 6 REST API.