10.3 Spring Security를 이용한 OAuth2 구현 - OAuth 뷰 구성

SummerToday·2024년 3월 17일
1
post-thumbnail
post-custom-banner

OAuth 뷰 구성

OAuth 구현의 마지막 단계인 OAuth 뷰를 구성한다.


UserViewController 수정

로그인 뷰를 oauthLogin으로 변경한다.

// controller - UserViewController.java

@Controller
public class UserViewController {

    @GetMapping("/login")
    public String login() {
        return "oauthLogin";
    }

    @GetMapping("/signup")
    public String signup() {
        return "signup";
    }
}    

로그인 이미지 다운


oauthLogin.html 작성

위에서 다운받은 이미지를 활용해 로그인 화면에 OAuth 연결 버튼을 생성한다.

// temlplates - oauthLogin.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css">

  <style>
    .gradient-custom {
      background: #6a11cb;
      background: -webkit-linear-gradient(to right, rgba(106, 17, 203, 1), rgba(37, 117, 252, 1));
      background: linear-gradient(to right, rgba(106, 17, 203, 1), rgba(37, 117, 252, 1))
    }
  </style>
</head>
<body class="gradient-custom">
<section class="d-flex vh-100">
  <div class="container-fluid row justify-content-center align-content-center">
    <div class="card bg-dark" style="border-radius: 1rem;">
      <div class="card-body p-5 text-center">
        <h2 class="text-white">LOGIN</h2>
        <p class="text-white-50 mt-2 mb-5">서비스 사용을 위해 로그인을 해주세요!</p>

        <div class = "mb-2">
          <a href="/oauth2/authorization/google">
            <img src="/img/google.png">
          </a>
        </div>
      </div>
    </div>
  </div>
</section>
</body>
</html>

자바스크립트 파일 작성

HTML 파일과 연결할 자바 스크립트 파일을 작성한다. 다음 코드는 파라미터로 받은 토큰이 있다면 토큰을 로컬 스토리지에 저장하는 역할의 코드이다.

// resources- static - js - token.js

const token = searchParam('token')

if (token) {
    localStorage.setItem("access_token", token)
}

function searchParam(key) {
    return new URLSearchParams(location.search).get(key);
}
  • searchParam()
    현재 페이지 URL의 쿼리 문자열을 파싱하여 'token' 매개변수의 값을 반환한다.

  • token 값이 존재하는 경우(즉, URL에 'token' 매개변수가 있는 경우), 이를 로컬 스토리지에 'access_token'이라는 키로 저장한다.


articleList.html 파일 수정

articleList.html에서 token.js를 가져올 수 있도록 파일을 수정한다.

// resources - templates - articleList.html

~ 생략 ~

<script src="/static/token.js"></script>   <!--token.js가 이 화면에서 동작하도록 import-->
<script src="/static/article.js"></script> <!--article.js가 이 화면에서 동작하도록 import-->
</body>

article.js 파일 수정

생성, 수정, 삭제 요청들이 httpRequest() 함수를 사용하도록 코드를 수정한다.

// resources - static - js - article.js

// 삭제 기능
const deleteButton = document.getElementById('delete-btn');

if (deleteButton) {
    deleteButton.addEventListener('click', event => {
        let id = document.getElementById('article-id').value;
        function success() {
            alert('삭제가 완료되었습니다.');
            location.replace('/articles');
        }

        function fail() {
            alert('삭제 실패했습니다.');
            location.replace('/articles');
        }

        httpRequest('DELETE',`/api/articles/${id}`, null, success, fail);
    });
}

// 수정 기능
const modifyButton = document.getElementById('modify-btn');

if (modifyButton) {
    modifyButton.addEventListener('click', event => {
        let params = new URLSearchParams(location.search);
        let id = params.get('id');

        body = JSON.stringify({
            title: document.getElementById('title').value,
            content: document.getElementById('content').value
        })

        function success() {
            alert('수정 완료되었습니다.');
            location.replace(`/articles/${id}`);
        }

        function fail() {
            alert('수정 실패했습니다.');
            location.replace(`/articles/${id}`);
        }

        httpRequest('PUT',`/api/articles/${id}`, body, success, fail);
    });
}

