API 서버와 JWT - 2

유승욱·2024년 2월 22일
0

브라우저에서 JWT 확인

API 서버를 이용하는 구조에서는 브라우저에서 HTTP로 JWT 토큰을 전송하고 필요한 자원에 접근하는 방식을 이용한다. 예제에서는 Access Token과 Refresh Token 등으이 활용을 우선적으로 같은 서버 환경에서 먼저 체크하고 이후에 별도의 서버를 구축해서 확인하도록 한다.

JWT를 이용하는 시나리오

현재 static 폴더의 sample.html에서는 'http:localhost:8080/api/sample/doA'를 호출하고 있는 상황이다.

현재 예제에서는 '/api'로 시작하는 모든 경로는 TokenCheckFilter를 거치기 때문에 sample.html은 Access Token이 없는 상태로 호출되므로 에러가 발생하게 된다.
브라우저에서 JWT를 이용하는 시나리오를 정리하면 다음과 같다.

  1. '/generateToken'을 호출해서 서버에서 발행한 Access Token과 Refresh Token을 받는 단계이다. 브라우저는 받은 토큰들을 저장해 두고 필요할 때마다 토큰들을 찾아서 사용하도록 구성해야한다.
  2. 브라우저에서 '/api/sample/doA'를 호출할 때 가지고 있는 Access Token을 같이 전달했을 때 정상적인 결과가 나오는지를 확인한다.
  3. Access Token의 유효 기간이 만료되는 상황에 대한 처리이다. Access Token의 유효 기간이 만료되면 서버에서는 에러 메시지를 전송하게 되는데, 이 메시지를 판단해서 브라우저는 Refresh Token으로 다시 새로은 Access Token을 받고 원래 의도했던 작업을 수행해야한다.
  4. Refresh Token 마저도 만료된 상황에 대한 처리이다. Refresh Token이 만료되면 새로운 Access Token을 발행할 수 없기 때문에 사용자에게 1단계부터 다시 시작해야 함을 알려주어야 한다.

1단계 - 토큰 생성과 저장

apiLogin.html

생성된 토큰들은 위의 코드를 통해서 보관된다.

2,3,4단계 - Access Token을 이용한 접근, Refresh 처리, 만료된 Refresh Token
토큰들을 생성하고 보관했다면 다른 html에서 이를 이용해서 서버를 호출해보자.

sendJWT.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<div class="result">

</div>

<button class="btn1">CALL SERVER</button>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>

    const callServer = async() => {
        console.log("call server 1...")

        const accessToken = localStorage.getItem("accessToken")

        if(!accessToken) {
            throw 'Cannot Find Access Token'
        }

        const authHeader = {"Authorization": `Bearer ${accessToken}`}
        try {
            const res = await axios.get("http://localhost:8080/api/sample/doA",
                {headers: authHeader})
            return res.data
        }catch(err) {

            if(err.response.data.msg === 'Expired Token'){
                console.log("Refresh Your Token")

                try{
                    await callRefresh()
                    console.log("new tokens....saved..")
                    return callServer()

                }catch(refreshErr){
                    throw refreshErr.response.data.msg
                }
            }//end if
        }
    }

    const callRefresh = async () => {

        const accessToken = localStorage.getItem("accessToken")
        const refreshToken = localStorage.getItem("refreshToken")

        const tokens = {accessToken, refreshToken}
        const res = await axios.post("http://localhost:8080/refreshToken", tokens)
        localStorage.setItem("accessToken", res.data.accessToken)
        localStorage.setItem("refreshToken", res.data.refreshToken)
    }


    const resultDiv = document.querySelector(".result")

    document.querySelector(".btn1").addEventListener("click", () => {

        callServer().then(result => {
            console.log(result)
        }).catch(error => {
            alert(error)
        })
    },false)

</script>

</body>
</html>

Ajax와 CORS 설정

