고래씌

[Spring] 8-1. 일반 게시글 등록(크로스사이트스크립트 공격 방지) 본문

Server/Spring

[Spring] 8-1. 일반 게시글 등록(크로스사이트스크립트 공격 방지)

고래씌 2024. 1. 24. 12:52

1. 게시글 등록

=> views/board/boardEnrollForm.jsp 파일 생성

 

▶ boardEnrollForm.jsp

=> 사진도 보내고 해야하기 때문에 multipart 사용

=> 첨부파일은 ORIGIN_NAME과 CHANGE_NAME에 컬럼에 저장할 예정

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

	<jsp:include page="/WEB-INF/views/common/header.jsp" />
	
	<div class="content">
		<br><br>
		<div class="innerOuter">
			<h2>게시글 작성하기</h2>
			<br>
			<form action="${contextPath }/board/insert/${boardCode }" id="enrollForm"
			method="post" enctype="multipart/form-data">
			<!-- 사진도 보내고 해야하기 때문에 multipart 사용 --> 
			<!-- 첨부파일은 ORIGIN_NAME과 CHANGE_NAME에 컬럼에 저장할 예정 -->
			
				<table align="center">
                    <tr>
                        <th>제목</th>
                        <td><input type="text" id="title" class="form-control" name="boardTitle" required></td>
                    </tr>
                    <tr>
                        <th>작성자</th>
                        <td>${loginUser.userId }</td>
                    </tr>
                        <tr>
                            <th>첨부파일</th>
                            <td><input type="file" id="upfile" class="form-control" name="upfile"></td>
                        </tr>
                    <tr>
                        <th>내용</th>
                        <td>
                            <textarea id="content" style="resize:none;" rows="10" class="form-control"
                            name="boardContent" required="required"></textarea>
                        </td>
                    </tr>
                </table>
                <div align="center">
                    <button type="submit" class="btn btn-primary">등록</button>
                    <button type="reset" class="btn btn-danger">취소</button>
                </div>
			</form>
		</div>
	</div>
	
	<jsp:include page="/WEB-INF/views/common/footer.jsp" />

</body>
</html>

 

 

▶ BoardController.java

	@GetMapping("/insert/{boardCode}")
	public String enrollBoard(@PathVariable("boardCode") String boardCode) {
		return "board/boardEnrollForm";
	}

 

 

▶ boardListView.java

=> 글쓰기 버튼을 클릭하면 다음과 같이 이동되도록 설정

 

 

▶ pom.xml

=> 메이븐레파지토리에서 다음과 같이 복사하여 붙여놓기함.

 

 

		<!-- 파일업로드와 관련된 의존성 -->
		<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
		<dependency>
		    <groupId>commons-fileupload</groupId>
		    <artifactId>commons-fileupload</artifactId>
		    <version>1.4</version>
		</dependency>
		
		<!-- 입출력 관련 라이브러리 -->
		<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
		<dependency>
		    <groupId>commons-io</groupId>
		    <artifactId>commons-io</artifactId>
		    <version>2.11.0</version>
		</dependency>

 

 

 

▶ root-context.xml

인코딩 처리 설정용량제한 설정을 해줄 예정

 

1) 업로드 되는 파일에 대한 설정

   - maxUploadSize : 한번에 업로드 되는 파일의 총 용량 설정
   - maxInMemorySize : 디스크에 임시파일을 생성하기전에 메모리에 보관시킬 최대 바이트 크기

	 <!-- 업로드 되는 파일에 대한 설정 -->
	 <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
	 	
	 	<!-- 인코딩 처리 -->
	 	<property name="defaultEncoding" value="UTF-8"></property>
	 	
	 	<!-- 
	 		maxUploadSize : 한번에 업로드 되는 파일의 총 용량 설정
	 		maxInMemorySize : 디스크에 임시파일을 생성하기전에 메모리에 보관시킬 최대 바이트 크기
	 	 -->
	 	 
	 	 <!-- 총용량 -->
	 	 <property name="maxUploadSize" value="10000000"></property>
	 	 <property name="maxInMemorySize" value="10000000"></property>

	 </bean>

 

 

▶ BoardController.java

 

=> 맨위에 application 추가

 

 

