요구사항
- 로그아웃
- 자동 로그인
- 컨트롤러와 화면에서 인증처리
- 로그인 유무와 권한에 따른 제어
- 컨트롤러 인증된 정보 활용하는 경우의 처리
로그아웃
스프링 시큐리티는 HttpSession을 이용해 로그인을 수행하기 때문에 쿠키를 삭제하게 된다면 세션이 종료되고, 자동으로 로그아웃
로그인부터 로그아웃까지 동작 과정
- 입력한 아이디로 해당 아이디의 사용자 정보를 가져온다.
- 가져온 사용자의 비밀번호와 입력한 비밀번호가 일치하면 세션을 성립하고, 인증(로그인) 처리한다.
- 쿠키를 삭제하면 성립한 세션이 종료되고 자동으로 로그아웃처리가 된다.
CSRF 토큰을 사용하지 않는 경우 GET방식의 로그아웃 처리가 가능
(1) GET방식의 로그인 메서드에서 로그아웃 여부 확인
- '/logout' 경로 호출시 -> 'login?logout' 경로로 이동
-이를 이용해 로그아웃 여부 확인
@GetMapping("/login")
//로그인 과정에서 문제 발생 시, 로그아웃 시 사용하기 위한 파라미터
public void loginGET(String error, String logout){
log.info("login get...");
log.info("logout : "+logout);
if(logout!=null){
log.info("user logout...");
}
(2) login.html에서 '/member/login/?logout'인 경우 로그아웃 메시지 출력
-logout이 null이 아니면, 로그아웃 페이지
-logout이 null이면, 로그인하도록 로그인 페이지
<div class="card-body">
<!-- 문자열 로그아웃이 null이 아니면 로그아웃 메시지 출력-->
<th:block th:if="${param.logout != null}">
<h1>Logout........</h1>
</th:block>
<!-- 문자열 로그아웃이 null이면 로그인 화면 출력-->
<form id="registerForm" action="/member/login" method="post" th:if="${param.logout==null}">
<div class="input-group mb-3">
<span class="input-group-text">아이디</span>
<input type="text" name="username" class="form-control" placeholder="USER ID">
</div>
<div class="input-group mb-3">
<span class="input-group-text">패스워드</span>
<input type="text" name="password" class="form-control" placeholder="PASSWORD">
</div>
<!-- <div class="input-group mb-3 ">-->
<!-- <input class="form-check-input" type="checkbox" name="remember-me">-->
<!-- <label class="form-check-label">-->
<!-- 자동 로그인-->
<!-- </label>-->
<!-- </div>-->
<div class="my-4">
<div class="float-end">
<button type="submit" class="btn btn-primary submitBtn">LOGIN</button>
</div>
</div>
</form>
<!-- <a href="/oauth2/authorization/kakao">KAKAO</a>-->
</div><!--end card body-->
-> <th:block> : 블록 처리를 담당하는 태그로 태그안에 자체 태그를 추가하는 태그
자동 로그인
스프링 시큐리티의 'remember-me' 기능
: 쿠키를 이용해 브라우저에 로그인한 정보를 기억해 로그인 상태를 유지하는 기능
기존 방법 | 스프링 시큐리티 |
HttpSession | 쿠키 |
세션이 성립하는 기간동안 유지 | 쿠키에 유효기간을 지정해 브라우저가 쿠키를 저장 (로그인 관련 정보를 유지) |
1. 커스텀 로그인 페이지를 이용하는 경우에 필요한 설정
-쿠키를 저장하는 테이블 생성 (추후에 토큰을 이용한 JWT로 전환)
-persistent_logins 테이블 생성
create table persistent_logins
(
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null
)
2. 자동 로그인 설정 변경
(1) CustomSecurityConfig에서 쿠키를 발행하도록 내용을 수정
-쿠키 관련 정보를 테이블로 보관하도록 지정
-DataSource, UserDetailsService 객체를 이용해 쿠키 정보를 저장
- 메서드 명 : CustomSecurityConfig
- 의존성 주입 객체 : DataSource, CustomUserDetailsService
- 빈으로 등록 : SecurityFilterChain, WebSecurityCustomizer, PersistentTokenRepository
- 쿠키 생성하기 위한 요소
- 쿠키 값을 인코딩하기 위한 키 값
- 필요한 정보를 저장하는 tokenRepository 지정
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
log.info("---configure---");
//별도의 로그인 페이지를 이용해 로그인 처리하도록 변경
http.formLogin().loginPage("/member/login");
//remember-me 기능을 위한 쿠키 생성
http.rememberMe()
.key("12345678")
.tokenRepository(persistentTokenRepository())
.userDetailsService(userDetailsService)
.tokenValiditySeconds(60*60*24*30);
return http.build();
}
- rememberMe()
- key
- tokenRepository
- userDetailsService
- tokenValiditySeconds
//쿠키 저장을 위한 메서드
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
repo.setDataSource(dataSource);
return repo;
}
(2) remember-me 기능 활성화를 위해 로그인 화면에서 'remember-me' 이름의 값을 전달하기 위한 체크박스 추가
<!-- 문자열 로그아웃이 null이면 로그인 화면 출력-->
<form id="registerForm" action="/member/login" method="post" th:if="${param.logout==null}">
<div class="input-group mb-3">
<span class="input-group-text">아이디</span>
<input type="text" name="username" class="form-control" placeholder="USER ID">
</div>
<div class="input-group mb-3">
<span class="input-group-text">패스워드</span>
<input type="text" name="password" class="form-control" placeholder="PASSWORD">
</div>
<div class="input-group mb-3 ">
<input class="form-check-input" type="checkbox" name="remember-me">
<label class="form-check-label">
자동 로그인
</label>
</div>
<div class="my-4">
<div class="float-end">
<button type="submit" class="btn btn-primary submitBtn">LOGIN</button>
</div>
</div>
</form>
컨트롤러와 화면에서 인증 처리
- 로그인 유무와 권한에 따른 제어
- 컨트롤러 인증된 정보 활용하는 경우의 처리
화면 | 컨트롤러 |
게시물 작성자만 수정/삭제 버튼 노출 게시물 작성시, 사용자의 정보 세팅 |
현재 로그인한 사용자 저어보와 게시물 작성자가 같을 때만 삭제 처리 URL을 통해 권한이 없거나 인가가 없는 사용자의 부적절한 접근 차단 |
타임리프에서 인증 정보 활용
- 게시물 CRUD
- AccessDeniedHandler
타임리프 스프링 시큐리티 라이브러리 의존성 추가
-타임리프 의존성 추가되어있는지 확인
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
//타임리프 스프링 시큐리티
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
게시물 CRUD
1. 게시물 등록 작업
(1) 자바스크립트에서 게시물 등록 인증 정보 생성
//현재 사용자 정보 생성
const auth =[[${#authentication.principal}]]
const errors = [[${errors}]]
console.log(errors)
auth 정보 : password, username, authorities, accountNonExpired, accountNonLocked, credentialsNonExpired, enabled
[Username=user1, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_USER]] |
(2) 사용자 정보를 가져오기 위해 타임리프 스프링 시큐리티 네임스페이스 추가
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
layout:decorate="~{layout/basic.html}">
(3) 게시물 작성자 부분을 현재 로그인한 사용자 아이디로 처리
변경 전,
<!--작성자-->
<div class="input-group mb-3">
<div class="input-group-text">Writer</div>
<input type="text" name="writer" class="form-control" placeholder="Writer">
</div>
변경 후,
<div class="input-group mb-3">
<div class="input-group-text">Writer</div>
<input type="text" name="writer" class="form-control" placeholder="Writer" sec:value="${#authentication.principal.username}" readonly>
</div>
2. 게시물 조회 작업
(1) BoardController에서 로그인한 사용자만 조회할 수 있도록 수정
-@PreAuthorize와 isAuthenticated() 표현식으로 로그인한 사용자만 제한
@PreAuthorize("isAuthenticated()")
@GetMapping({"/read", "/modify"})
public void read(Long bno, PageRequestDTO pageRequestDTO, Model model){
BoardDTO boardDTO=boardService.readOne(bno);
log.info(boardDTO);
model.addAttribute("dto", boardDTO);
}
게시물 조회시 인증 유무에 따른 처리
- 로그인한 사용자
- 조회 페이지
- 로그인하지 않는 사용자
- 로그인 페이지
(2) read.html 처리
-현재 로그인한 사용자와 게시물 작성자가 같은 경우만 Modify 버튼 활성화
-현재 로그인한 사용자와 댓글 작성자가 같은 경우만 Modify/Remove 버튼 활성화
- 글 작성자만 수정 버튼 활성화
- 댓글 작성자는 로그인한 사용자로 설정
- 댓글 수정/삭제는 댓글 작성자와 로그인한 사용자가 동일한 사용자 일경우
스프링 시큐리티 네임스페이스 추가
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
xmlns:th="http://www.thymeleaf.org"
layout:decorate="~{layout/basic.html}">
1. 글 작성자만 수정 버튼 활성화
변경 전, 수정 버튼
<!--목록 / 수정 버튼-->
<div class="my-4">
<div class="float-end" th:with="link = ${pageRequestDTO.getLink()}">
<a th:href="|@{/board/list}?${link}|" class="text-decoration-none">
<button type="button" class="btn btn-primary">List</button>
</a>
<a th:href="|@{/board/modify(bno=${dto.bno})}&${link}|" class="text-decoration-none">
<button type="button" class="btn btn-secondary">Modify</button>
</a>
</div>
</div>
변경 후, 수정 버튼
<!--목록 / 수정 버튼-->
<div class="my-4">
<div class="float-end" th:with="link = ${pageRequestDTO.getLink()}">
<a th:href="|@{/board/list}?${link}|" class="text-decoration-none">
<button type="button" class="btn btn-primary">List</button>
</a>
<a th:if="${user!=null && user.username==dto.writer}" th:href="|@{/board/modify(bno=${dto.bno})}&${link}|" class="text-decoration-none">
<button type="button" class="btn btn-secondary">Modify</button>
</a>
</div>
</div>
2. 댓글 작성자는 로그인한 사용자로 설정
변경 전, 댓글 작성자
<!-- 댓글 등록을 위한 모달창-->
<div class="modal registerModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Register Reply</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="input-group mb-3">
<span class="input-group-text">Reply Text</span>
<!-- <p>Modal body text goes here.</p>-->
<input type="text" class="form-control replyText">
</div>
<div class="input-group mb-3">
<span class="input-group-text">Replyer</span>
<!-- <p>Modal body text goes here.</p>-->
<input type="text" class="form-control replyer">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary registerBtn">Register</button>
<button type="button" class="btn btn-online-dark closeRegisterBtn">Close</button>
</div>
</div>
</div>
</div>
변경 후, 댓글 작성자를 현재 로그인 사용자로 변경
<!-- 댓글 등록을 위한 모달창-->
<div class="modal registerModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Register Reply</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="input-group mb-3">
<span class="input-group-text">Reply Text</span>
<!-- <p>Modal body text goes here.</p>-->
<input type="text" class="form-control replyText">
</div>
<div class="input-group mb-3" th:with="user=${#authentication.principal}">
<span class="input-group-text">Replyer</span>
<!-- <p>Modal body text goes here.</p>-->
<input type="text" class="form-control replyer" th:value="${user.username}" readonly>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary registerBtn">Register</button>
<button type="button" class="btn btn-online-dark closeRegisterBtn">Close</button>
</div>
</div>
</div>
</div>
3. 댓글 수정/삭제는 댓글 작성자와 로그인한 사용자가 동일한 사용자 일경우에만 활성화
- 자바스크립트를 이용한 댓글 수정/삭제 처리
- 현재 로그인한 사용자의 아이디를 변수로 지정
- 댓글 작성자와 현재 로그인한 사용자(currentUser)가 동일한 유저인지 확인해 수정/삭제 제어
- hasAuth의 참거짓으로 수정/삭제 버튼 클릭시 이벤트 처리
- 자바스크립트에서 '==', '==='
- '==' : 두 변수의 [값] 비교
- '===' : 두 변수의 [값 & 자료형] 비교
(1) 현재 로그인한 사용자의 아이디를 변수로 지정
//+) 로그인 유무와 특정 권한별 제어를 위한 현재 로그인한 사용자 변수 설정
const currentUser = [[${#authentication.principal.username}]]
// 댓글 작성자와 현재 로그인한 유저 일치여부를 기본값으로 false로 지정
let hasAuth = false
+) 댓글 내용을 띄울 때, 참거짓 설정
//댓글 내용을 모달창에 띄우기
//+) 댓글 작성자와 현재 로그인한 사용자의 일치 여부 판단
getReply(rno).then(reply =>{
console.log(reply)
//댓글을 구별하기 위한 변수
replyHeader.innerHTML=reply.rno
//String값은 value로 접근
modifyText.value=reply.replyText
//댓글 내용을 채운 모달창 띄우기
modifyModal.show()
//현재 사용자와 댓글 작성자의 값&유형 비교
//같으면 true, 다르면 false
hasAuth=currentUser===reply.replyer
}).catch(e=>alert('error'))
},false)
(2) 댓글 작성자와 현재 로그인한 사용자(currentUser)가 동일한 유저인지 확인해 수정/삭제 제어
수정
//수정 버튼 이벤트 처리
modifyBtn.addEventListener("click",function (e){
if(!hasAuth){
alert("댓글 작성자만 수정이 가능합니다.")
modifyModal.hide()
return
}
//입력받은 내용을 필요한 파라미터를 채워서 객체로 전달
//: rno를 replyHeader의 주소로 지정한다.
const replyObj={
bno:bno,
rno:replyHeader.innerHTML,
replyText:modifyText.value
}
//수정한 댓글 출력하고, 댓글을 수정하던 페이지 정보를 이용해 이전 페이지로 이동
modifyReply(replyObj).then(result =>{
alert(result.rno+' 번 댓글이 수정되었습니다.')
replyText.value=''
modifyModal.hide()
printReplies(page,size)
}).catch(e=> {
console.log(e)
})
},false)
삭제
//삭제 버튼 클릭시 모달창 이벤트 처리
removeBtn.addEventListener("click", function (e){
if(!hasAuth){
alert("댓글 작성자만 삭제가 가능합니다.")
modifyModal.hide()
return
}
//rno
removeReply(replyHeader.innerHTML).then(result=>{
alert(result.rno+' 번 댓글이 삭제되었습니다.')
//댓글 삭제 후, 변수 초기화
replyText.value=''
modifyModal.hide()
//1페이지로 이동하기 위한 변수
page=1
//댓글 목록 출력
printReplies(page, size)
})
},false)
사용자에 따른 제어를 적용한 댓글 모달창
//댓글 조회/수정/삭제 모달창 띄우기
//댓글 조회/수정/삭제 클래스 모달창/속성값 변수 설정
const modifyModal = new bootstrap.Modal(document.querySelector(".modifyModal"))
const replyHeader = document.querySelector(".replyHeader")
const modifyText = document.querySelector(".modifyText")
const modifyBtn = document.querySelector(".modifyBtn")
const removeBtn = document.querySelector(".removeBtn")
const closeModifyBtn = document.querySelector(".closeModifyBtn")
//+) 로그인 유무와 특정 권한별 제어를 위한 현재 로그인한 사용자 변수 설정
const currentUser = [[${#authentication.principal.username}]]
// 댓글 작성자와 현재 로그인한 유저 일치여부를 기본값으로 false로 지정
let hasAuth = false
//댓글 클릭시 모달창 띄우기
replyList.addEventListener("click", function (e){
e.preventDefault()
e.stopPropagation()
const target=e.target
if(!target || target.tagName!='SPAN'){
return
}
const rno = target.getAttribute("data-rno")
if(!rno){
return
}
//댓글 내용을 모달창에 띄우기
//+) 댓글 작성자와 현재 로그인한 사용자의 일치 여부 판단
getReply(rno).then(reply =>{
console.log(reply)
//댓글을 구별하기 위한 변수
replyHeader.innerHTML=reply.rno
//String값은 value로 접근
modifyText.value=reply.replyText
//댓글 내용을 채운 모달창 띄우기
modifyModal.show()
//현재 사용자와 댓글 작성자의 값&유형 비교
//같으면 true, 다르면 false
hasAuth=currentUser===reply.replyer
}).catch(e=>alert('error'))
},false)
//수정 버튼 이벤트 처리
modifyBtn.addEventListener("click",function (e){
if(!hasAuth){
alert("댓글 작성자만 수정이 가능합니다.")
modifyModal.hide()
return
}
//입력받은 내용을 필요한 파라미터를 채워서 객체로 전달
//: rno를 replyHeader의 주소로 지정한다.
const replyObj={
bno:bno,
rno:replyHeader.innerHTML,
replyText:modifyText.value
}
//수정한 댓글 출력하고, 댓글을 수정하던 페이지 정보를 이용해 이전 페이지로 이동
modifyReply(replyObj).then(result =>{
alert(result.rno+' 번 댓글이 수정되었습니다.')
replyText.value=''
modifyModal.hide()
printReplies(page,size)
}).catch(e=> {
console.log(e)
})
},false)
//닫기 버튼 클릭시 모달창 이벤트 처리
closeModifyBtn.addEventListener("click", function (e){
modifyModal.hide()
},false)
//삭제 버튼 클릭시 모달창 이벤트 처리
removeBtn.addEventListener("click", function (e){
if(!hasAuth){
alert("댓글 작성자만 삭제가 가능합니다.")
modifyModal.hide()
return
}
//rno
removeReply(replyHeader.innerHTML).then(result=>{
alert(result.rno+' 번 댓글이 삭제되었습니다.')
//댓글 삭제 후, 변수 초기화
replyText.value=''
modifyModal.hide()
//1페이지로 이동하기 위한 변수
page=1
//댓글 목록 출력
printReplies(page, size)
})
},false)
3. 게시물 수정 작업
현재 로그인한 사용자와 게시물의 작성자가 일치할 때만 수정/삭제 가능하도록
BoardController의 @PreAuthorize을 이용한다.
modify 메서드에 @PreAuthorize의 표현식을 로그인한 사용자가 게시물의 작성자일 때로 작성해 어노테이션 설정
- '#boardDTO' : 현재 파라미터가 수집된 boardDTO 객체를 뜻한다.
@PreAuthorize("principal.username==#boardDTO.writer")
@PostMapping("/modify")
public String modify(PageRequestDTO pageRequestDTO, @Valid BoardDTO boardDTO, BindingResult bindingResult, RedirectAttributes redirectAttributes){
log.info("board modify post..."+boardDTO);
//검증 실패시 - 에러 메시지 전송 및 이전 페이지로 이동
if(bindingResult.hasErrors()) {
log.info("modify post has errors.......");
String link = pageRequestDTO.getLink();
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors() );
redirectAttributes.addAttribute("bno", boardDTO.getBno());
return "redirect:/board/modify?"+link;
}
//검증 성공시 - 수정 수행
boardService.modify(boardDTO);
//수정 완료 메시지 전송
redirectAttributes.addFlashAttribute("result", "modified");
//수정한 게시물 번호 전송
redirectAttributes.addAttribute("bno", boardDTO.getBno());
//수정 완료시 조건없이 read 페이지로 이동
return "redirect:/board/read";
}
-> @PathAuthorize 어노테이션은 URL로 다른 사용자의 게시물의 수정페이지로 이동을 하려고 할 때 에러를 발생한다.
4. 접근거부핸들러 AccessDeniedHandler
-403에러와 같이 서버에서 사용자의 요청의 거부에 대한 에러로, 원인이 다양하다.
403 에러 원인
- 사용자가 권한이 없을 경우
- 특정 조건이 맞지 않는 경우
AccessDeniedHandler : 에러페이지를 대체하고, 403 에러 원인에 따라 처리하기 위한 인터페이스
- <form>태그 요청이 403일 경우
- 로그인 페이지 이동시 'ACCESS_DENIED'값을 파라미터로 전달
- Ajax인 경우
- JSON 데이터를 만들어서 전송
(1) /security/handler/Custom403Handler 클래스 작성
-"Content-Type"이 "application/json"으로 시작하는지 확인해 JSON 데이터 유무 파악
-JSON 데이터가 아니면 로그인 페이지로 리다이렉트
-추후에 JSON 데이터의 경우 처리
package org.zerock.b01.security;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Log4j2
public class Custom403Handler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.info("---ACCESS DENIED---");
response.setStatus(HttpStatus.FORBIDDEN.value());
//JSON 요청인지 확인
String contentType = request.getHeader("Content-Type");
boolean jsonRequest = contentType.startsWith("application/json");
log.info("isJSON : "+jsonRequest);
//일반 request
if(!jsonRequest){
response.sendRedirect("/member/login?error=ACCESS_DENIED");
}
}
}
(2) 403핸들러를 동작하기 위한 CustomSecurityConfig에 필요한 시큐리티 설정
-빈 처리
-예외 처리
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
log.info("---configure---");
//별도의 로그인 페이지를 이용해 로그인 처리하도록 변경
http.formLogin().loginPage("/member/login");
//remember-me 기능을 위한 쿠키 생성
http.rememberMe()
.key("12345678")
.tokenRepository(persistentTokenRepository())
.userDetailsService(userDetailsService)
.tokenValiditySeconds(60*60*24*30);
//csrf 토큰 비활성화
http.csrf().disable();
//403 에러를 커스텀 핸들러로 예외처리
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
return http.build();
}
//403 에러의 커스텀 핸들러
@Bean
public AccessDeniedHandler accessDeniedHandler(){
return new Custom403Handler();
}
-> 403 핸들러에 지정한 작업인 로그인 페이지로 이동을 수행한다.
4. 게시물 삭제 처리
게시물 번호를 이용한 삭제처리 -> 작성자를 추가로 설정해 @PreAuthorize로 권한 확인
//이미지 포함된 게시물 삭제하도록 파라미터를 bno에서 BoardDTO로 변경
//: 게시물 삭제 후, 첨부파일 삭제 처리
//+) 권한을 가진 사용자만 삭제하도록 @PreAuthorize 표현식 추가
@PreAuthorize("principal.username==#boardDTO.writer")
@PostMapping("/remove")
public String remove(BoardDTO boardDTO, RedirectAttributes redirectAttributes){
Long bno=boardDTO.getBno();
log.info("remove post..."+bno);
boardService.remove(bno);
//게시물이 DB상에 삭제되었다면 첨부파일 삭제하도록 처리
log.info(boardDTO.getFileNames());
List<String> fileNames=boardDTO.getFileNames();
if(fileNames !=null && fileNames.size()>0){
//파일을 삭제하는 메서드 호출
removeFiles(fileNames);
}
redirectAttributes.addFlashAttribute("result", "removed");
return "redirect:/board/list";
}
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
8장 -4. 회원 가입 처리 (0) | 2022.12.15 |
---|---|
8장-3. 회원 데이터 처리 (+ UserDetailsService, enum, MSA) (1) | 2022.12.14 |
8장-1. 스프링 시큐리티 (+ SecurityFilterChain, webSecurityCustomizer(), @EnableGlobalMethodSecurity, CSRF 토큰) (0) | 2022.12.13 |
7장-5. 이미지 추가를 위한 컨트롤러와 화면 처리 (+ 파일명에 언더바가 들어간 경우 에러 발생) (0) | 2022.12.11 |
7장-4. 이전 프로젝트에 이미지 추가 (0) | 2022.12.10 |