일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- Java
- LV01
- LV1
- docker
- JPA
- 연습문제
- Lv.0
- CoffiesVol.02
- 데이터 베이스
- 포트폴리오
- 코테
- LV02
- 이것이 자바다
- LV.02
- mysql
- Redis
- LV03
- Join
- LV0
- Spring Frame Work
- jpa blog
- 디자인 패턴
- 배열
- 네트워크
- Til
- 프로그래머스
- 일정관리프로젝트
- 포트 폴리오
- 알고리즘
- SQL
- Today
- Total
코드 저장소.
Jwt를 활용한 로그인을 구현하기 본문
목차
1.Jwt를 프로젝트에 적용한 이유
2.프로젝트에서 사용된 인증 로직
3.구현 결과물
1.Jwt를 프로젝트에 적용한 이유
Jwt로그인을 프로젝트에 적용을 하게된 이유는 크게 다음과 같다.
- Jwt방식을 사용하면 서버는 비밀키만 알고 있으면 되기 때문에 세션 방식과 같이 별도의 인증 저장소가 필요하지 않으므로 서버의 부담이 감소
- 무상태 인증, 간편한 클라이언트-서버 분리, 분산 시스템으로 확장 가능성 등의 장점
위와 같은 이유로 Jwt를 적용하고자 한다.
2.프로젝트에서 사용된 인증 로직
보통 jwt로그인에서 사용되는 재발급의 로직은 로그인시 발급되는 accessToken의 유효기간이 만료가 되면 서버에서 만료가 된지 확인을 한 뒤에 refreshToken을 활용해서 accessToken을 재발급하는 방법이다.
하지만 이 방법의 경우에는 accessToken의 만료기간이 만료가 되면 api를 이용하는데 있어서 인증이 안되거나 다시 로그인 화면으로 이동을 하는 등의 문제가 생기게 된다. 그래서 생각을 해낸 것이 다시 인증을 할 필요 없이 조용히 자동으로 accessToken과 refreshToken을 재발급을 하는 기능에 대해 생각을 하게 되었다.
우선 프로젝트에서 사용되는 서버단의 인증로직의 순서는 사진과 같다.
로그인 로직
우선은 서버단의 설명을 하자면 이러하다.
- 클라이언트에서 아이디와 패스워드를 입력하고 로그인 요청을 한다.
- 모든 요청은 컨트롤러로 가기 전에 JwtAuthenticationFilter를 통해서 토큰의 값이 있는지 확인과 유효성검사를 통해서 인증을 확인하고 토큰이 없는 경우에는 컨트롤러에서 서비스로 이동한다.
- 서비스에서 이동한 뒤에는 회원이 1분안에 로그인한지를 확인 후 결과값에 의해서 방문자를 저장한다.(중복회원 방지)
- 그 후 해당 권한을 가져오는 메서드 getAuthorities()를 통해서 회원의 권한을 가져온다.
- generateToken()으로 토큰을 생성한다.
- 만약에 redis에 refreshToken이 있는 경우 기존의 토큰값을 삭제하고 다시 만들어진 refreshToken을 유효기간에 저장을 한다.
- 결과물로 accessToken과 refreshToken을 담은 tokenDto이고 위의 컨트롤러에서 refreshToken은 쿠키에 담아서 보내는데 secure속성을 넣고 발급한다.
로그아웃 로직
로그아웃의 로직은 다음과 같다.
- 헤더에 로컬스토리지에 있는 accessToken을 담아서 로그아웃 요청을 보낸다.
- 요청을 받으면 토큰값을 확인을 하고 레디스에 저장 되어있는 refreshToken와 요청된 refreshToken이 같은지 확인 후 조건에 맞으면 레디스에 저장된 토큰을 삭제한다.
- 그 후 요청으로 보낸 accessToken을 redis에 value로 저장을 하고 key값으로 logout으로 정해서 저장을 한다. → 똑같은 accessToken으로 접근하는 것(중복 로그인)을 방지하기 위함.
- 컨트롤러에서 저장되어 있는 쿠키를 삭제한다.
재발급 로직
재발급을 하는 로직은 이러하다.
- 로컬스토리지에 저장되어 있는 accessToken의 만료기간에서 현재시간을 뺀 시간에서 만료되기 2분전에 쿠키에 있는 refreshToken과 함께 재발급을 요청을 한다.
- 서버에서 redis에 저장된 refreshToken이 있는지를 확인 후 토큰이 있는 경우에는 저장된 토큰 값을 삭제한다.
- 다시 로그인과 마찬가지로 accessToken토큰을 보낸다.
3.구현 결과물
로그인 api
로그인을 하게 되면 accessToken 과 refreshToken을 응답값으로 나오게 되고 쿠키에 refreshToken을 넣었습니다.
위의 로직과 같이 redis에도 refreshToken값이 들어갑니다
재발급
서버단의 설명은 이렇고 클라이언트(프론트)는 이렇게 진행이 됩니다.
위의 그림을 설명을 하면 다음과 같습니다.
1. 아이디와 비밀번호를 입력을 하고 ajax로 로그인을 통신을 합니다.
2. 통신의 응답값으로 accessToken,refreshToken (at, rt)를 받습니다.
3. 우선 accessToken을 파싱을 해서 회원의 아이디, 권한,유효기간을 로컬스토리지에 저장을 하고 refreshToken은 쿠키에 저장을 합니다.
4.토큰의 유효기간과 현재기간을 차이를 setInterval()을 1분마다 계산을 합니다.
5.accesToken이 만료가 되기 1분 전에 쿠키에 저장이 된 refreshToken을 가져와서 재발급을 합니다.
6.재발급으로 다시 서버에서 accessToken,refreshToken을 발급을 한다.
7.이후 계속 반복을 합니다.
코드를 설명을 하면 다음과 같습니다.
function loginproc(){
let userid = $('#user_id').val().trim();
let userpw = $('#user_pw').val().trim();
let logindata = {
username : userid,
password : userpw
};
$.ajax({
url:'/api/login/signup',
type:'POST',
data:JSON.stringify(logindata),
dataTye:"json",
xhrFields: {
withCredentials: true
},
contentType:'application/json; charset=utf-8'
}).done(function(data){
console.log(data);
//토큰을 파싱하고 로컬스토리지에 토큰값 저장하기.
let tokenResult = tokenParse(data.accessToken);
setItemWithExpireTime('Authorization',data.accessToken,tokenResult.exp);
//토큰값 가져오기.
let token = getItems();
console.log(token);
//권한에 따른 페이지 이동
loginSuccessProc(token.value);
}).fail(function (error) {
console.log(error);
alert("존재하지 않는 회원입니다.");
});
}
로그인 버튼을 누르고 로그인 로직을 한 뒤에는 응답값으로 accessToken과 refreshToken을 발급을 받는데 refreshToken은 쿠키로 저장이 되고 accessToken은 토큰을 파싱을 해야 합니다. 파싱을 하는 코드는 다음과 같습니다.
function tokenParse(token){
if(token){
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(window
.atob(base64)
.split('')
.map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);}).join(''));
let result = JSON.parse(jsonPayload);
//at토큰 유효기간 검증
validTokenExpiredTime(result.exp);
return result;
}
}
서버에서 보낸 토큰에 페이로드에 있는 값을 Base64 URL 디코딩을 통해서 추출을 하고 그 내용을 JSON객체로 변환후 반환하고 accessToken의 유효기간을 검증을 합니다.
다음은 로그인에 성공을 했을 경우 권한에 따른 페이지 이동함수 loginSuccessProc()함수입니다.
function loginSuccessProc(tokenId){
//토큰값이 있다면 파싱후 권한에 맞는 페이지로 이동하기.
let result = tokenParse(tokenId);
if(result.role === 'ROLE_ADMIN'){
//관리자로 로그인을 할 경우에는 어드민 페이지 활성화, 로그인 비활성화
//관리자 페이지 활성화
location.href='/page/member/adminlist';
}else if(result.role === 'ROLE_USER'){
location.href='/page/main/mainpage';
}else{
console.log('익명');
}
}
토큰을 파싱해서 토큰에 있는 아이디와 권한을 가져와서 해당 권한에 맞게끔 페이지를 이동을 하게 했습니다.
그리고 유효기간 검증 함수에 유효기간 값을 보내서 실행을 합니다. 토큰의 유효기간과 토큰 재발급을 다루는 함수는 다음과 같습니다.
function validTokenExpiredTime(exp){
//AccessToken의 유효기간
let expirationDate = new Date(exp*1000);
//현재 시간
let currentDate = new Date();
let differentTime = expirationDate - currentDate;
let TokenValue = getItems();
if(TokenValue!=null){
console.log(differentTime);
console.log(TokenValue.value);
if(differentTime == 120000){ //토큰의 유효기간이 만료되기 1분전에 재발급하기.
console.log("재발급을 시작");
//ajax를 사용해서 rt와 at를 헤더에 넣어서 토큰을 재발급을 하고 로그인 절차와 똑같이 한다.
//이방법으로 하는 경우에는 작동은 되지만 다른 방법을 생각해 봐야 할 것 같다.
console.log(TokenValue);
console.log("액세스토큰::"+TokenValue.value);
var refreshToken = getRefreshToken();
console.log("리프레시토큰::"+refreshToken);
$.ajax({
url:'/api/login/reissue',
type:'post',
xhrFields: {
withCredentials: true
},
headers:{
'Authorization':'Bearer '+TokenValue.value,
'Cookie': 'refresh-token='+refreshToken
},
dataTye:'json'
}).always(function(data){
console.log('재발급성공!!');
//작동이 되면 at를 로컬 스토리지에 저장을 하고 rt는 쿠키에 저장을 한다.
console.log(data);
//기존에 있던 로컬스토리지에 있는 값을 제거
localStorage.clear();
let accessToken = tokenParse(data.accessToken);
//받은 데이터에 at를 로컬 스토리지에 저장하기.
setItemWithExpireTime('Authorization',data.accessToken,accessToken.exp);
//rt는 쿠키에 저장
console.log("재발급 끗. 토큰저장!");
}).fail(function(err){
console.log(err);
});
}else{
console.log("토큰 유효");
}
}
}
function getRefreshToken() {
// document.cookie로부터 쿠키 값을 읽어옵니다.
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// "refresh-token="으로 시작하는 쿠키를 찾아 해당 값을 반환합니다.
if (cookie.startsWith('refresh-token=')) {
return cookie.substring('refresh-token='.length);
}
}
return null;
}
function deleteCookie(name) {
document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
위의 함수의 기능은 다음과 같습니다.
- accessToken에서 받은 내용에서 토큰의 유효기간과 현재시간을 계산을 해서 setInterval()을 사용해서 1분마다 주기적으로 유효기간을 검증을 합니다.
- if문에서 유효기간이 1분이 남았을때 쿠키에 저장이 되어있는 토큰을 getRefreshToken()메서드를 사용해서 토큰값을 읽습니다.
- ajax를 사용해서 헤더에 accessToken과 refreshToken을 넣어서 토큰 재발급을 합니다.
- 재발급에 성공을 하면 accessToken과 refreshToken을 발급을 받고 accessToken은 토큰값을 파싱하고 로컬스토리지에 넣고 refreshToken은 쿠키에 집어넣습니다.
마지막으로 로그인에 성공을 했을시 화면을 동적으로 바꾸는 공통 로직입니다.
/**
* 로그인후 권한에 따라 화면 변환
**/
window.onload= function (){
//로컬스토리지에 토큰값이 있는 경우
let tokenId = localStorage.getItem('Authorization');
console.log(tokenId);
let result = tokenParse(tokenId);
if(tokenId){
let username = result.userId;
let role = result.role;
let expire = result.exp;
let expireTime = new Date(expire *1000);
console.log(result);
console.log(document.cookie);
console.log("아이디:"+username);
console.log("권한:"+role);
console.log("유효기간:"+expire);
console.log(expireTime);
//알림기능
notification(username);
$('#userDetail').attr('href','/page/mypage/detail/'+username);//마이페이지
$('#userComment').attr('href','/page/mypage/my-comment/'+username);//내가 작성한 댓글
$('#userBoard').attr('href','/page/mypage/my-article/'+username);//내가 작성한 글
$('#scrapList').attr('href','/page/mypage/list/'+username);//스크랩을 한 글 목록
//1분마다 주기적으로 유효기간을 검증한다.
setInterval(function(){validTokenExpiredTime(expire)},60000);
//권한에 따라 사이드바를 변경
if(role === 'ROLE_ADMIN'){
console.log('관리자');
$('#admin-side-bar')3.css("display","block");
$('#admin-page').css("display","block");
$('#user-side-bar').css("display","none");
$('.user-side-bar-page').css("display","none");
$(".userId").text(username + "님 환영합니다.");//로그인한 회원의 아이디 표시
$('.admin-token').css("display","block");//관리자 페이지 오픈
$('#loginPage').css("display","none");//로그인 숨기기
$('#main').css("display","block");//메인 페이지 오픈
$('.logout').css("display","block");//로그아웃 오픈
}else if(role === 'ROLE_USER'){
console.log('회원');
$('#admin-side-bar').css("display","none");
$('#admin-page').css("display","none");
$('#user-side-bar').css("display","block");
$('#user-side-bar-page').css("display","block");
$(".userId").text(username + "님 환영합니다.");//로그인한 회원의 아이디 표시
$('.admin-token').css("display","none");//관리자 페이지 숨기기
$('#loginPage').css("display","none");//로그인 숨기기
$('#main').css("display","block");//메인 페이지 오픈
$('.logout').css("display","block");//로그아웃 오픈
$('#mylist').css("display","block")//마이페이지 오픈
$('#mypage').attr('href','/page/mypage/list/'+username);//마이페이지
}
}else{//토큰이 없는 경우
console.log('익명');
$('#admin-side-bar').css("display","none");//관리자 페이지 사이드바 숨김.
$('#user-side-bar').css("display","block");//회원 페이지 사이드바 오픈
$('#admin-page').css("display","none");
$(".userId").text('로그인을 해주세요.');
$('#loginPage').css("display","block");//로그인 오픈
$('.logout').css("display","none");//로그아웃 숨기기
}
};
/**
* 로컬스토리지에 저장하기.(기간 포함)
**/
function setItemWithExpireTime(keyName,keyValue,ttl){
//로컬스토리지에 저장할 객체(토큰값,유효기간)
const obj = {
value : keyValue,
expire : ttl
}
// 객체를 JSON 문자열로 변환
const objString = JSON.stringify(obj);
// 로컬스토리지에 토큰값을 저장
window.localStorage.setItem(keyName, objString);
}
/**
* 로컬스토리지에 저장된 값을 가져오기.
**/
function getItems(){
//value값
let value =localStorage.getItem('Authorization');
let result = JSON.parse(value);
return result;
}
이렇게 구현을 하면 사용자는 매번 클라이언트에게 accessToken이 만료가 되었는지를 확인을 할 필요가 없어지게 되고 토큰이 만료가 되기 전에 토큰을 재발급을 할 수 있기 때문에 사용자 입장에서는 불편함이 줄어들 수 있습니다.
'포폴 > JPABlog' 카테고리의 다른 글
게시글 조회수에서 발생한 동시성 제어 (0) | 2024.05.31 |
---|---|
JPQL에서 QueryDSL을 활용한 동적쿼리 적용 (0) | 2024.05.31 |
Redis Cache로 조회 성능 향상시키기. (0) | 2024.05.31 |
MyBatis에서 JPA로 변경 (0) | 2023.09.17 |
JPA 블로그 게시판 (0) | 2023.03.04 |