☞ url로 전달받은 동적 파라미터 => @PathVariable("boardCode") String boardCode
@ModelAttribute("loginUser") Member loginUser => loginUser와 일치하는 것을 가져올 수 있음 
@ModelAttribute("loginUser") Member loginUser => sessionAttributes에 의해 session으로 이관된 데이터를 매개변수에서 얻어올 때 사용하는 방법

 

 

☞ 이미지 파일을 저장할 경로는 다음과 같다.

String webPath = "/resources/images/board/" + boardCode + "/";

 

 

☞ Utils는 아래에서 생성하므로 Utils 생성후, 추가함

if(!upfile.getOriginalFilename().equals("")) { // 원본파일이름이 비어있지않다면
    String changeName = Utils.saveFile(upfile, serverFolderPath);  // 업로드 파일, 저장되는 경로
    b.setOrginName(upfile.getOriginalFilename());
    b.setChangeName(changeName);
}
	// url로 전달받은 동적 파라미터 => @PathVariable("boardCode") String boardCode
	// @ModelAttribute("loginUser") Member loginUser => loginUser와 일치하는 것을 가져올 수 있음 
	// @ModelAttribute("loginUser") Member loginUser => sessionAttributes에 의해 session으로 이관된 데이터를 매개변수에서 얻어올 때 사용하는 방법
	@PostMapping("/insert/{boardCode}")  
	public String insertBoard(
			Board b, 
			@PathVariable("boardCode") String boardCode,
			@ModelAttribute("loginUser") Member loginUser, 
			Model model,
			HttpSession session, 
			@RequestParam(value="upfile", required=false) MultipartFile upfile // 첨부파일
			) {
		
		// 이미지, 파일을 저장할 저장경로 얻어오기
		String webPath = "/resources/images/board/" + boardCode + "/";  // boardCode가 T라면 사진게시판, C라면 일반게시판..
		String serverFolderPath = application.getRealPath(webPath);
		
		// resources 폴더안에는 아무것도 없기 때문에 디렉토리가 존재하지 않는다면 생성해주는 코드 추가
		File dir = new File(serverFolderPath);
		if(!dir.exists()) {   // 존재하지 않는다면 디렉토리 생성
			dir.mkdirs();  // 디렉토리를 여러개 추가해야하니까 mkdirs로 추가
		}
		
		// 사용자가 첨부파일을 등록한 경우
		// upfile은 첨부파일이 있든, 없든 무조건 객체는 생성됨.
		// 단, 첨부파일 등록을 하지 않은경우 내부에 데이터가 비어있다. ("")
		// 사용자가 전달한 파일이 있는지 없는지는 filename이 존재하는지로 확인하면 된다.
		if(upfile != null && !upfile.getOriginalFilename().equals("")) { // 원본파일이름이 비어있지않다면
			String changeName = Utils.saveFile(upfile, serverFolderPath);  // 업로드 파일, 저장되는 경로
			b.setOriginName(upfile.getOriginalFilename());
			b.setChangeName(changeName);
		}

		
		// Board 객체에 데이터 추가
		b.setBoardWriter(loginUser.getUserNo()+"");
		b.setBoardCd(boardCode);
		
		log.info("board {}", b);
		
		int result = boardService.insertBoard(b);
		
		String url = "";
		if(result > 0) {
			session.setAttribute("alertMsg", "글작성 성공함");
			url = "redirect:/board/list/" + boardCode;
		}else {
			model.addAttribute("errorMsg", "게시글 작성 실패함");
			url = "common/errorPage";
		}
		
		return url;
	}

 

 

 

▶ Utils.java

=> 파일을 저장할 함수

=> 파일을 저장시키면서, 파일명을 수정한 후 수정된 파일명을 반환

 

=> common 폴더아래에 Utils.java 파일 생성

 

package com.kh.spring.common;

import java.io.File;
import java.io.IOException;

