Vue.js 3 & NodeJS/Vue 3

Vue CLI 레스트 API 인증 토큰 재발급 (Refresh-Tokon) - Vue CLI REST API Authentication, Refresh-Tokon

carrotweb 2022. 1. 23. 17:32
728x90
반응형

이전 Node.js 레스트 API 서버에서 Refresh-Token으로 Access-Token이 재발급되게 처리하였습니다.

이번에는 Vue에서 레스트 API 서버로 요청을 보내기 전에 Access-Token이 만료되었는지 확인을 하여 Access-Token이 만료되었다면 레스트 API 서버로 "/refresh"를 요청하여 Access-Token이 재발급되도록 처리하겠습니다.

Node.js 레스트 API 서버를 먼저 실행시킵니다.

콘솔을 실행하고 Node.js 레스트 API 서버 프로젝트가 있는 C:\workspaces\nodeserver\testrestapi 폴더로 이동합니다. 그리고 npm run 명령어를 실행합니다.

npm run start

 

 

Login Store 모듈에 토큰 재발급 추가하기

1. C:\workspaces\nodeserver\testvue\src\store\modules 폴더에서 login.js 파일을 오픈합니다.

refreshToken를 저장하기 위해 state에 선언합니다.

state: {
	memberId: '',
	accessToken: '',
	refreshToken: '' --> 추가
}

 

2. state.refreshToken를 변경하기 위해 mutationssetRefreshToken() 메서드를추가합니다.

mutations: {
	// memberId를 설정합니다.
	setMmemberId(state, memberId) {
		state.memberId = memberId;
	},
	// Access-Token를 설정합니다.
	setAccessToken(state, accessToken) {
		state.accessToken = accessToken;
	},
	// Refresh-Token를 설정합니다.
	setRefreshToken(state, refreshToken) {
		state.refreshToken = refreshToken;
	},
	// 초기화시킵니다.
	reset(state) {
		state.memberId = '';
		state.accessToken = '';
		state.refreshToken = '';
	}
}

 

3. actions의 doLogin() 메서드에서 refreshToken를 저장하기 위해 store의 commit() 메서드로 setRefreshToken() 메서드를 호출합니다. 추가로 에러의 상태 코드를 추가합니다.

// 로그인합니다.
async doLogin({ commit }, memberInfo) {
	let result = false;
	let resultErr = null;
	try {
		let res = await axios.post("http://localhost:9000/members/login", memberInfo);
		if (res.data.success == true) {
			console.log("로그인되었습니다.");
			commit('setMmemberId', memberInfo.id);
			commit('setAccessToken', res.data.accessToken);
			commit('setRefreshToken', res.data.refreshToken); --> 추가
			axios.defaults.headers.common['Access-Token'] = res.data.accessToken;
			result = true;
		} else {
			console.log("로그인되지 않았습니다.");
			let err = new Error("Request failed with status code 401");
			err.status = 401; --> 추가
			err.response = {data:{"success":false, "errormessage":"로그인되지 않았습니다."}};
			resultErr = err;
		}
	} catch(err) {
		if (!err.response) {
			err.response = {data:{"success":false, "errormessage":err.message}};
		}
		resultErr = err;
	}
	return new Promise((resolve, reject) => {
		if (result) {
			resolve();
		} else {
			reject(resultErr);
		}
	});
}

 

4. actions에 doRefreshToken() 메서드를 추가합니다.

토큰 재발급 처리를 위해 Node.js 레스트 API 서버의 refresh API를 호출하고 토큰 재발급이 성공하면 store의 commit() 메서드로 setAccessToken() 메소드를 호출합니다. 그리고 실패하면 에러 메시지를 설정합니다.

리턴은 Promise로 하여 호출된 곳에서 성공(resolve)과 실패(reject)로 처리할 수 있게 합니다.

// Access-Token를 갱신합니다.
async doRefreshToken({commit, state}) {
	let result = false;
	let resultErr = null;
	if (state.accessToken != "") {
		let token = { id: state.memberId, accessToken : state.accessToken, refreshToken : state.refreshToken };
		try {
			let res = await axios.post("http://localhost:9000/members/refresh", token);
			if (res.data.success == true) {
				console.log("Access-Token이 갱신되었습니다.");
				commit('setAccessToken', res.data.accessToken);
				console.log(res.data.accessToken);
				axios.defaults.headers.common['Access-Token'] = res.data.accessToken;
				result = true;
			} else {
				console.log("Access-Token이 갱신되지 않았습니다.");
				let err = new Error("Request failed with status code 401");
				err.status = 401;
				err.response = {data:{"success":false, "errormessage":"Access-Token이 갱신되었습니다."}};
				resultErr = err;
			}
		} catch(err) {
			if (!err.response) {
				err.response = {data:{"success":false, "errormessage":err.message}};
			}
			resultErr = err;
		}
	} else {
		let err = new Error("Access-Token does not exist");
		err.status = 401;
		err.response = {data:{"success":false, "errormessage":"Access-Token이 없습니다."}};
		resultErr = err;
	}
	return new Promise((resolve, reject) => {
		if (result) {
			resolve();
		} else {
			reject(resultErr);
		}
	});
}

 

5. accessToken이 만료되었는지 확인하기 위하여 getters에 isAccessTokenExpire() 메서드를 추가합니다.

