spring Security 4. - log-in 접근 권한

알파로그·2023년 3월 22일
0

Spring Boot

목록 보기
26/57
post-custom-banner

클라이언트의 로그인 여부와 로그인 했을경우 정보를 조회하는 방법

  • 목표
    • Answer 를 작성하면 SiteUser 의 정보를 조회해 로그인 회원일경우 Answer 에 Username 을 표시하는 기능을 구현
    • log-out 상태로 게시물 작성을 시도하면 log-in 페이지로 이동후,
      log-in 을 완료하면 요청했던 페이지로 이동
    • answer 의 답글작성 공간이 log-out 회원에게 노출되지 않음

✏️ Service 계층

📍 UserService

  • user 의 이름을 찾기위한 로직 추가
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository repository;
    private final PasswordEncoder passwordEncoder;

		....

	//-- username 으로 user 조회 --//
    public SiteUser getUser(String username) {
        Optional<SiteUser> siteUser = repository.findByUsername(username);
        if (siteUser.isPresent())
            return siteUser.get();
        else
            throw new DataNotFoundException("SiteUser not found");
    }
}

📍 AnswerService

  • Answer 를 생성할 때 username (author) 를 저장
@Service
@RequiredArgsConstructor
public class AnswerService {

    private final AnswerRepository answerRepository;

    //-- answer 생성 --//
    public void 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);
    }
}

📍 Controller 계층

@RequestMapping("/answer")
@RequiredArgsConstructor
@Controller
public class AnswerController {

	//-- DI --//
    private final QuestionService questionService;
    private final AnswerService answerService;
    private final UserService userService;

	//-- Anser 생성 로직 --//
    @PostMapping("/create/{id}")
    public String createAnswer(
            @PathVariable Integer id,
            @Valid AnswerForm answerForm,
            BindingResult bindingResult,
            Principal principal, // Spring Security 가 제공하는 로그인한 사용자의 정보를 확인해주는 객체
            Model model
    ) {
		// answer 를 등록할 Question 을 조회하는 로직
        Question question = this.questionService.getQuestion(id);
        // 현재 로그인한 사용자의 username 을 조회해 SiteUser 를 찾는 로직
        SiteUser siteUser = this.userService.getUser(principal.getName());

		// 클라이언트가 입력 양식에 맞지 않게 작성할 경우 에러 반환
        if (bindingResult.hasErrors()) {
            model.addAttribute("question", question);
            return "question_detail";
        }

		// answer 생성
        this.answerService.create(question, answerForm.getContent(), siteUser);
        return String.format("redirect:/question/detail/%s" , id);
    }
}

⚠️ Question 에도 작성자를 추가하고 싶다면 같은 방식으로 추가해주면 된다.


📍 Web 계층

  • th:if 문을 사용해 log-in 을 해서 등록했다면 글쓴이를 표시하게 변경했다.
  • question_list.html
<html layout:decorate="~{layout}" xmlns:layout="http://www.w3.org/1999/xhtml">
<div layout:fragment="content" class="container my-3">
    <table class="table">
        <thead class="table-dark">
        <tr>
            <th>번호</th>
            <th style="width:50%">제목</th>
            <th>글쓴이</th>
            <th>작성일시</th>
        </tr>
        </thead>
        <tbody>
<!--        question list 시작 -->
        <tr class="text-center" th:each="question, loop : ${paging}">
<!--            question 고유 번호 시작-->
            <td th:text="${paging.getTotalElements - (paging.number * paging.size) - loop.index}"></td>
<!--            question 고유 번호 종료-->
<!--            question 제목 시작-->
            <td class="text-start">
                <a th:href="@{|/question/detail/${question.id}|}" th:text="${question.subject}"></a>
<!--                게시물의 댓글 개수 표시 시작-->
                <span class="danger small ms-2"
                      th:if="${#lists.size(question.answerList)} > 0"
                      th:text="${#lists.size(question.answerList)}">
                </span>
<!--                게시물의 댓글 개수 표시 종료-->
            </td>