import org.springframework.web.multipart.MultipartFile;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class Utils {
	
	// 파일 저장 함수
	// 파일을 저장시키면서, 파일명을 수정한 후 수정된 파일명을 반환
	public static String saveFile(MultipartFile upfile, String savePath) {
		
		// 랜덤파일명 생성하기
		String originName = upfile.getOriginalFilename();
		String currentTime = new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(new java.util.Date());
		int random = (int)(Math.random() * 90000 + 10000); // 10000 ~ 99999 5자리 랜덤값
		String ext = originName.substring(originName.indexOf("."));   // 이것을 조합해서 변경된 파일이름을 반환할 예정
		
		String changeName = currentTime + random + ext;  // 3가지 문자열을 합쳐서 랜덤한 확장자를 만들어줄 것임
		
		try {
			upfile.transferTo(new File(savePath + changeName));
		} catch (IllegalStateException | IOException e) {
			e.printStackTrace();
		}
		
		return changeName;
	}
}

 

 

▶ BoardService.java

	int insertBoard(Board b);

 

 

▶ BoardServiceImpl.java

	@Override
	public int insertBoard(Board b) {
		/*
		 * 게시글 삽입
		 * 
		 * 게시글 삽입시 게시글의 제목, 내용에 들어가는 문자열에 크로스사이트스크립트 공격을 방지하기 위한 메소드 추가
		 * textarea, 제목에 들어가는 엔터 및 스페이스바를 개행문자로 변환처리
		 */
		
		b.setBoardContent(Utils.XSSHandling(b.getBoardContent()));
		b.setBoardContent(Utils.newLineHandling(b.getBoardContent()));
		b.setBoardTitle(Utils.XSSHandling(b.getBoardTitle()));
		
		return boardDao.insertBoard(b);
	}

 

☞ 게시글 삽입시 게시글의 제목, 내용에 들어가는 문자열에 크로스사이트스크립트 공격을 방지하기 위한 메소드 추가

☞ Utils.java에 추가

 

1) 크로스사이트스크립트 공격 방지

▶ Utils.java

	public static String XSSHandling(String content) {
		if(content != null) {
			content = content.replaceAll("&", "&amp;");
			content = content.replaceAll("<", "&lt;");
			content = content.replaceAll(">", "&gt;");
			content = content.replaceAll("\"", "&quot;");  // 쌍따옴표 막으려면 "뒤에 \이 와야 쌍따옴표로 인식
		}
		
		return content;
	}

 

 

2) 가독성을 위해 추가

▶ Utils.java

 

- 개행문자 처리

- textarea → \n,  p → <br>

	public static String newLineHandling(String content) {
		return content.replaceAll("(\r\n|\r|\n|\n\r)", "<br>");
	}

 

 

- 수정할때는 개행처리 된 것을 다시 해제해주어야함. 
- 개행해제 처리

	public static String newLineClear(String content) {
		return content.replaceAll("<br>", "\n");
	}

 

 

▶ BoardDao.java

	int insertBoard(Board b);

 

 

 

▶ BoardDaoImpl.java

	@Override
	public int insertBoard(Board b) {
		log.info("등록 이전 b {}" , b);
		int result = sqlSession.insert("boardMapper.insertBoard", b);
		log.info("등록 이후 b {}", b);
		
		return result;
	}

 

 

▶ board-mapper.xml

	<!-- 게시글 등록 -->
	<insert id="insertBoard" parameterType="board" useGeneratedKeys="true"> 
	<!-- useGeneratedKeys => insert 되고 나서 setter함수를 호출해서 board안에 넣어줌. selectKey와 무조건 함께 사용 -->
		
		<selectKey keyProperty="boardNo" resultType="int" order="BEFORE">
			SELECT SEQ_BNO.NEXTVAL FROM DUAL
		</selectKey>
		
		INSERT INTO BOARD
		(
			BOARD_NO,
			BOARD_TITLE,
			BOARD_CONTENT,
			ORIGIN_NAME,
			CHANGE_NAME,
			BOARD_CD,
            BOARD_WRITER
		)VALUES (
			SEQ_BNO.CURRVAL,  <!-- #{boardNo} 써도됨 -->
			#{boardTitle},
			#{boardContent},
			#{originName},
			#{changeName},
			#{boardCd},
			#{boardWriter}
		)
		
	</insert>

 

 

▶ 결과

 

 

=> 게시글 등록을 하면 이와같이 목록에 추가된 것을 확인