API 서버에서는 JSON 데이터만 주고받는 방식이기 때문에 실제로 화면이 존재하지 않는다. 실제 회면은 별도의 서버를 이용해서 처리하거나 리액트, Vue.js 등을 이용하는 SPA(Single Page Application) 방식으로 구현해서 물리적으로 분리되어 있는 서버나 프로그램에서 Ajax로 호출하게 된다.
이처럼 다른 서버에서 Ajax를 호출하면 '동일 출처 정책'을 위반하게 되면서 Ajax 호출이 정상적으로 이루어지지 않는다. '동일 출처 정책'은 웹 브라우저 보안을 위해 프로토콜, 호스트, 포트가 같은 서버로만 ajax 요청을 주고 받을 수 있도록 한 정책으로 Ajax를 이용해서 다른 서버의 자원들을 마음대로 사용하는 것을 막기 위한 보안 조치이다.
Ajax 호출이 '동일 출처 정책'으로 인해서 제한 받기 때문에 이를 해결하려면 'CORS(Cross Origin Resource Sharing)' 처리가 필요하다. CORS 처리를 하게 되면 Ajax 호출 서버와 API 서버가 다른 경우에도 접근과 처리를 허용할 수 있다.

실제로 어떤 문제가 있는지 확인하기 위해서 현재의 프로젝트가 실행되는 환경(API 서버)에서 다른 포트로 별도의 서버(웹 서버)를 구성하고 어떤 문제가 생기는지 확인해 보도록 하자.

Nginx 웹 서버의 설치

별도의 서버를 구성하려는 방법은 여러 가지가 있을 수 있지만 최근에 많이 사용되는 Nginx 서버를 세팅해서 html 파일들을 서비스하고 Ajax를 이용해서 JWT를 사용해보도록 하자.

압축을 풀어둔 경로에는 nginx-xxx 폴더가 생성되고 내부에는 다음과 같은 폴더와 파일이 생성된 것을 확인할 수 있다.

Nginx의 시작과 종료

폴더들 중에서 html 폴더를 살펴보면 내부에 index.html과 50x.html이 존재하는 것을 볼 수 있다.

Nginx가 시작되면 80 포트를 기본으로 동작하는데 이때 가장 먼저 사용할 수 있는 파일이 index.html이다.
Nginx의 시작과 종료는 해당 폴더에서 'nginx.exe' 파일을 이용해서 처리한다.
명령 프롬프트를 이용해서 해당 폴더로 이동하고 'start nginx'를 이용해서 서버를 실행한다.

서버 실행 후에는 브라우저로 'http://localhost'를 호출헤서 index.html이 서비스 되는 것을 확인한다.

서버의 종료는 'nginx -s quit'으로 실행한다.

html 폴더 편집

실제 html 파일들의 내용은 html 폴더에 위치하므로 예제에서 작성했던 파일들을 html 폴더에 넣고 편집할 필요가 있다.

Nginx 서버를 실행하고 http://localhost/apiLogin.html을 실행해서 버튼을 누르면 문제가 발생하는 것을 볼 수 있다.

발샣아는 문제는 Ajax 호출에 사용하는 CORS 문제와 GET 방식이 아닌 POST 방식을 이용할 때 발생하는 Preflight 문제이다.

CORS 문제 해결

Ajax의 '동일 출처 정책'을 해결하는 방법에는 여러 가지 방식이 있다. 예를 들어 브라우저에서 직접 서버를 호출하는 대신에 현재 서버 내 다른 프로그램을 이용해서 API 서버를 호출하는 프록시(proxy - 대리자) 패턴을 이용하거나 JSONP와 같이 JSON이 아니라 순수한 JS 파일을 요청한느 방식 등이 있다.
가장 권장되는 해결책은 당연히 서버에서 CORS 관련 설정으로 해결하는 것이다. 서버에서 CORS 설정은 주로 필터를 이용해서 브라우저의 응답 메시지에 해당 호출이 문제 없었다는 헤더 정보들을 같이 전송하는 방식이다. 스프링 부트는 이러한 상황을 처리하기 위해서 웹 관련 설정을 약간 조정하는 방식을 이용하거나 컨트롤러는 @CrossOrigin 어노테이션을 이용해서 처리할 수 있다.

CustomSecurityConfig 수정

정상 작동

0개의 댓글