<!--            question 제목 종료-->
<!--            글쓴이 표시 시작-->
            <td>
                <span th:if="${question.author != null}" 
                      th:text="${question.author.username}"></span>
            </td>
<!--            글쓴이 표시 종료-->
            <td th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></td>
        </tr>
<!--        question list 종료 -->
        </tbody>
    </table>
<!--    페이징 처리 시작 -->
    <div th:if="${!paging.isEmpty()}">
        <ul class="pagination justify-content-center">
            <li class="page-item" th:classappend="${!paging.hasPrevious()} ? 'disabled'">
                <a class="page-link"
                   th:href="@{|?page=${paging.number-1}|}">
                    <span>이전</span>
                </a>
            </li>
            <li th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}"
                th:if="${page >= paging.number-5 and page <= paging.number+5}"
                th:classappend="${page == paging.number} ? 'active'"
                class="page-item">
                <a th:text="${page}" class="page-link" th:href="@{|?page=${page}|}"></a>
            </li>
            <li class="page-item" th:classappend="${!paging.hasNext} ? 'disabled'">
                <a class="page-link"
                   th:href="@{|?page=${paging.number+1}|}">
                    <span>다음</span>
                </a>
            </li>
        </ul>
    </div>
<!--    페이징 처리 종료-->
    <a th:href="@{/question/create}" class="btn btn-primary">질문 등록하기</a>
</div>
</html>

  • question_detail.html
<html layout:decorate="~{layout}" xmlns:layout="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.w3.org/1999/xhtml">
<div layout:fragment="content" class="container my-3">
    <!-- 질문 -->
    <h2 class="border-bottom py-2" th:text="${question.subject}"></h2>
    <div class="card my-3">
        <div class="card-body">
            <div class="card-text" style="white-space: pre-line;" th:text="${question.content}"></div>
            <div class="d-flex justify-content-end">

<!--                날자, 글슨이 정보 시작-->
                <div class="badge bg-light text-dark p-2 text-start">
    <!--                글쓴이 정보 추가 시작-->
                    <div class="mb-2">
                        <span th:if="${question.author != null}"
                              th:text="${question.author.username}"></span>
                    </div>
    <!--                글쓴이 정보 추가 종료-->
                    <div th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>
                </div>
<!--                날자, 글슨이 정보 종료-->
            </div>
        </div>
    </div>
    <!-- 답변의 갯수 표시 -->
    <h5 class="border-bottom my-3 py-2"
        th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
    <!-- 답변 반복 시작 -->
    <div class="card my-3" th:each="answer : ${question.answerList}">
        <div class="card-body">
            <div class="card-text" style="white-space: pre-line;" th:text="${answer.content}"></div>
            <div class="d-flex justify-content-end">
                <div class="badge bg-light text-dark p-2 text-start">
<!--                    답변자 정보 추가 시작-->
                    <div class="mb-2">
                        <span th:if="${answer.author != null}"
                              th:text="${answer.author.username}"></span>
                    </div>
<!--                    답변자 정보 추가 종료-->
                    <div th:text="${#temporals.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
                </div>
            </div>
        </div>
    </div>
    <!-- 답변 반복 끝  -->
    <!-- 답변 작성 -->
    <form th:action="@{|/answer/create/${question.id}|}" th:object="${answerForm}" method="post" class="my-3">

        <!--        오류 처리 탬플릿 사용 시작-->
        <div th:replace="~{form_errors :: formErrorsFragment}"></div>
        <!--        오류 처리 탬플릿 사용 종료-->

        <textarea th:field="*{content}"
                  class="form-control"
                  rows="10"></textarea>
      
        <input type="submit" value="답변등록" class="btn btn-primary my-2">
    </form>
</div>
</html>

✏️ 권한이 없는 클라이언트에게 log-in 페이지 응답하기

  • log-out 일경우 Princiapl 에 의한 500 Error 가 발생 하게된다.
    • answer 와 question 을 생성할 때 princial.getName 으로 username 을 조회하는데 princial 읙 객체가 null (로그아웃 상태) 일경우 발생하는 오류이다.
    • 문제를 해결하기 위해 접근 권한을 설정해야 한다.

