OAuth 구현의 마지막 단계인 OAuth 뷰를 구성한다.
로그인 뷰를 oauthLogin으로 변경한다.
// controller - UserViewController.java
@Controller
public class UserViewController {
@GetMapping("/login")
public String login() {
return "oauthLogin";
}
@GetMapping("/signup")
public String signup() {
return "signup";
}
}
로그인 화면에서 사용할 이미지를 다음 구글 로그인 브랜드 사이트에서 다운로드 받는다.
https://developers.google.com/identity/branding-guidelines?hl=ko
다운 받은 파일을 /resources/static/img 디렉토리에 붙여 넣는다.
위에서 다운받은 이미지를 활용해 로그인 화면에 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에서 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>
생성, 수정, 삭제 요청들이 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() 함수를 호출하여 실패 처리를 수행한다.