5월 ~ 7월) 웹/jsp
61Day -[홈페이지 만들기 4일차] review_detail.jsp / review_write.jsp/ review_write_action.jsp / ReviewDAO. // Utllity.java / review_write.jsp
첼로그
2023. 7. 3. 02:42
6월 30일
- 리뷰쓰기 저장
- 비밀글
- 리뷰게시판에 이미지넣기 >> / 답글삽입쓰기
- XSS 공격 방어(마음대로 글씨색바꾸거나, 글씨크기 바꾸기방어)
(아직 후기내용 구현안함)
*** 페이징처리 가장어려움/ 답글저장하는거 어려움!!!
에러떨어지면 왜그런지 알아야함!
+ webapp > WEB-INF > lib) 있어야함
cos.jar
webapp > review)
review_detail.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%-- 글번호를 전달받아 REVIEW 테이블에 저장된 게시글을 검색하여 클라이언트에게 전달하여 응답하는 JSP 문서 --%>
<%-- => 전달된 페이지번호, 검색대상, 검색단어는 반환받아 [review_list.jsp] 문서를 요청할 때 전달 --%>
review_write.jsp 변수
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%-- 사용자로부터 게시글(새글 또는 답글) 정보를 입력받기 위한 JSP 문서 --%>
<%-- => 로그인 상태의 사용자만 요청 가능한 JSP 문서 --%>
<%-- => [글저장] 태그를 클릭한 경우 [review/review_write_action.jsp] 문서 - 입력값(게시글정보) 전달 --%>
<%-- 새글 : [review_list.jsp] 문서에 의해 [review_writer.jsp] 문서를 요청한 경우 - 전달값 : X --%>
<%-- 답글 : [review_detail.jsp] 문서에 의해 [review_writer.jsp] 문서를 요청한 경우 - 전달값 : O --%>
<%-- => [review_detail.jsp] 문서에서 부모 게시글 관련 정보(ref, restep, relevel, pageNum) 전달--%>
<%-- 비로그인 상태의 사용자가 JSP 문서를 요청한 경우 에러페이지로 이동되도록 응답 처리 --%>
<%@include file="/security/login_check.jspf" %>
<%
//전달값을 반환받아 저장 - 전달값이 없는 경우(새글) 변수에 초기값 저장
String ref="0", restep="0", relevel="0", pageNum="1";
if(request.getParameter("ref")!=null) {//전달값이 있는 경우 - 답글
ref=request.getParameter("ref");
restep=request.getParameter("restep");
relevel=request.getParameter("relevel");
pageNum=request.getParameter("pageNum");
}
%>
<style type="text/css">
table {
margin: 0 auto;
}
th {
width: 100px;
font-weight: bold;
}
td {
text-align: left;
}
</style>
<% if(ref.equals("0")) {//새글인 경우 %>
<h1>새글쓰기</h1>
<% } else {//답글인 경우 %>
<h1>답글쓰기</h1>
<% } %>
<%-- 파일을 입력받아 전달하기 위해 반드시 enctype 속성값을 [multipart/form-data]로 설정 --%>
<form action="<%=request.getContextPath() %>/review/review_write_action.jsp"
method="post" enctype="multipart/form-data" id="reviewForm">
<input type="hidden" name="ref" value="<%=ref%>">
<input type="hidden" name="restep" value="<%=restep%>">
<input type="hidden" name="relevel" value="<%=relevel%>">
<input type="hidden" name="pageNum" value="<%=pageNum%>">
<table>
<tr>
<th>제목</th>
<td>
<input type="text" name="subject" id="subject" size="40">
<input type="checkbox" name="secret" value="2">비밀글
</td>
</tr>
<tr>
<th>내용</th>
<td>
<textarea rows="7" cols="60" name="content" id="rContent"></textarea>
</td>
</tr>
<tr>
<th>리뷰이미지</th>
<td>
<input type="file" name="reviewimg">
</td>
</tr>
<tr>
<th colspan="2">
<button type="submit">글저장</button>
<button type="reset" id="resetBtn">다시쓰기</button>
</th>
</tr>
</table>
</form>
<div id="message" style="color: red;"></div>
<script type="text/javascript">
$("#subject").focus();
$("#reviewForm").submit(function() {
if($("#subject").val()=="") {
$("#message").text("제목을 입력해 주세요.");
$("#subject").focus();
return false;
}
if($("#rContent").val()=="") {
$("#message").text("내용을 입력해 주세요.");
$("#rContent").focus();
return false;
}
});
$("#resetBtn").click(function() {
$("#subject").focus();
$("#message").text("");
});
</script>
review_write_action.jsp
<%@page import="xyz.itwill.util.Utility"%>
<%@page import="xyz.itwill.dto.ReviewDTO"%>
<%@page import="xyz.itwill.dao.ReviewDAO"%>
<%@page import="com.oreilly.servlet.multipart.DefaultFileRenamePolicy"%>
<%@page import="com.oreilly.servlet.MultipartRequest"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%-- 게시글(새글 또는 답글)을 전달받아 REVIEW 테이블에 삽입하고 [review/review_list.jsp] 문서로
이동하기 위한 URL 주소를 클라이언트에게 전달하여 응답하는 JSP 문서 --%>
<%-- => 로그인 상태의 사용자만 요청 가능한 JSP 문서 --%>
<%-- => [multipart/form-data] 형태로 값이 전달되므로 COS 라이브러리의 MultipartRequest 클래스를 사용하여 처리 --%>
<%-- => 전달받은 파일은 [/review_images] 서버 디렉토리에 업로드 처리하여 저장 --%>
<%@include file="/security/login_check.jspf" %>
<%
if(request.getMethod().equals("GET")) {
response.sendRedirect(request.getContextPath()+"/index.jsp?group=error&worker=error_400");
return;
}
//전달파일을 저장할 서버 디렉토리(웹자원)의 파일 시스템 경로를 반환받아 저장
//String saveDirectory=application.getRealPath("/review_images");
String saveDirectory=request.getServletContext().getRealPath("/review_images");
//System.out.println("saveDirectory = "+saveDirectory);
//MultipartRequest 객체 생성 - 모든 전달파일을 서버 디렉토리에 업로드 처리하여 저장
// => cos.jar 라이브러리 파일을 프로젝트에 반드시 빌드 처리
MultipartRequest multipartRequest=new MultipartRequest(request, saveDirectory
, 20*1024*1024, "utf-8", new DefaultFileRenamePolicy());
//전달값을 반환받아 저장
int ref=Integer.parseInt(multipartRequest.getParameter("ref"));
int restep=Integer.parseInt(multipartRequest.getParameter("restep"));
int relevel=Integer.parseInt(multipartRequest.getParameter("relevel"));
String pageNum=multipartRequest.getParameter("pageNum");
//사용자로부터 입력받아 전달된 값에 태그 관련 문자값이 존재할 경우 웹프로그램 실행시 문제 발생
// => XSS(Cross Site Scripting) 공격 : 사용자가 악의적인 스크립트를 입력하여 페이지가 깨지거나
//다른 사용자의 사용을 방해하거나 쿠키 및 기타 개인 정보를 특정 사이트로 전송하는 공격
//String subject=multipartRequest.getParameter("subject");
//XSS 공격을 방어하기 위해 전달값을 변환하여 필드값으로 저장
//String subject=Utility.stripTag(multipartRequest.getParameter("subject"));//사용자가 입력한 태그 관련 문자열을 제거하여 저장
String subject=Utility.escapeTag(multipartRequest.getParameter("subject"));//사용자가 입력한 태그를 문자열로 처리하여 저장
int status=1;//전달값이 없는 경우 - 일반글
if(multipartRequest.getParameter("secret")!=null) {//전달값이 있는 경우 - 비밀글
status=Integer.parseInt(multipartRequest.getParameter("secret"));
}
String content=Utility.escapeTag(multipartRequest.getParameter("content"));
//업로드 처리된 파일명을 반환받아 저장
String reviewimg=multipartRequest.getFilesystemName("reviewimg");
//REVIEW_SEQ 시퀸스의 다음값을 검색하여 반환하는 DAO 클래스의 메소드 호출
// => 게시글의 글번호(NUM 컬럼값)로 저장
// => 새글인 경우에는 게시글의 글그룹(REF 컬럼값)의 값으로 저장
int num=ReviewDAO.getDAO().selectNextNum();
//게시글을 작성한 클라이언트의 IP 주소를 반환받아 저장
//request.getRemoteAddr() : JSP 문서를 요청한 클라이언트의 IP 주소를 반환하는 메소드
// => 이클립스에 등록되어 동작되는 WAS 프로그램은 기본적으로 128Bit 형식(IPV6)의 IP 주소 제공
//32Bit 형식(IPV4)의 IP 주소를 제공받을 수 있도록 이클립스의 Apache Tomcat 실행 환경 변경
// => Run >> Run Configurations... >> Apache Tomcat >> 사용중인 Apache Tomcat 선택
// >> Arguments >> VM Arguments >> [-Djava.net.preferIPv4Stack=true] 추가 >> Apply
String ip=request.getRemoteAddr();
//System.out.println("ip = "+ip);
//게시글을 새글과 답글로 구분하여 REVIEW 테이블의 컬럼값으로 저장될 변수값 변경
// => [review_write.jsp] 문서에서 hidden 타입으로 전달된 값이 저장된 ref, restep, relevel
//변수값 변경 - 새글 : 초기값(0), 답글 : 부모글로부터 전달된 값
if(ref==0) {//새글인 경우
//REVIEW 테이블의 REF 컬럼값에는 시퀸스의 다음값(num 변수값)을 저장하고 RESTEP 컬럼과
//RELEVEL 컬럼에는 restep 변수값(0)과 relevel 변수값(0)을 저장
ref=num;
} else {//답글인 경우
//REVIEW 테이블에 저장된 기존 게시글에서 REF 컬럼값이 ref 변수값(부모글)과 같은 게시글
//중 RESTEP 컬럼값이 restep 변수값(부모글)보다 큰 모든 게시글의 RESTEP 컬럼값을 1 증가
//되도록 변경 처리
// => 새로운 답글이 기존 답글보다 먼저 검색되도록 기존 답글의 순서를 증가
// => REVIEW 테이블에 저장된 게시글의 RESTEP 컬럼값을 변경하는 DAO 클래스의 호출
ReviewDAO.getDAO().updateRestep(ref, restep);
//REVIEW 테이블의 REF 컬럼값에는 ref 변수값(부모글)을 저장하고 RESTEP 컬럼과 RELEVEL
//컬럼에는 restep 변수값(부모글)과 relevel 변수값(부모글)을 1 증가하여 저장
restep++;
relevel++;
}
//DTO 객체를 생성하고 전달값(변수값)으로 필드값 변경
ReviewDTO review=new ReviewDTO();
review.setNum(num);
review.setReviewid(loginMember.getId());
review.setSubject(subject);
review.setContent(content);
review.setReviewimg(reviewimg);
review.setRef(ref);
review.setRestep(restep);
review.setRelevel(relevel);
review.setIp(ip);
review.setStatus(status);
//게시글을 전달받아 REVIEW 테이블에 삽입하는 DAO 클래스의 메소드 호출
ReviewDAO.getDAO().insertReview(review);
//페이지 이동
response.sendRedirect(request.getContextPath()+"/index.jsp?group=review&worker=review_list&pageNum="+pageNum);
%>
main>java> xyz.itwill > dao)
ReviewDAO. (수정)
package xyz.itwill.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import xyz.itwill.dto.ReviewDTO;
public class ReviewDAO extends JdbcDAO {
private static ReviewDAO _dao;
private ReviewDAO() {
// TODO Auto-generated constructor stub
}
static {
_dao=new ReviewDAO();
}
public static ReviewDAO getDAO() {
return _dao;
}
//게시글 검색 관련 정보를 전달받아 REVIEW 테이블에 저장된 게시글 중 검색 처리된 전체
//게시글의 갯수를 검색하여 반환하는 메소드
public int selectReviewCount(String search, String keyword) {
Connection con=null;
PreparedStatement pstmt=null;
ResultSet rs=null;
int count=0;
try {
con=getConnection();
//매개변수에 저장된 값을 비교하여 DBMS 서버에 다른 SQL 명령을 전달하여 실행
// => 동적 SQL(DynamicSQL) 기능
if(keyword.equals("")) {//게시글 검색 기능을 사용하지 않은 경우
//REVIEW 테이블에 저장된 전체 게시글의 갯수 검색
String sql="select count(*) from review";
pstmt=con.prepareStatement(sql);
} else {//게시글 검색 기능을 사용한 경우
//검색대상(컬럼명)에 검색단어가 포함한 게시글의 갯수 검색 - 삭제글 제외
String sql="select count(*) from review join member on reviewid=id where "
+search+" like '%'||?||'%' and status <> 0";
pstmt=con.prepareStatement(sql);
pstmt.setString(1, keyword);
}
rs=pstmt.executeQuery();
if(rs.next()) {
count=rs.getInt(1);
}
} catch (SQLException e) {
System.out.println("[에러]selectReviewCount() 메소드의 SQL 오류 = "+e.getMessage());
} finally {
close(con, pstmt, rs);
}
return count;
}
//페이징 처리 관련 정보와 게시글 검색 기능 관련 정보를 전달하여 REVIEW 테이블에 저장된
//게시글 목록을 검색하여 List 객체로 반환하는 메소드
public List<ReviewDTO> selectReviewList(int startRow, int endRow, String search, String keyword) {
Connection con=null;
PreparedStatement pstmt=null;
ResultSet rs=null;
List<ReviewDTO> reviewList=new ArrayList<>();
try {
con=getConnection();
if(keyword.equals("")) {//게시글 검색 기능을 사용하지 않은 경우
String sql="select * from (select rownum rn, temp.* from (select num, reviewid"
+ ", name, subject, content, reviewimg, regdate, readcount, ref, restep"
+ ", relevel,ip, status from review join member on reviewid=id order by"
+ " ref desc, restep) temp) where rn between ? and ?";
pstmt=con.prepareStatement(sql);
pstmt.setInt(1, startRow);
pstmt.setInt(2, endRow);
} else {//게시글 검색 기능을 사용한 경우
String sql="select * from (select rownum rn, temp.* from (select num, reviewid"
+ ", name, subject, content, reviewimg, regdate, readcount, ref, restep"
+ ", relevel,ip, status from review join member on reviewid=id where "
+ search + " like '%'||?||'%' and status <> 0 order by ref desc, restep)"
+ " temp) where rn between ? and ?";
pstmt=con.prepareStatement(sql);
pstmt.setString(1, keyword);
pstmt.setInt(2, startRow);
pstmt.setInt(3, endRow);
}
rs=pstmt.executeQuery();
while(rs.next()) {
ReviewDTO review=new ReviewDTO();
review.setNum(rs.getInt("num"));
review.setReviewid(rs.getString("reviewid"));
review.setName(rs.getString("name"));
review.setSubject(rs.getString("subject"));
review.setContent(rs.getString("content"));
review.setReviewimg(rs.getString("reviewimg"));
review.setRegdate(rs.getString("regdate"));
review.setReadcount(rs.getInt("readcount"));
review.setRef(rs.getInt("ref"));
review.setRestep(rs.getInt("restep"));
review.setRelevel(rs.getInt("relevel"));
review.setIp(rs.getString("ip"));
review.setStatus(rs.getInt("status"));
reviewList.add(review);
}
} catch (SQLException e) {
System.out.println("[에러]selectReviewList() 메소드의 SQL 오류 = "+e.getMessage());
} finally {
close(con, pstmt, rs);
}
return reviewList;
}
//REVIEW_SEQ 시퀸스의 다음값을 검색하여 반환하는 메소드
public int selectNextNum() {
Connection con=null;
PreparedStatement pstmt=null;
ResultSet rs=null;
int nextNum=0;
try {
con=getConnection();
String sql="select review_seq.nextval from dual";
pstmt=con.prepareStatement(sql);
rs=pstmt.executeQuery();
if(rs.next()) {
nextNum=rs.getInt(1);
}
} catch (SQLException e) {
System.out.println("[에러]selectNextNum() 메소드의 SQL 오류 = "+e.getMessage());
} finally {
close(con, pstmt, rs);
}
return nextNum;
}
//게시글정보를 전달받아 REVIEW 테이블에 삽입하고 삽입행의 갯수를 반환하는 메소드
public int insertReview(ReviewDTO review) {
Connection con=null;
PreparedStatement pstmt=null;
int rows=0;
try {
con=getConnection();
String sql="insert into review values(?,?,?,?,?,sysdate,0,?,?,?,?,?)";
pstmt=con.prepareStatement(sql);
pstmt.setInt(1, review.getNum());
pstmt.setString(2, review.getReviewid());
pstmt.setString(3, review.getSubject());
pstmt.setString(4, review.getContent());
pstmt.setString(5, review.getReviewimg());
pstmt.setInt(6, review.getRef());
pstmt.setInt(7, review.getRestep());
pstmt.setInt(8, review.getRelevel());
pstmt.setString(9, review.getIp());
pstmt.setInt(10, review.getStatus());
rows=pstmt.executeUpdate();
} catch (SQLException e) {
System.out.println("[에러]insertReview() 메소드의 SQL 오류 = "+e.getMessage());
} finally {
close(con, pstmt);
}
return rows;
}
//부모글 관련 정보를 전달받아 REVIEW 테이블에 저장된 게시글에서 부모글의 글그룹과 같은
//게시글 중 부모글의 글순서보다 큰 게시글의 RESTEP 컬럼값을 1 증가되도록 변경하고
//변경행의 갯수를 반환하는 메소드
public int updateRestep(int ref, int restep) {
Connection con=null;
PreparedStatement pstmt=null;
int rows=0;
try {
con=getConnection();
String sql="update review set restep=restep+1 where ref=? and restep>?";
pstmt=con.prepareStatement(sql);
pstmt.setInt(1, ref);
pstmt.setInt(1, restep);
rows=pstmt.executeUpdate();
} catch (SQLException e) {
System.out.println("[에러]updateRestep() 메소드의 SQL 오류 = "+e.getMessage());
} finally {
close(con, pstmt);
}
return rows;
}
}
main>java> xyz.itwill >util)
Utllity.java - XSS공격방어
package xyz.itwill.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Pattern;
//웹프로그램 작성에 필요한 기능을 제공하기 위한 클래스
public class Utility {
//문자열을 전달받아 암호화 처리하여 반환하는 메소드
public static String encrypt(String passwd) {
String encryptPasswd="";//암호화 처리된 비밀번호를 저장하기 변수
try {
//MessageDigest.getInstance(String algorithm) : 매개변수로 전달받은 암호화
//알고리즘이 저장된 MessageDigest 객체를 생성하여 반환하는 메소드
// => MessageDigest 객체 : 암호화 처리 기능을 제공하기 위한 객체
// => 매개변수에 잘못된 암호화 알고리즘을 전달할 경우 NoSuchAlgorithmException 발생
//단방향 암호화 알고리즘(복호화 불가능) : MD5, SHA-1, SHA-256, SHA-512 등
//양방향 암호화 알고리즘(복호화 가능) : AES-123, RSA 등
MessageDigest messageDigest=MessageDigest.getInstance("SHA-256");
//MessageDigest.update(byte[] input) : MessageDigest 객체에 암호화 처리하기 위한
//문자열을 byte 배열로 전달받아 저장하기 위한 메소드
//String.getBytes() : String 객체에 저장된 문자열을 원시데이타(byte 배열)로 변환하여 반환하는 메소드
messageDigest.update(passwd.getBytes());
//MessageDigest.digest() : MessageDigest 객체에 저장된 암호화 알고리즘을 사용하여
//byte 배열의 값을 암호화 처리하여 byte 배열로 반환하는 메소드
byte[] digest=messageDigest.digest();
//암호화 처리된 byte 배열을 String 객체의 문자열로 변환하여 저장
for(int i=0;i<digest.length;i++) {
//Integer.toHexString(int i) : 매개변수로 전달받은 정수값을 16진수의 문자열로 변환하여 반환하는 메소드
encryptPasswd+=Integer.toHexString(digest[i]&0xff);
}
} catch (NoSuchAlgorithmException e) {
System.out.println("[에러]잘못된 암호화 알고리즘을 사용 하였습니다.");
}
return encryptPasswd;
}
//문자열을 전달받아 태그 관련 문자열을 모두 제거하여 반환하는 메소드
public static String stripTag(String source) {
//Pattern.compile(String regex) : 매개변수로 전달받은 정규표현식이 저장된 Pattern 객체를
//생성하여 반환하는 메소드
Pattern htmlTag=Pattern.compile("\\<.*?\\>");
//Pattern.matcher(CharSequence input) : 매개변수에 입력값을 전달받아 Pattern 객체의
//정규표현식과 비교값이 저장된 Matcher 객체를 생성하여 반환하는 메소드
// => Matcher 객체 : 정규표현식과 입력값을 비교하여 문자열의 검색,변경,삭제 기능을 제공하기 위한 객체
//Matcher.replaceAll(String replacement) : 입력값에서 정규표현식과 동일한 패턴의
//문자열을 모두 찾아 매개변수로 전달받은 문자열로 변경하는 메소드
return htmlTag.matcher(source).replaceAll("");//문자열에서 HTML 태그를 삭제하여 반환
}
//문자열을 전달받아 태그 관련 문자를 회피문자로 변경하여 반환하는 메소드
public static String escapeTag(String source) {
return source.replace("<", "<").replace(">", ">");
}
}