IT 성장일기

[Spring] Fetch API로 사용자 정보 일치 여부 확인하기 (feat. 403 Error) 본문

Web/Spring

[Spring] Fetch API로 사용자 정보 일치 여부 확인하기 (feat. 403 Error)

고 양 2023. 9. 8. 11:42
반응형
Fetch API로 사용자 정보 일치 여부 확인하기 (feat. 403 Error)

비밀번호를 입력받아 실시간으로 사용자의 비밀번호와 일치하는지 검사하는 로직을 구현해 봤습니다.
사용 언어 및 개발환경은 Java 17, Spring boot 3, Spring security, JPA입니다.
Spring Security 로그인 인증 관련 403 Error가 발생해 해결 내용도 함께 작성합니다.😊

 

Controller

  • @PostMapping("/password/check") : 보안의 이점을 가지기 위해 Post 방식으로 데이터를 받습니다.
  • @PreAuthorize("isAuthenticated()")  : 인증된 사용자만 해당 URL에 접근할 수 있게 하기 위해 적용했습니다.
  • @ResponseBody  : boolean 값을 Model에 직접 넘겨주기 위해 애너테이션을 적용했습니다.
  • 인증정보(Principal 객체)와 비밀번호를 매개변수로 서비스의 메서드를 호출하고 boolean 값을 리턴합니다. (설레네요 ㅠㅠ)
@PostMapping("/user/password/check")
@PreAuthorize("isAuthenticated()")
@ResponseBody
public boolean isPasswordMatching(Principal principal, @RequestParam String password) {
    return this.userService.isPasswordMatching(principal, password);
}

 

 

Service

  • 인증정보와 비밀번호를 넘겨받아 DB에 저장된 사용자 정보를 가져오고 비밀번호 일치여부를 판단합니다.
public boolean isPasswordMatching(Principal principal, String password) {
    if (principal == null || principal.getName() == null) {
        throw new IllegalArgumentException("Invalid principal");
    }

    Optional<SiteUser> userOptional  = this.userRepository.findByusername(principal.getName());

    if (!userOptional .isPresent()) {
        throw new DataNotFoundException("User not found.");
    }

    SiteUser user = userOptional.get();
    String storedPassword = user.getPassword();

    return passwordEncoder.matches(password, storedPassword);
}

 

 

Template

  • 현재 사용 중인 비밀번호를 입력받을 input 요소입니다.
<div class="mb-3">
    <label for="currentPassword" class="form-label">Current password</label>
    <input type="password" class="form-control currentPassword">
</div>

 

Javascript

  1. 여러 개의 input 요소를 querySelectorAll 메서드를 이용해 모두 가져옵니다.
  2. forEach문을 활용해서 각각의 요소에 EventListener를 붙여줬습니다.
  3. input 창에 입력을 할 때마다 위에 작성한 Controller의 메서드를 호출해 결괏값을 리턴 받아 요소에 표시해 줍니다.
document.addEventListener("DOMContentLoaded", function() {
    const passwordInputs = document.querySelectorAll(".currentPassword");

    passwordInputs.forEach(input => {
        input.addEventListener("input", function() {
            const password = input.value;

            fetch("/user/password/check", {
                method: "POST",
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                },
                body: `password=${encodeURIComponent(password)}`
            })
            .then(response => response.json())
            .then(data => {
                if (data === true) {
                    input.classList.add("border-success");
                    input.classList.remove("border-danger");
                } else {
                    input.classList.add("border-danger");
                    input.classList.remove("border-success");
                }
            })
            .catch(error => {
                console.error("Error:", error);
            });
        });
    });
});

 

여기까지 작성했을 때 로직상으로 아무 문제도 없었지만 콘솔창에는 403 에러가 발생했습니다.

비밀번호 입력
케찹을 뿌린 듯한 콘솔창

 

콘솔을 뜯어보면 아래와 같이 fetch 요청을 보내는 부분에서 에러가 발생하고 있었습니다.

그래서 방법이 있을 것 아니에요

Spring Security에서 CSRF 방어를 위해서 CSRF 토큰을 사용하는 경우
이렇게 요청을 보낼 때 CSRF 토큰을 포함해야 한다고 합니다.


Security Configuration에서 CSRF를 사용하지 않도록 설정하는 방법도 있었지만
보안상 불리한 경우가 생길 수 있을 것 같아서 토큰을 포함하는 방법으로 해결했습니다.

 


Template

  • head 태그 내부에 CSRF 토큰 정보를 포함시켜 줍니다.
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
	<meta name="_csrf" th:content="${_csrf.token}">
	<meta name="_csrf_header" th:content="${_csrf.headerName}">
	<link rel="stylesheet" type="text/css" th:href="@{/style.css}">
	<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}">
</head>

 

Javascript

  • 요청 시 토큰 값을 header에 담아줍니다.
document.addEventListener("DOMContentLoaded", function() {
    const passwordInputs = document.querySelectorAll(".currentPassword");

    passwordInputs.forEach(input => {
        input.addEventListener("input", function() {
            const password = input.value;
            const csrfToken = document.querySelector("[name='_csrf']").getAttribute("content"); // CSRF 토큰 가져오기

            fetch("/user/password/check", {
                method: "POST",
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                    "X-CSRF-TOKEN": csrfToken // CSRF 토큰 추가
                },
                body: `password=${encodeURIComponent(password)}`
            })
            .then(response => response.json())
            .then(data => {
                if (data === true) {
                    input.classList.add("border-success");
                    input.classList.remove("border-danger");
                } else {
                    input.classList.add("border-danger");
                    input.classList.remove("border-success");
                }
            })
            .catch(error => {
                console.error("Error:", error);
            });
        });
    });
});

 

결과적으로 해결된 것 같습니다.

도움이 되어드렸다면 구독 / 좋아요 부탁드립니다.👏🏻

감사합니다.🙂

 

반응형