📍 Controller 수정

  • @PreAuthorize("isAuthenticated()")
    • 해당 어노테이션이 선언된 mehod 는 접근하기 위해 log-in 이 필요하다는 의미이다.
    • 만약 log-out 상태로 요청할 경우 로그인 페이지로 이동시킨다.
    • 그 상태에서 log-in 을 할경우 redirect 로 설정한 페이지가 아닌 마지막 요청했던 페이지로 이동하게 된다.
@RequestMapping("/answer")
@RequiredArgsConstructor
@Controller
public class AnswerController {

    private final QuestionService questionService;
    private final AnswerService answerService;
    private final UserService userService;

    @PostMapping("/create/{id}")
    @PreAuthorize("isAuthenticated()") // 접근 권한 설정
    public String createAnswer(
            @PathVariable Integer id,
            @Valid AnswerForm answerForm,
            BindingResult bindingResult,
            Principal principal,
            Model model
    ) {
        Question question = this.questionService.getQuestion(id);
       
        SiteUser siteUser = this.userService.getUser(principal.getName());

        if (bindingResult.hasErrors()) {
            model.addAttribute("question", question);
            return "question_detail";
        }
        this.answerService.create(question, answerForm.getContent(), siteUser);
        return String.format("redirect:/question/detail/%s" , id);
    }
}

📍 Configuration 수정

  • @EnableMethodSecurity(prePostEnabled = true)
    • 로그인 여부를 판별하기 위해 사용한 @PreAuthorize 의 기능을 사용하기위해 반드시 필요하다.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // 접근 권한 설정
public class SecurityConfig {

✏️ log-out 사용자에게 answer 입력란 비공개 하기

  • disabled
    • text area 를 비활성화 하는 기능
    • sec 조건문을 사용해 log-out 상태일 때 비공개로 설정함
  • sec:authorize="isAnonymous()”
    • log-out 일경우 true
  • sec:authorize="isAuthenticated()"
    • log-in 이면 true
<html layout:decorate="~{layout}" xmlns:layout="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.w3.org/1999/xhtml">
<div layout:fragment="content" class="container my-3">
    <!-- 질문 -->
    <h2 class="border-bottom py-2" th:text="${question.subject}"></h2>
    <div class="card my-3">
        <div class="card-body">
            <div class="card-text" style="white-space: pre-line;" th:text="${question.content}"></div>
            <div class="d-flex justify-content-end">
                <div class="badge bg-light text-dark p-2 text-start">
                    <div th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>
                </div>
            </div>
        </div>
    </div>
    <!-- 답변의 갯수 표시 -->
    <h5 class="border-bottom my-3 py-2"
        th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
    <!-- 답변 반복 시작 -->
    <div class="card my-3" th:each="answer : ${question.answerList}">
        <div class="card-body">
            <div class="card-text" style="white-space: pre-line;" th:text="${answer.content}"></div>
            <div class="d-flex justify-content-end">
                <div class="badge bg-light text-dark p-2 text-start">
                    <div th:text="${#temporals.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
                </div>
            </div>
        </div>
    </div>
    <!-- 답변 반복 끝  -->
    <!-- 답변 작성 -->
    <form th:action="@{|/answer/create/${question.id}|}" th:object="${answerForm}" method="post" class="my-3">

        <!--        오류 처리 탬플릿 사용 시작-->
        <div th:replace="~{form_errors :: formErrorsFragment}"></div>
        <!--        오류 처리 탬플릿 사용 종료-->

<!--        log-out 클라이언트에게 답변 공간 비공개 시작-->
        <textarea sec:authorize="isAnonymous()" disabled
                  th:field="*{content}"
                  class="form-control"
                  rows="10"></textarea>
      
      <textarea sec:authorize="isAuthenticated()"
                  th:field="*{content}"
                  rows="10"
                  class="form-control" ></textarea>
<!--        log-out 클라이언트에게 답변 공간 비공개 시작-->

        <input type="submit" value="답변등록" class="btn btn-primary my-2">
    </form>
</div>
</html>
profile
잘못된 내용 PR 환영
post-custom-banner

0개의 댓글