day38 - 0919
Interceptor를 이용하여 권한에 따른 게시판 접근 처리
1. 회원(관리자)만 게시글 작성, 수정, 삭제 페이지에 접근할 수 있도록
2. 공지 사항은 관리자만 작성하도록
3. 회원은 자기 자신이 쓴 게시글만 수정 및 삭제할 수 있도록
4. 관리자는 모든 게시글을 삭제할 수 있도록 (수정은 불가능)
1. 회원(관리자)만 게시글 작성, 수정, 삭제 페이지에 접근
InterceptorConfiguration 설정 추가
- 게시판 전체(/board/**)에 대한 접근을 제한한다
- 게시글 목록(/board/list)과 게시글 상세(/board/detail)에 대한 접근만 허용한다
@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {
// 의존성 주입
@Autowired
MemberInterceptor memberInterceptor;
// 추상 메소드 오버라이딩 - addInterceptors(InterceptorRegistry registry)
@Override
public void addInterceptors(InterceptorRegistry registry) {
// TestInterceptor에 대한 설정
registry.addInterceptor(testInterceptor)
.addPathPatterns("/**");
// MemberInterceptor에 대한 설정
registry.addInterceptor(memberInterceptor)
.addPathPatterns(
"/pocketmon/**", // 포켓몬 전부
"/music/detail", // 음원 상세
"/member/**", // 회원 전체
"/board/**" // 게시글 전체
)
.excludePathPatterns(
"/member/join", // 회원가입
"/member/join_success", // 회원 가입 완료
"/member/login", // 로그인
"/member/goodbye_result", // 탈퇴 완료
"/board/list", // 게시글 목록
"/board/detail" // 게시글 상세
);
}
}
list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix = "fmt" uri = "http://java.sun.com/jsp/jstl/fmt" %>
<%-- 오늘 날짜를 구해서 today라는 변수로 만든다 --%>
<jsp:useBean id = "now" class = "java.util.Date"></jsp:useBean>
<c:set var = "today">
<fmt:formatDate value = "${now}" pattern = "yyyy-MM-dd"/>
</c:set>
<jsp:include page = "/WEB-INF/views/template/header.jsp">
<jsp:param value = "게시글 목록" name = "title"/>
</jsp:include>
<div align = "center">
<h1>게시글 목록</h1>
<table border = "1" width = "900">
<thead>
<%-- 로그인 상태에서만 글쓰기 항목이 보이도록 설정 --%>
<c:if test = "${loginId != null}">
<tr>
<td align = "right" colspan = "5">
<a href = "write">글쓰기</a>
</td>
</tr>
</c:if>
<tr>
<th>번호</th>
<th width = "45%">제목</th>
<th>작성자</th>
<th>작성일</th>
<th>조회수</th>
</tr>
</thead>
<tbody align = "center">
<c:forEach var = "boardDto" items = "${list}">
<tr>
<td>${boardDto.boardNo}</td>
<td align = "left">
<%-- 말머리가 있을 경우에만 출력 --%>
<c:if test = "${boardDto.boardHead != null}">
[${boardDto.boardHead}]
</c:if>
<%-- 제목을 누르면 해당 게시글의 상세 페이지로 이동하도록 --%>
<a href = "detail?boardNo=${boardDto.boardNo}">
${boardDto.boardTitle}
</a>
</td>
<td>${boardDto.boardWriter}</td>
<td>
<%-- 작성 날짜를 current라는 변수로 만든다 --%>
<c:set var = "current">
<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "yyyy-MM-dd"/>
</c:set>
<c:choose>
<%-- today(오늘 날짜)와 current(작성 날짜)가 같다면 시간과 분만 표시--%>
<c:when test = "${today == current}">
<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "HH:mm"/>
</c:when>
<%-- 그렇지 않다면 년도-월-일로 표시 --%>
<c:otherwise>
<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "yyyy-MM-dd"/>
</c:otherwise>
</c:choose>
</td>
<td>${boardDto.boardRead}</td>
</tr>
</c:forEach>
</tbody>
<c:if test = "${loginId != null}">
<tfoot>
<tr>
<td align = "right" colspan = "5">
<a href = "write">글쓰기</a>
</td>
</tr>
</tfoot>
</c:if>
</table>
<%-- 페이지 네비게이터 --%>
<h3> « < 1 2 3 4 5 6 7 8 9 10 > » </h3>
<%--검색창 --%>
<form action = "list" method = "get">
<select name = "type" required>
<option value = "board_title" <c:if test = "${vo.type == 'board_title'}">selected</c:if>>제목</option>
<option value = "board_content" <c:if test = "${vo.type == 'board_content'}">selected</c:if>>내용</option>
<option value = "board_writer" <c:if test = "${vo.type == 'board_writer'}">selected</c:if>>작성자</option>
</select>
<input type = "search" name = "keyword" placeholder = "검색어" required value = "${vo.keyword}">
<button type = "submit">검색</button>
</form>
</div>
<jsp:include page = "/WEB-INF/views/template/footer.jsp"></jsp:include>
2. 공지 사항은 관리자만 작성하도록
- 게시글 작성 페이지(write.jsp)에서 HttpSession에 저장된 회원 등급(mg)이 관리자일 때만 '공지' 말머리 사용 가능
- 관리자가 아닌 회원이 공지 게시글을 작성하는 것을 차단하는 Interceptor 생성 및 설정 추가
write.jsp
- 로그인 중인 회원 등급(mg)이 관리자일 때만 '공지' 말머리 사용 가능
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<jsp:include page="/WEB-INF/views/template/header.jsp">
<jsp:param value="자유 게시판" name="title"/>
</jsp:include>
<h1>게시글 작성</h1>
<div align = "center">
<form action="write" method="post">
<table border="1" width="500">
<tbody>
<tr>
<th>말머리</th>
<td>
<select name="boardHead">
<option value="">선택</option>
<option>정보</option>
<option>유머</option>
<c:if test="${mg == '관리자'}">
<option>공지</option>
</c:if>
</select>
</td>
</tr>
<tr>
<th>제목</th>
<td>
<input type="text" name="boardTitle" required>
</td>
</tr>
<tr>
<th>내용</th>
<td>
<textarea name="boardContent" rows="10" cols="50" required></textarea>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td align="right" colspan="2">
<a href="list">목록으로</a>
<button type="submit">등록하기</button>
</td>
</tr>
</tfoot>
</table>
</form>
</div>
<jsp:include page="/WEB-INF/views/template/footer.jsp"></jsp:include>
MemberBoardPermissionCheckInterceptor 생성
- 회원의 게시판 접근을 감시하는 Interceptor
- 해당 Interceptor는 MemberInterceptor 후에 동작하므로 검사 대상은 오직 회원인 경우에 한한다
- 회원 중 관리자가 아닌 회원이 공지 사항을 작성하려는 경우 차단(false)
- 가장 먼저 POST 방식(게시글 작성)인지 검사 - POST 방식이 아니라면 통과(true)
1) 로그인 중인 회원 등급이 관리자인지 검사 - 관리자이면 통과(true)
2) 1)이 아닌 경우(관리자가 아닌 경우)에 대하여 boardHead의 값이 null이거나 '공지'이면 차단
@Component
public class MemberBoardPermissionCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 만약 POST 방식이 아니라면 통과
if(!request.getMethod().equals("POST")) {
return true; // POST 방식이 아니라면 통과(true)
}
// 준비
// request에서 HttpSession을 반환
HttpSession session = request.getSession();
// 1. 관리자인지 아닌지 검사 - 관리자면 통과
// HttpSession에서 저장된 회원 등급(mg)의 값을 반환
String memberGrade = (String) session.getAttribute("mg");
if(memberGrade.equals("관리자")) { // 만약 회원 등급이 관리자라면
return true; // 관리자이면 통과(true)
}
// 2. 1번이 아니라면 boardHead라는 파라미터 값이 "공지"이면 차단, 아니면 허용
String boardHead = request.getParameter("boardHead");
if(boardHead != null && !boardHead.equals("공지")) {
return true; // boardHead의 값이 null이 아니면서 "공지"가 아니면 통과(true)
}
// 그 외 나머지 경우는 차단
// response.sendRedirect(String location)
// response.sendError(int sc)
response.sendError(403);
//response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
}
InterceptorConfiguration 설정 - MemberBoardPermissionCheckInterceptor
@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {
// 의존성 주입
@Autowired
MemberBoardPermissionCheckInterceptor memberBoardPermissionCheckInterceptor;
// 추상 메소드 오버라이딩 - addInterceptors(InterceptorRegistry registry)
@Override
public void addInterceptors(InterceptorRegistry registry) {
// MemberBoardPermissionCheckInterceptor에 대한 설정
registry.addInterceptor(memberBoardPermissionCheckInterceptor)
.addPathPatterns(
"/board/edit", // 게시글 수정
"/board/write" // 게시글 작성
);
}
}
3. 회원은 자기 자신이 쓴 게시글만 수정 및 삭제할 수 있도록
detail.jsp
- 로그인 중인 회원의 아이디(loginId)의 값과 Controller에서 전달받은 boardDto의 작성자(boardWriter)를 비교
- loginId와 boardWriter의 값이 같은 경우만 게시글 수정, 삭제 메뉴 표시 (EL에서는 등호(==)로 문자열을 비교할 수 있다)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix = "fmt" uri = "http://java.sun.com/jsp/jstl/fmt" %>
<jsp:include page = "/WEB-INF/views/template/header.jsp">
<jsp:param value = "게시글 상세" name = "title"/>
</jsp:include>
<div align = "center">
<h1>게시글 상세</h1>
<table border = "1" width = "900">
<tbody>
<tr>
<th width = "25%">번호</th>
<td>${boardDto.boardNo}</td>
</tr>
<tr>
<th>말머리</th>
<td>${boardDto.boardHead}</td>
</tr>
<tr>
<th>제목</th>
<td>${boardDto.boardTitle}</td>
</tr>
<tr>
<th>작성자</th>
<td>${boardDto.boardWriter}</td>
</tr>
<tr height = "200" valign = "top">
<th>내용</th>
<td>
<%-- pre 태그는 엔터, 띄어쓰기, 탭키 등을 있는 그대로 표시하는 영역 --%>
<pre>${boardDto.boardContent}</pre>
</td>
</tr>
<tr>
<th>조회수</th>
<td>${boardDto.boardRead}</td>
</tr>
<tr>
<th>작성일</th>
<td>
<fmt:formatDate value = "${boardDto.boardWritetime}"
pattern = "y년 M월 d일 E요일 a h시 m분 s초"/>
</td>
</tr>
<%-- 한 번이라도 수정한 적이 있다면(수정 시간이 null이 아니라면) 수정 시간을 표시 --%>
<c:if test = "${boardDto.boardUpdatetime != null}">
<tr>
<th>수정일</th>
<td>
<fmt:formatDate value = "${boardDto.boardUpdatetime}"
pattern = "y년 M월 d일 E요일 a h시 m분 s초"/>
</td>
</tr>
</c:if>
</tbody>
<tfoot>
<tr>
<td colspan = "2" align = "right">
<%-- 로그인 상태에서만 게시글 작성이 가능하도록 설정 --%>
<c:if test = "${loginId != null}">
<a href = "write">글쓰기</a>
</c:if>
<%-- 로그인한 회원이 게시글 작성자인지에 대한 변수(boolean) 생성 --%>
<c:set var = "owner" value = "${loginId == boardDto.boardWriter}"></c:set>
<%-- 만약 로그인한 회원이 게시글 작성자라면 게시글을 수정할 수 있도록 설정--%>
<c:if test = "${owner}">
<a href = "edit?boardNo=${boardDto.boardNo}">수정</a>
</c:if>
<a href = "list">목록으로</a>
</td>
</tr>
</tfoot>
</table>
</div>
<jsp:include page = "/WEB-INF/views/template/footer.jsp"></jsp:include>
4. 관리자는 모든 게시글을 삭제할 수 있도록 (수정은 불가능)
detail.jsp
- 로그인 중인 회원의 등급(mg)를 반환하여 관리자인지 확인
- 로그인 중인 회원의 등급(mg)이 관리자인 경우 게시글 삭제 표시
- 작성자일 경우(owner)와 관리자일 경우(admin)를 OR(||) 조건으로 해야 삭제 메뉴가 하나만 표시된다
(두 경우를 분리시키면 삭제 메뉴가 2개가 나옴)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix = "fmt" uri = "http://java.sun.com/jsp/jstl/fmt" %>
<jsp:include page = "/WEB-INF/views/template/header.jsp">
<jsp:param value = "게시글 상세" name = "title"/>
</jsp:include>
<div align = "center">
<h1>게시글 상세</h1>
<table border = "1" width = "900">
<tbody>
<tr>
<th width = "25%">번호</th>
<td>${boardDto.boardNo}</td>
</tr>
<tr>
<th>말머리</th>
<td>${boardDto.boardHead}</td>
</tr>
<tr>
<th>제목</th>
<td>${boardDto.boardTitle}</td>
</tr>
<tr>
<th>작성자</th>
<td>${boardDto.boardWriter}</td>
</tr>
<tr height = "200" valign = "top">
<th>내용</th>
<td>
<%-- pre 태그는 엔터, 띄어쓰기, 탭키 등을 있는 그대로 표시하는 영역 --%>
<pre>${boardDto.boardContent}</pre>
</td>
</tr>
<tr>
<th>조회수</th>
<td>${boardDto.boardRead}</td>
</tr>
<tr>
<th>작성일</th>
<td>
<fmt:formatDate value = "${boardDto.boardWritetime}"
pattern = "y년 M월 d일 E요일 a h시 m분 s초"/>
</td>
</tr>
<%-- 한 번이라도 수정한 적이 있다면(수정 시간이 null이 아니라면) 수정 시간을 표시 --%>
<c:if test = "${boardDto.boardUpdatetime != null}">
<tr>
<th>수정일</th>
<td>
<fmt:formatDate value = "${boardDto.boardUpdatetime}"
pattern = "y년 M월 d일 E요일 a h시 m분 s초"/>
</td>
</tr>
</c:if>
</tbody>
<tfoot>
<tr>
<td colspan = "2" align = "right">
<%-- 로그인 상태에서만 게시글 작성이 가능하도록 설정 --%>
<c:if test = "${loginId != null}">
<a href = "write">글쓰기</a>
</c:if>
<%-- 로그인한 회원이 게시글 작성자인지에 대한 변수(boolean) 생성 --%>
<c:set var = "owner" value = "${loginId == boardDto.boardWriter}"></c:set>
<%-- 만약 로그인한 회원이 게시글 작성자라면 게시글을 수정할 수 있도록 설정--%>
<c:if test = "${owner}">
<a href = "edit?boardNo=${boardDto.boardNo}">수정</a>
</c:if>
<%-- 로그인한 회원의 등급이 관리자인지에 대한 변수(boolean) 생성 --%>
<c:set var = "admin" value = "${mg == '관리자'}"></c:set>
<%-- 만약 로그인한 회원이 게시글 작성자이거나 등급이 관리자라면 게시글을 삭제할 수 있도록 설정--%>
<c:if test = "${owner || admin}">
<a href = "delete?boardNo=${boardDto.boardNo}">삭제</a>
</c:if>
<a href = "list">목록으로</a>
</td>
</tr>
</tfoot>
</table>
</div>
<jsp:include page = "/WEB-INF/views/template/footer.jsp"></jsp:include>
MemberBoardOwnerCheckInterceptor
- 회원이 게시글 작성자인지 확인하기 위한 Interceptor
- 회원이 게시글 작성자인 경우 통과(true) - 게시글 삭제(/board/delete)에 접근 가능
- 회원의 등급이 관리자이면서 요청 URL이 게시글 삭제(/board/delete)일 경우 통과(true)
@Component
public class MemberBoardOwnerCheckInterceptor implements HandlerInterceptor {
@Autowired
private BoardDao boardDao;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 준비
// request에서 HttpSession을 얻어온다
HttpSession session = request.getSession();
// 1. 현재 사용자가 작성자인지 검사 - 작성자라면 통과(true)
// HttpSession에서 저장된 회원 아이디(loginId)의 값을 반환
String memberId = (String) session.getAttribute("loginId");
// 게시글 상세로 이동할 때 하이퍼링크에 포함된 글 번호(boardNo)의 값을 반환
int boardNo = Integer.parseInt(request.getParameter("boardNo"));
// 해당 글 번호(boardNo)를 매개변수로 하여 단일 조회 실행
BoardDto boardDto = boardDao.selectOne(boardNo);
// 현재 사용자가 작성자인지 검사
// HttpSession에서 반환한 아이디(memberId)와
// 단일 조회의 결과로 얻은 boardDto의 작성자(boardWriter)가 같은지 비교
boolean isOwner = memberId.equals(boardDto.getBoardWriter());
if(isOwner) { // 작성자라면 통과(true)
return true;
}
// 2. 관리자가 게시글 삭제를 하는 경우인지 검사
// HttpSession에 저장된 회원 등급(mg)의 값을 반환
String memberGrade = (String) session.getAttribute("mg");
// HttpSession에서 반환한 회원 등급의 값(memberGrade)가 관리자인지
boolean isAdmin = memberGrade.equals("관리자");
// 요청이 GET 방식의 게시글 삭제 Mapping(/board/delete)인지
boolean isDelete = request.getRequestURI().equals("/board/delete");
if(isAdmin && isDelete) { // 관리자가 삭제할 경우 통과(true)
return true;
}
// 그 외 나머지 경우는 차단
response.sendError(403);
return false;
}
}
InterceptorConfiguration 설정 - MemberBoardOwnerCheckInterceptor
- 작성자 또는 관리자가 아닐 경우 해당 게시글 수정(/board/edit)과 게시글 삭제(/board/delete)에 대한 접근을 제한한다
@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {
// 의존성 주입
@Autowired
MemberBoardOwnerCheckInterceptor MemberBoardOwnerCheckInterceptor;
// 추상 메소드 오버라이딩 - addInterceptors(InterceptorRegistry registry)
@Override
public void addInterceptors(InterceptorRegistry registry) {
// MemberBoardOwnerCheckInterceptor에 대한 설정
registry.addInterceptor(memberBoardPermissionCheckInterceptor)
.addPathPatterns(
"/board/edit", // 게시글 수정
"/board/delete" // 게시글 삭제
);
}
}
조회수 증가 중복 방지
- 이미 읽은 게시글은 해당 게시글을 다시 눌렀을 때 조회수가 증가하지 않도록 한다
- 로그인 시 HttpSession에 읽은 게시글 번호(boardNo)를 저장하는 저장소를 생성한다
- 게시글을 읽을 때 해당 게시글 번호(boardNo)가 저장되어있지 않다면 그 번호를 저장한 후 조회수를 증가시킨다
- 게시글을 읽을 때 해당 게시글 번호(boardNo)가 이미 저장되어 있다면 조회수가 증가되지 않도록 한다
- 중복 방지를 위해 게시글 번호를 저장하기 위한 저장소로 Set을 사용한다
BoardController
- 게시글 상세로 연결될 때 무조건 조회수가 증가하던 것을 게시글 번호 저장 여부에 따른 조건부 증가로 바꾼다
- 누른 게시글의 번호가 저장되어있지 않다면 해당 번호를 저장한 후 조회수가 증가하는 단일 조회 read(int boardNo) 실행
- 누른 게시글의 번호가 이미 저장되어 있다면 조회수가 증가하지 않는 단일 조회 selectOne(int boardNo) 실행
- 바뀐 게시글 번호 저장소 history를 HttpSession에 저장 (기존 history에 덮어쓰기)
@Controller
@RequestMapping("/board")
public class BoardController {
// 의존성 주입
@Autowired
BoardDao boardDao;
// 3. 게시글 상세
// 게시글 상세 Mapping
@GetMapping("/detail")
public String detail(Model model, @RequestParam int boardNo, HttpSession session) {
// HttpSession에 읽은 게시글 번호를 저장할 수 있는 Set 생성
Set<Integer> history = (Set<Integer>) session.getAttribute("history");
// 만약 history가 없다면 새로 생성
if(history == null) {
history = new HashSet<>();
}
// 현재 게시글 번호를 저장한 적이 있는지 판정
// - HashSet의 .add(E e) 명령은 저장할 수 있는지(boolean)을 반환
if(history.add(boardNo)) { // 저장할 수 있다면 (처음 들어온 게시글)
// 조회수 증가 + 게시글 상세의 결과를 model에 첨부
BoardDto boardDto = boardDao.read(boardNo);
model.addAttribute("boardDto", boardDto);
}
else { // 저장할 수 없다면 (이미 들어온 적이 있는 게시물)
// 게시글 상세의 결과를 model에 첨부
BoardDto boardDto = boardDao.selectOne(boardNo);
model.addAttribute("boardDto", boardDto);
}
// 갱신된 history를 다시 HttpSession에 저장 (해당 게시글을 다시 볼 때 조회수 증가 방지)
session.setAttribute("history", history);
// 게시글 상세 페이지(detail.jsp)로 연결
return "board/detail";
}
}