기존 프로젝트에 시큐리티 접목
로그인 페이지 처리
체크리스트
- JSTL/시큐리티 태그 사용하도록 선언
- CSS/JS 파일의 링크를
절대경로
로 설정 (/resources/css/….) - CSRF 토큰 항목 추가
- JavaScript 통한 로그인 처리
customLogin.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Login</title>
<!-- Custom fonts for this template-->
<link href="/resources/vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
<link
href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"
rel="stylesheet">
<!-- Custom styles for this template-->
<link href="/resources/css/sb-admin-2.min.css" rel="stylesheet">
</head>
<body class="bg-gradient-primary">
<div class="container">
<!-- Outer Row -->
<div class="row justify-content-center">
<div class="col-xl-10 col-lg-12 col-md-9">
<div class="card o-hidden border-0 shadow-lg my-5">
<div class="card-body p-0">
<!-- Nested Row within Card Body -->
<div class="row">
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
<div class="col-lg-6">
<div class="p-5">
<div class="text-center">
<h1 class="h4 text-gray-900 mb-4">Welcome Back!</h1>
</div>
<form class="user" method="post" action="/login">
<div class="form-group">
<input type="text" class="form-control form-control-user"
id="username" name="username" aria-describedby="emailHelp"
placeholder="Enter username...">
</div>
<div class="form-group">
<input type="password" class="form-control form-control-user"
id="password" name="password" placeholder="Password">
</div>
<div class="form-group">
<div class="custom-control custom-checkbox small">
<input type="checkbox" class="custom-control-input" id="customCheck" name="remember-me">
<label class="custom-control-label" for="customCheck">Remember
Me</label>
</div>
</div>
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<input type='submit' class="btn btn-primary btn-user btn-block" value="Login">
<!-- <hr>
<a href="index.html" class="btn btn-google btn-user btn-block">
<i class="fab fa-google fa-fw"></i> Login with Google
</a>
<a href="index.html" class="btn btn-facebook btn-user btn-block">
<i class="fab fa-facebook-f fa-fw"></i> Login with Facebook
</a> -->
</form>
<hr>
<!-- <div class="text-center">
<a class="small" href="forgot-password.html">Forgot Password?</a>
</div>
<div class="text-center">
<a class="small" href="register.html">Create an Account!</a>
</div> -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript-->
<script src="/resources/vendor/jquery/jquery.min.js"></script>
<script src="/resources/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- Core plugin JavaScript-->
<script src="/resources/vendor/jquery-easing/jquery.easing.min.js"></script>
<!-- Custom scripts for all pages-->
<script src="/resources/js/sb-admin-2.min.js"></script>
</body>
</html>
게시판 스프링 시큐리티 처리
스프링 시큐리티 적용시 한글 처리
필터의 순서에 따라 한글이 깨지는 경우가 발생한다.
web.xml 에서 한글인코딩 필터와 시큐리티 필터의 순서에 주의한다. (인코딩 필터 먼저.)
<filter>
<filter-name>encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding</filter-name>
<servlet-name>appServlet</servlet-name>
</filter-mapping>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
authentication-success-handler-ref 속성 제거
- 별도로 authentication-success-handler-ref 를 지정하지 않으면, 로그인 성공 시, 사용자가 요청했던 페이지를 보여준다. 디폴트 처리가
SavedRequestAwareAuthenticationSuccessHandler
클래스 이용하기 때문이다.
security-context.xml
<security:http>
<!-- 생략 -->
<!-- <security:logout logout-url="/customLogout" invalidate-session="true" delete-cookies="remember-me, JSESSIONID" success-handler-ref="customLogoutSuccessHandler"/> -->
<security:logout logout-url="/customLogout" invalidate-session="true" delete-cookies="remember-me, JSESSIONID" />
<security:remember-me data-source-ref="dataSource" token-validity-seconds="604800"/>
<!-- 생략 -->
</security:http>
SecurityConfig
http.formLogin()
.loginPage("/customLogin")
.loginProcessingUrl("/login");
// .successHandler(loginSuccessHandler());
게시물 작성
BoardController 메서드 일부에 시큐리티 어노테이션 추가한다.
isAuthenticated()
표현식은 어떤 사용자든 로그인이 성공한 사용자만 기능을 사용할 수 있도록 처리한다.
package org.example.controller;
@Controller
@RequestMapping("/board/*")
@Log4j
@AllArgsConstructor
public class BoardController {
private BoardService service;
@PreAuthorize("isAuthenticated()")
@GetMapping("/register")
public void register() {
}
@PreAuthorize("isAuthenticated()")
@PostMapping("/register")
public String register(BoardVO vo, RedirectAttributes rttr) {
service.register(vo);
rttr.addFlashAttribute("result", "success");
return "redirect:/board/list";
}
// 생략
}
화면에서 게시물 작성 시, 로그인한 사용자의 아이디가 출력되도록 한다.
- 시큐리티 태그를 선언한다 (
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
). -
<sec:authentication property="principal.username" />
태그를 사용한다. - CSRF 토큰을 설정한다 (
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
)
register.jsp
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<!-- 생략 -->
<form role="form" action="/board/register" method="post">
<!-- 생략 -->
<div class="form-group">
<label>Writer</label><input class="form-control" name="writer" value='<sec:authentication property="principal.username" />' readonly="readonly"/>
</div>
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<button type="submit" class="btn btn-default">Submit Button</button>
<button type="reset" class="btn btn-default">Reset Button</button>
</form>
게시물 조회화면
게시물 조회 화면에서 게시글 작성자와 로그인한 사용자가 일치 할 때만, 수정/삭제가 가능하도록 처리한다.
게시물 댓글 추가버튼은, 로그인한 사용자만 이용할 수 있도록 처리한다.
/board/get.jsp
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<!-- 생략 -->
<sec:authentication property="principal" var="userInfo"/>
<sec:authorize access="isAuthenticated()">
<c:if test="${board.writer eq userInfo.username}">
<button data-oper='modify' class="btn btn-primary">Modify</button>
</c:if>
</sec:authorize>
<button data-oper='list' class="btn btn-info">List</button>
<!-- 생략 -->
<!-- 댓글처리 START-->
<div class="col-lg-12 mt-3">
<div class="card">
<div class="card-header">
<i class="fa fa-comments fa-fw"></i>Reply
<sec:authorize access="isAuthenticated()">
<button id='addReplyBtn' class='btn btn-primary btn-sm float-right'>New Reply</button>
</sec:authorize>
</div>
<!-- 생략 -->
게시물 수정/삭제
- 로그인한 사용자만 접근 가능하며, 작성자와 로그인 사용자가 같은 경우에만 수정/삭제가 가능해야 한다.
- Url을 조작해서 수정/삭제가 가능하므로 jsp에 CSRF 토큰을 설정하고 Controller 에도 인증을 체크한다.
modify.jsp
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<!-- 생략 -->
<div class="panel-body">
<form action='/board/modify' method='post'>
<!-- 생략 -->
<sec:authentication property="principal" var="userInfo"/>
<sec:authorize access="isAuthenticated()">
<c:if test="${userInfo.username eq board.writer}">
<button data-oper='modify' class="btn btn-info">Modify</button>
<button data-oper='remove' class="btn btn-danger">Remove</button>
</c:if>
</sec:authorize>
<button data-oper='list' class="btn btn-primary">List</button>
<input type='hidden' name="pageNumber" value="${criteria.pageNumber}"/>
<input type='hidden' name="amount" value="${criteria.amount}"/>
<input type='hidden' name="rowindex" value="${criteria.rowindex}"/>
<input type='hidden' name='type' value='${criteria.type}'/>
<input type='hidden' name='keyword' value='${criteria.keyword}'/>
<input type='hidden' name='${_csrf.parameterName}' value='${_csrf.token}'/>
</form>
</div>
BoardController.java
delete()는, writer를 파라미터로 추가하여 @PreAuthorize의 표현식에서 #writer로 사용한다.
modify()는, 이미 BoardVO를 파라미터로 받고 있으므로 @PreAuthorize의 표현식에서 #vo.writer로 사용한다.
package org.example.controller;
@Controller
@RequestMapping("/board/*")
@Log4j
@AllArgsConstructor
public class BoardController {
private BoardService service;
@PreAuthorize("principal.username == #vo.writer")
@PostMapping("/modify")
public String modify(Criteria cri, BoardVO vo, RedirectAttributes rttr) {
if( service.modify(vo)) {
rttr.addFlashAttribute("result","success");
}
return "redirect:/board/list" + cri.getListLink();
}
//삭제는 반드시, post 로만 처리합니다.
@PreAuthorize("principal.username == #writer")
@PostMapping("/remove")
public String delete(Criteria cri, BoardVO vo, RedirectAttributes rttr, String writer) {
if(service.remove(vo.getBno())) {
rttr.addFlashAttribute("result","success");
}
return "redirect:/board/list" + cri.getListLink();
}
}
게시판 Ajax 스프링 시큐리티 처리
Ajax로 Post, put, patch, delete 같은 방식을 데이터 전송하는 경우 반드시 추가적으로 ‘X-CSRF-TOKEN' 헤더정보를 추가해서
CSRF
토큰값을 전달해야 한다.
(1) javascript 파일의 상단에 csrf 헤더이름과 값을 선언하고 Ajax의beforeSend
를 이용해서 추가적인 헤더를 지정한 후 전송하는 방법(파일첨부시에는 이 방법을 사용한다.)
(2) jQuery를 이용해서 CSRF TOKEN을 보내는 것을 기본으로 지정해서 사용하는 방법 ( 보통의 CRUD에는 이 방법이 더 편하다.)
예제 (1)
var csrfHeaderName = "${_csrf.headerName}";
var csrfTokenValue = "${_csrf.token}";
$.ajax({
url: '/upload',
processData: false,
contentType: false,
beforeSend : function(xhr) {
xhr.setRequestHeader(csrfHeaderName, csrfTokenValue);
},
data: formData,
type: 'post',
dytaType: 'json',
success: function(result){
console.log(result);
}
});
예제 (2)
$(document).ajaxSend(function(e, xhr, options){
xhr.setRequestHeader(csrfHeaderName, csrfTokenValue);
});
// 생략, CSRF를 위한 별도의 처리를 하지 않아도 됨.
첨부파일 수정/삭제
댓글등록
get.jsp
$(document).ready() 안에 아래와 같이 csrf 설정을 한다.
시큐리티 태그로 로그인 사용자를 댓글등록 폼에 입력한다.
var csrfHeaderName = '${_csrf.headerName}';
var csrfTokenValue = '${_csrf.token}';
$(document).ajaxSend(function(e, xhr, options){
xhr.setRequestHeader(csrfHeaderName, csrfTokenValue);
});
var replyer = null;
<sec:authorize access="isAuthenticated()">
replyer = '<sec:authentication property="principal.username"/>';
</sec:authorize>
$('#addReplyBtn').on('click', function(){
modal.find('input').val('');
modal.find('input[name="replyer"]').val(replyer).attr('readonly','readonly');
modalInputReplyDate.closest("div").hide();
modal.find('button[id != "modalCloseBtn"]').hide();
modalRegisterBtn.show();
$('#replyModal').modal('show');
})
ReplyController.java
로그인한 사용자만 댓글을 달 수 있도록, @PreAuthorize("isAuthenticated()")
을 추가한다.
@RestController
@RequestMapping("/reply")
@Log4j
public class ReplyCotroller {
@Autowired
private ReplyService service;
@PostMapping(value="/new",
consumes = "application/json" ,
produces = {MediaType.TEXT_PLAIN_VALUE})
@PreAuthorize("isAuthenticated()")
public ResponseEntity<String> register(@RequestBody ReplyVO vo){
return service.register(vo) ? new ResponseEntity<>("success", HttpStatus.OK) : new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
댓글삭제
로그인한 사용자가 자신이 등록한 댓글만 삭제가 가능하도록 해야한다.
Controller로 댓글번호외에 추가적으로, 작성자 정보도 전송해서 Controller 에서도 작성자 확인 작업을 해야 한다.
get.jsp
$(document).ready(function(){
// 생략
var csrfHeaderName = '${_csrf.headerName}';
var csrfTokenValue = '${_csrf.token}';
$(document).ajaxSend(function(e, xhr, options){
xhr.setRequestHeader(csrfHeaderName, csrfTokenValue);
});
// 생략
var replyer = null;
<sec:authorize access="isAuthenticated()">
replyer = '<sec:authentication property="principal.username"/>';
</sec:authorize>
// 생략
modalRemoveBtn.on('click', function(e){
//로그인 안한 상태
if(!replyer){
alert("로그인 후 삭제가 가능합니다.");
modal.modal('hide');
}
//로그인한 사람과 작성자가 다른 상태
var originalReplyer = modalInputReplyer.val();
if(replyer != originalReplyer){
alert('자신이 작성한 댓글만 삭제가 가능합니다.');
modal.modal('hide');
return;
}
replyService.remove(modal.data('rno'), originalReplyer, function(result){
if(result==='success')alert('댓글이 삭제되었습니다.');
modal.modal('hide');
showList(1);
})
})
}
reply.js
//댓글삭제
function remove(rno, replyer, callback, error){
$.ajax({
url : '/reply/'+ rno,
type : 'delete',
data : JSON.stringify({rno:rno, replyer:replyer}),
// contentType에 json으로 보낼것을 꼭 명시해야 함.
contentType : "application/json; charset=utf-8",
success : function(result,status,xhr){
if(callback) callback(result);
},
error : function(xhr,status,err){
if(error) error(err);
}
});
}
ReplyController.java
로그인한 사람과 작성자가 일치하는지를 체크해야 한다.
@PreAuthorize("principal.username == #vo.replyer")
@DeleteMapping( value ="/{rno}",
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.TEXT_PLAIN_VALUE})
public ResponseEntity<String> remove(@RequestBody ReplyVO vo, @PathVariable("rno") Long rno){
return service.remove(rno) ? new ResponseEntity<>("success", HttpStatus.OK) : new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
댓글수정
로그인한 사용자가 자신이 등록한 댓글만 수정이 가능하도록 해야한다.
Controller 에서도 작성자와 로그인한 사용자가 같은지 확인 작업을 해야 한다.
get.jsp
$(document).ready(function(){
// 생략
var csrfHeaderName = '${_csrf.headerName}';
var csrfTokenValue = '${_csrf.token}';
$(document).ajaxSend(function(e, xhr, options){
xhr.setRequestHeader(csrfHeaderName, csrfTokenValue);
});
// 생략
var replyer = null;
<sec:authorize access="isAuthenticated()">
replyer = '<sec:authentication property="principal.username"/>';
</sec:authorize>
// 생략
modalModBtn.on('click', function(e){
//로그인 안한 상태
if(!replyer){
alert("로그인 후 삭제가 가능합니다.");
modal.modal('hide');
return;
}
//로그인한 사람과 작성자가 다른 상태
var originalReplyer = modalInputReplyer.val();
if(replyer != originalReplyer){
alert('자신이 작성한 댓글만 수정 가능합니다.');
modal.modal('hide');
return;
}
if(!modalInputReply.val()){
alert('댓글을 입력하세요.');
return;
}
const data = {
rno:modal.data('rno'),
reply:modalInputReply.val(),
replyer:originalReplyer};
replyService.modify(data, function(result){
alert('댓글이 수정되었습니다.');
modal.modal('hide');
showList(1);
});
});
}
ReplyController.java
@PreAuthorize("principal.username == #vo.replyer")
@RequestMapping( method = {RequestMethod.PATCH, RequestMethod.PUT},
value = "/{rno}",
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.TEXT_PLAIN_VALUE})
public ResponseEntity<String> modify(@PathVariable("rno") Long rno, @RequestBody ReplyVO vo){
vo.setRno(rno);
return service.modify(vo) ? new ResponseEntity<>("success", HttpStatus.OK ) : new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}