그리고 isLogin() 메서드를 accessToken를 refreshToken으로 수정합니다.

getters: {
	// 로그인 여부를 가져옵니다.
	isLogin(state) {
		return state.refreshToken == '' ? false : true; --> 수정
	},
	// accessToken이 만료되었는지 여부를 가져옵니다.
	isAccessTokenExpire(state) {
		let expire = false;
		// accessToken에서 .(도트)로 분리하여 payload를 가져옵니다.
		let base64Payload = state.accessToken.split('.')[1];
		// URL과 호환되지 않는 문자를 base64 표준 문자로 교체합니다.
		base64Payload = base64Payload.replace(/-/g, '+').replace(/_/g, '/');
		// atob() 메소드로 복호화합니다.
		base64Payload = atob(base64Payload);
		// JSON 객체로 변환합니다.
		var payloadObject = JSON.parse(base64Payload);
		// accessToken의 만료 시간을 확인합니다.
		var currentDate = new Date().getTime() / 1000;
		if (payloadObject.exp <= currentDate) {
			console.log("token expired");
			expire = true;
		} else {
			console.log("token valid");
		}
		return expire;
	}
}

 

 

 

게시물 등록 컴포넌트에 토큰 만료 확인 및 재발급 추가하기

1. C:\workspaces\nodeserver\testvue\src\views 폴더에서 BoardWrite.vue 파일을 오픈합니다.

accessToken이 만료되었는지 확인하여 만료되면 토큰 재발급을 진행하게 추가합니다.

동기화 처리를 위해 async와 await를 사용합니다. 동기 처리를 하지 않으면 바로 다음 라인으로 처리되기 때문입니다.

async boardSaveClick() {
	if (this.subject == "") {
		alert("제목을 입력하세요.");
		this.$refs.subjectInput.focus();
		return;
	} else if (this.content == "") {
		alert("내용을 입력하세요.");
		this.$refs.contentTextarea.focus();
		return;
	}

	var result = confirm("등록하시겠습니까?");
	if (result) {
		// accessToken이 만료되었는지 확인합니다.
		var isAccessTokenExpire = this.$store.getters['loginStore/isAccessTokenExpire'];
		if (isAccessTokenExpire) {
			// accessToken이 만료되면 토큰 재발급을 진행합니다.
			try {
				await this.$store.dispatch("loginStore/doRefreshToken");
				isAccessTokenExpire = false;
			} catch(err) {
				alert("accessToken이 만료되었습니다.\n다시 로그인 해주시기 바랍니다.\n" + err.response.data.errormessage);
			}
		}
				
		if (!isAccessTokenExpire) {
			let boardItem = { subject : this.subject, content : this.content };
			this.axios.post("http://localhost:9000/boards", boardItem).then((res)=>{
				console.log(res);
				if (res.data.success == true) {
					alert("등록되었습니다.");
					this.$router.push({name : 'BoardList'});
				} else {
					alert("등록되지 않았습니다.");
				}
			}).catch((err) => {
				console.log(err);
				alert("등록되지 않았습니다.");
			});
		}
	}
}

 

2. 콘솔을 실행하고 Vue 프로젝트가 있는 C:\workspaces\nodeserver\testvue 폴더로 이동합니다. 그리고 콘솔에서 npm run 명령어를 실행합니다.

npm run serve

3. 웹 브라우저에서 "http://localhost:8080/board"를 입력합니다. 그리고 쓰기 버튼을 클릭합니다. Login 페이지에서 아이이와 패스워드를 입력하면 게시물 작성 페이지로 이동됩니다. 테스트를 위해 제목에 "테스트 제목4", 내용에 "테스트 내용4"를 입력하고 10분 후 등록 버튼을 누릅니다.

 

4. 그러면 토큰이 만료되어 콘솔에 "token expired"이 출력됩니다. 그리고 토큰이 재발급되면서 "Access-Token이 갱신되었습니다."라고 출력됩니다.

token expired
Access-Token이 갱신되었습니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZW1iZXJJZCI6InRlc3RpZDEiLCJtZW1iZXJOYW1lIjoi7ZmN6ri464-ZIiwiaWF0IjoxNjQyNzc0MzM2LCJleHAiOjE2NDI3NzQ5MzZ9.9zxd1ZoNgmpmnU0szGVwdk7g8lhFZBgzqvGxYl2hYTk

 

Node.js 레스트 API 서버의 로그를 보면 Refresh-Token으로 Access-Token이 재발급된 것을 확인할 수 있습니다.

REST API Post Method - Member JWT Refresh
Refresh-Token Payload :
{ memberId: 'testid1', iat: 1642773726, exp: 1642860126 }
Refresh-Token Verify :
Access-Token Payload :
{
  memberId: 'testid1',
  memberName: '홍길동',
  iat: 1642773726,
  exp: 1642774326
}
Access-Token Verify :
Access-Token : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZW1iZXJJZCI6InRlc3RpZDEiLCJtZW1iZXJOYW1lIjoi7ZmN6ri464-ZIiwiaWF0IjoxNjQyNzc0MzM2LCJleHAiOjE2NDI3NzQ5MzZ9.9zxd1ZoNgmpmnU0szGVwdk7g8lhFZBgzqvGxYl2hYTk
Access-Token Error :
REST API Post Method - Create
728x90
반응형