// 생성 기능
const createButton = document.getElementById('create-btn');

if (createButton) {
    // 등록 버튼을 클릭하면 /api/articles로 요청을 보낸다
    createButton.addEventListener('click', event => {
        body = JSON.stringify({
            title: document.getElementById('title').value,
            content: document.getElementById('content').value
        });
        function success() {
            alert('등록 완료되었습니다.');
            location.replace('/articles');
        };
        function fail() {
            alert('등록 실패했습니다.');
            location.replace('/articles');
        };

        httpRequest('POST','/api/articles', body, success, fail)
    });
}


// 쿠키를 가져오는 함수
function getCookie(key) {
    var result = null;
    var cookie = document.cookie.split(';');
    cookie.some(function (item) {
        item = item.replace(' ', '');

        var dic = item.split('=');

        if (key === dic[0]) {
            result = dic[1];
            return true;
        }
    });

    return result;
}

// HTTP 요청을 보내는 함수
function httpRequest(method, url, body, success, fail) {
    fetch(url, {
        method: method,
        headers: { // 로컬 스토리지에서 액세스 토큰 값을 가져와 헤더에 추가
            Authorization: 'Bearer ' + localStorage.getItem('access_token'),
            'Content-Type': 'application/json',
        },
        body: body,
    }).then(response => {
        if (response.status === 200 || response.status === 201) {
            return success();
        }
        const refresh_token = getCookie('refresh_token');
        if (response.status === 401 && refresh_token) {
            fetch('/api/token', {
                method: 'POST',
                headers: {
                    Authorization: 'Bearer ' + localStorage.getItem('access_token'),
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    refreshToken: getCookie('refresh_token'),
                }),
            })
                .then(res => {
                    if (res.ok) {
                        return res.json();
                    }
                })
                .then(result => { // 재발급이 성공하면 로컬 스토리지값을 새로운 액세스 토큰으로 교체
                    localStorage.setItem('access_token', result.accessToken);
                    httpRequest(method, url, body, success, fail);
                })
                .catch(error => fail());
        } else {
            return fail();
        }
    });
}
  • getCookie(key){~}
    해당 함수는 특정 이름(key)에 해당하는 쿠키의 값을 가져오는 기능을 한다.

    • var cookie = document.cookie.split(';');
      현재 페이지의 모든 쿠키를 가져와 세미콜론으로 구분된 문자열로 변환한 후 배열에 저장한다.

    • cookie.some(function (item) { ... })
      some() 메소드를 사용하여 배열 cookie의 각 요소에 대해 주어진 콜백 함수를 실행한다. some() 메소드는 배열 요소를 반복하면서 콜백 함수를 실행하고, 콜백 함수가 true를 반환하면 반복을 중단한다.

    • tem = item.replace(' ', '');
      쿠키의 이름과 값 사이의 공백을 제거하여 쿠키를 파싱할 때 문제가 발생하지 않도록, 현재 반복 중인 쿠키 문자열에서 공백을 제거한다(' '->'').

    • var dic = item.split('=');
      현재 반복 중인 쿠키 문자열을 등호(=)를 기준으로 나누어 이름과 값으로 분리한다.\

    • if (key === dic[0]) { ... }
      현재 쿠키의 이름이 함수에 전달된 key와 일치하는지 확인한다. 이때, dic[0]은 쿠키의 이름을 나타낸다.

    • result = dic[1]; return true;
      이름이 일치하는 경우, 해당 쿠키의 값을 result 변수에 저장하고 true를 반환하여 반복을 중단한다. 이렇게 함으로써 함수는 쿠키를 찾았음을 알리고, 추가적인 반복을 중단한다.


  • httpRequest(method, url, body, success, fail) {~}
    주어진 메서드, URL, 본문(body)을 사용하여 서버로 HTTP 요청을 보내고, 그에 대한 응답을 처리하고, 액세스 토큰이 만료되었거나 재발급이 필요한 경우에 토큰을 갱신하고 재요청을 보내는 함수이다.

    • method
      HTTP 요청 메소드 (GET, POST, PUT, DELETE 등)

    • url
      요청을 보낼 URL

    • body
      요청 본문에 해당하는 데이터

    • success
      요청이 성공했을 때 호출될 콜백 함수

    • success
      요청이 성공했을 때 호출될 콜백 함수

    • fail
      요청이 실패했을 때 호출될 콜백 함수
    • fetch()
      JavaScript에서 네트워크 요청을 보내는 기능을 제공하는 함수이다. 주로 HTTP 요청을 보내고 응답을 처리하는 데 사용된다.

      • 다음과 같은 형식으로 사용한다.
        • fetch(url, option)
          .then(response => {
          // 응답을 처리하는 코드 (응답 성공 시)
          })
          .catch(error => {
          // 오류를 처리하는 코드 ( 오류 발생 시)
          });

        • option 부분에 들어가는 주요 요소
          • method : 요청의 HTTP 메서드를 지정한다. GET, POST, PUT, DELETE 등이 사용된다.
          • headers : 요청 헤더를 나타내는 Headers 객체나 헤더 정보를 포함한 객체를 지정한다.
          • Body : 요청의 본문에 해당하는 데이터를 지정한다.
      • headers
        HTTP 요청 헤더를 설정한다. 해당 경우에는 Authorization 헤더에 액세스 토큰을 포함시키고, Content-Type 헤더를 application/json으로 설정한다. 액세스 토큰은 로컬 스토리지에서 가져온다.


    • then()

      • response.status가 200 또는 201인 경우에는 success() 함수를 호출하여 성공적으로 처리된 것으로 한다.

      • const refresh_token = getCookie('refresh_token');
        쿠키에서 refresh_token을 가져온다.

      • 만약 응답 상태 코드가 401(Unauthorized)이고, 로컬 쿠키에서 refresh_token이 존재한다면, 즉 사용자의 인증 토큰이 만료되었을 때 refresh_token을 사용하여 새로운 액세스 토큰을 발급받아야 한다.

      • fetch('/api/token', { ... }
        새로운 액세스 토큰을 발급받기 위해 /api/token 엔드포인트로 POST 요청을 보낸다.

      • then(res => { if (res.ok) {return res.json();}})
        fetch() 메소드를 통해 받은 응답(res)에 대한 콜백 함수를 정의한다.

        • if (res.ok) { return res.json(); }
          해당 부분은 응답 객체(res)의 ok 속성을 확인하여 응답이 성공적인지 확인한다. 만약 응답이 성공적이라면, res.json()을 호출하여 응답의 본문을 JSON 형식으로 파싱하고 이를 반환한다.

          • res.json()은 Fetch API의 응답 객체(Response) 메소드 중 하나로, HTTP 응답의 본문(body)을 JSON 형식으로 파싱하여 JavaScript 객체로 반환한다.
            일반적으로 HTTP 요청에 대한 응답으로 JSON 형식의 데이터를 받을 때 사용된다. JSON 형식의 데이터를 JavaScript 객체로 변환하여 사용할 수 있게 해준다.
      • localStorage.setItem('access_token', result.accessToken);
        웹 브라우저의 로컬 스토리지에 'access_token'이라는 키와 그에 해당하는 값으로서 새로운 액세스 토큰을 저장한다.

      • httpRequest(method, url, body, success, fail);
        새로 발급받은 액세스 토큰으로 이전에 실패한 요청을 다시 시도한다.

      • return fail();
        응답이 성공하지 않았거나, 인증 토큰이 만료되지 않았을 때 fail() 함수를 호출하여 실패 처리를 수행한다.




해당 글은 다음 도서의 내용을 정리하고 참고한 글임을 밝힙니다.
신선영, ⌜스프링 부트 3 벡엔드 개발자 되기 - 자바 편⌟, 골든래빗(주), 2023, 384쪽
profile
IT, 개발 관련 정보들을 기록하는 장소입니다.
post-custom-banner

0개의 댓글