Luver Duck 2022. 9. 20. 22:16

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> &laquo; &lt; 1 2 3 4 5 6 7 8 9 10 &gt; &raquo; </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";
	}
}