해당 게시글은 점프 투 스프링부트 교재를 통한 개인 학습 용도이며 기초 세팅은 생략하였습니다.
자바 8, 스프링부트 2.7.7버전 입니다.
Question 속성 추가
Question 엔티티에 author 속성을 추가하자.
(... 생략 ...)
import javax.persistence.ManyToOne;
import com.example.board.user.SiteUser;
(... 생략 ...)
public class Question {
(... 생략 ...)
@ManyToOne
private SiteUser author;
}
여러개의 질문이 한 명의 사용자에게 작성될 수 있으므로 @ManyToOne 관계가 성립한다.
Answer 속성 추가
Answer 엔티티에도 추가해주자.
(... 생략 ...)
import com.example.board.user.SiteUser;
(... 생략 ...)
public class Answer {
(... 생략 ...)
@ManyToOne
private SiteUser author;
}
author 저장
답변에 작성자 저장하기
먼저 답변을 작성하는 AnswerController를 수정하자.
(... 생략 ...)
import java.security.Principal;
(... 생략 ...)
public class AnswerController {
(... 생략 ...)
@PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id,
@Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
(... 생략 ...)
}
}
현재 로그인한 사용자에 대한 정보를 알기 위해서는 스프링 시큐리티가 제공하는 Principal 객체를 사용해야 한다.
principal.getName()을 호출하면 현재 로그인한 사용자의 사용자명(사용자ID)을 알수 있다.
principal 객체를 사용하면 이제 로그인한 사용자의 사용자명을 알수 있으므로 사용자명을 통해 SiteUser객체를 조회할 수 있다.
SiteUser를 조회할 수 있는 getUser 메서드를 UserService에 추가하자.
(... 생략 ...)
import java.util.Optional;
import com.example.board.practice.DataNotFoundException;
(... 생략 ...)
@Service
public class UserService {
(... 생략 ...)
public SiteUser getUser(String username) {
Optional<SiteUser> siteUser = this.userRepository.findByusername(username);
if (siteUser.isPresent()) {
return siteUser.get();
} else {
throw new DataNotFoundException("siteuser not found");
}
}
}
UserRepository에 이미 findByusername을 선언했으므로 쉽게 만들수 있다.
답변 저장시 작성자를 저장할 수 있도록 다음과 같이 AnswerService를 수정하자.
(... 생략 ...)
import com.example.board.user.SiteUser;
(... 생략 ...)
public class AnswerService {
(... 생략 ...)
public Answer create(Question question, String content, SiteUser author) {
Answer answer = new Answer();
answer.setContent(content);
answer.setCreateDate(LocalDateTime.now());
answer.setQuestion(question);
answer.setAuthor(author);
this.answerRepository.save(answer);
return answer;
}
}
create 메서드에 SiteUser 객체를 추가로 전달받아 답변 저장시 author 속성에 세팅했다.
이제 답변을 작성하면 작성자도 함께 저장될 것이다.
AnswerController의 createAnswer 메서드를 완성해 보자.
(... 생략 ...)
import com.example.board.user.SiteUser;
import com.example.board.user.UserService;
(... 생략 ...)
public class AnswerController {
private final QuestionService questionService;
private final AnswerService answerService;
private final UserService userService;
@PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id,
@Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
Question question = this.questionService.getQuestion(id);
SiteUser siteUser = this.userService.getUser(principal.getName());
if (bindingResult.hasErrors()) {
model.addAttribute("question", question);
return "/question/question_detail";
}
this.answerService.create(question, answerForm.getContent(), siteUser);
return String.format("redirect:/question/detail/%s", id);
}
}
principal 객체를 통해 사용자명을 얻은 후에 사용자명을 통해 SiteUser 객체를 얻어서 답변을 등록하는 AnswerService의 create 메서드에 전달하여 답변을 저장하도록 했다.
질문에 작성자 저장하기
QuestionService를 수정하자.
(... 생략 ...)
import com.example.board.user.SiteUser;
(... 생략 ...)
public class QuestionService {
(... 생략 ...)
public void create(String subject, String content, SiteUser user) {
Question q = new Question();
q.setSubject(subject);
q.setContent(content);
q.setCreateDate(LocalDateTime.now());
q.setAuthor(user);
this.questionRepository.save(q);
}
}
create 메서드에 SiteUser 매개변수를 추가하여 Question 데이터를 생성했다.
이어서 QuestionController도 다음과 같이 수정하자.
(... 생략 ...)
import java.security.Principal;
import com.example.board.user.SiteUser;
import com.example.board.user.UserService;
(... 생략 ...)
public class QuestionController {
private final QuestionService questionService;
private final UserService userService;
(... 생략 ...)
@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm,
BindingResult bindingResult, Principal principal) {
if (bindingResult.hasErrors()) {
return "/question/question_form";
}
SiteUser siteUser = this.userService.getUser(principal.getName());
this.questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser);
return "redirect:/question/list";
}
}
QuestionService의 create 메서드의 매개변수로 SiteUser가 추가되었기 때문에 이전에 작성한 테스트 케이스가 오류가 발생할 것이다. 테스트 케이스의 오류를 임시 해결하기 위해 다음과 같이 수정하자.
package com.example.board;
(... 생략 ...)
@Test
void testJpa() {
for (int i = 1; i <= 300; i++) {
String subject = String.format("테스트 데이터입니다:[%03d]", i);
String content = "내용무";
this.questionService.create(subject, content, null);
}
}
}
로그인이 필요한 메서드
로그아웃 상태에서 질문 또는 답변을 등록하면 서버오류(500 오류)가 발생한다.
이 오류는 principal 객체가 널(null)값이라서 발생한 오류이다.
principal 객체는 로그인을 해야만 생성되는 객체이기 때문이다.
이 문제를 해결하려면 principal 객체를 사용하는 메서드에 @PreAuthorize("isAuthenticated()") 애너테이션을 사용해야 한다. @PreAuthorize("isAuthenticated()") 애너테이션이 붙은 메서드는 로그인이 필요한 메서드를 의미한다.
@PreAuthorize("isAuthenticated()") 애너테이션이 적용된 메서드가 로그아웃 상태에서 호출되면 로그인 페이지로 이동된다.
QuestionController를 다음과 같이 수정하자.
package com.example.board.question;
(... 생략 ...)
import org.springframework.security.access.prepost.PreAuthorize;
(... 생략 ...)
public class QuestionController {
(... 생략 ...)
@PreAuthorize("isAuthenticated()")
@GetMapping("/create")
public String questionCreate(QuestionForm questionForm) {
return "/question/question_form";
}
@PreAuthorize("isAuthenticated()")
@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm,
BindingResult bindingResult, Principal principal) {
(... 생략 ...)
}
}
로그인이 필요한 메서드들에 @PreAuthorize("isAuthenticated()") 애너테이션을 적용했다.
AnswerController도 다음과 같이 수정하자.
(... 생략 ...)
import org.springframework.security.access.prepost.PreAuthorize;
(... 생략 ...)
public class AnswerController {
(... 생략 ...)
@PreAuthorize("isAuthenticated()")
@PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id, @Valid AnswerForm answerForm,
BindingResult bindingResult, Principal principal) {
(... 생략 ...)
}
}
@PreAuthorize 애너테이션이 동작할 수 있도록 SecurityConfig를 다음과 같이 수정해야 한다.
(... 생략 ...)
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
(... 생략 ...)
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
(... 생략 ...)
}
SecurityConfig에 적용한@EnableMethodSecurity(prePostEnabled = true) 설정은 QuestionController와 AnswerController에서 로그인 여부를 판별하기 위해 사용했던 @PreAuthorize 애너테이션을 사용하기 위해 반드시 필요하다.
이렇게 수정한후 로그아웃 상태에서 질문을 등록하거나 답변을 등록하면 자동으로 로그인 화면으로 이동하는 것을 확인할 수 있을 것이다.
로그인 하지 않은 상태에서 "질문 등록" 버튼을 누르면 "로그인" 화면으로 이동한다. 그리고 로그인을 진행하면 원래 하려고 했던 "질문 등록" 화면으로 이동한다. 이것은 로그인 후에 원래 하려고 했던 페이지로 리다이렉트 시키는 스프링 시큐리티의 기능이다.
disabled
작성한 글이 사라지는 문제를 해결하려면 로그아웃 상태에서는 아예 답변 작성을 못하게 막는 것이 좋을 것이다.
question_detail.html 파일을 다음과 같이 수정하자.
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
(... 생략 ...)
<!-- 답변 작성 -->
<form th:action="@{|/answer/create/${question.id}|}" th:object="${answerForm}" method="post" class="my-3">
<div th:replace="~{form_errors :: formErrorsFragment}"></div>
<textarea sec:authorize="isAnonymous()" disabled th:field="*{content}" class="form-control" rows="10"></textarea>
<textarea sec:authorize="isAuthenticated()" th:field="*{content}" class="form-control" rows="10"></textarea>
<input type="submit" value="답변등록" class="btn btn-primary my-2">
</form>
</div>
</html>
로그인 상태가 아닌 경우 textarea 태그에 disabled 속성을 적용하여 입력을 못하게 만들었다.
현재 사용자의 로그인 상태를 체크하는 속성
sec:authorize="isAnonymous()" : 현재 로그아웃 상태
sec:authorize="isAuthenticated()" : 현재 로그인 상태
2023.04 ~ 백엔드 개발자의 기록
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!