본문 바로가기

VUE

NuxtJS(ReactJS) + Spring + JWT 토큰 발급 과정 및 보안 고려사항

반응형

웹 애플리케이션에서 JWT(JSON Web Token)를 활용하여 인증과 권한 부여를 처리하는 것은 매우 일반적입니다. 이 글에서는 NuxtJS와 Spring을 사용하여 JWT를 관리하는 과정과 관련된 보안 고려사항을 설명하겠습니다.


1. JWT 토큰 저장 위치 및 보안 고려사항

JWT 토큰은 크게 두 가지, 즉 accessToken과 refreshToken으로 나뉩니다. 각각의 저장 위치와 보안 측면을 고려할 때, 다음과 같은 옵션이 있습니다

  1. LocalStorage
    • 장점: 데이터 접근이 용이하여 편리합니다.
    • 단점: JavaScript에서 접근할 수 있어 XSS(Cross-Site Scripting) 공격에 취약합니다.
  2. Cookie
    • 장점: Secure 및 HttpOnly 속성을 설정하여 보안을 강화할 수 있습니다.
    • 단점: 쿠키는 CSRF(Cross-Site Request Forgery) 공격에 취약할 수 있습니다.
    보안 설정
    • Secure: HTTPS를 사용하는 경우에만 쿠키를 전송하도록 설정합니다.
    • HttpOnly: JavaScript에서 쿠키에 접근하지 못하도록 합니다.

결론

  • RefreshToken: 쿠키에 저장하며 Secure 및 HttpOnly 속성을 설정합니다.
  • AccessToken: NuxtJS 내 로컬 변수나 store에 저장하고, 페이지 새로고침 및 페이지 이동 시에만 재발급을 받도록 합니다. API 요청 시에는 Authorization 헤더에 accessToken을 포함시킵니다.
  • 위에서 설명한 접근 방법은 JWT 기반 인증 시스템의 보안을 강화할 순 있지만, 완벽한 보안을 보장할 수는 없습니다. 모든 보안 시스템은 잠재적인 취약점과 공격에 대해 완벽하게 막을 수는 없기 때문에, 가능한 할 수 있는 보안 조치를 취하는 것이 중요하다고 생각합니다.

2. 프론트엔드(NuxtJS)에서 JWT 토큰 관리

로그인 및 토큰 발급 처리

  1. 로그인 과정
    • 사용자가 아이디와 비밀번호를 입력하여 로그인 시도하면, Spring 서버는 JWT(accessToken 및 refreshToken)를 응답합니다.
    • 응답 시 refreshToken을 Secure 및 HttpOnly 속성으로 설정한 쿠키에 담아 클라이언트로 전송합니다.
      @PostMapping("/login")
      public ResponseEntity<?> login(HttpServletRequest request, HttpServletResponse response,
                                     @RequestBody @Valid MemberDto.Login login) {
      
          // 로그인 처리 및 토큰 생성
          TokenDto.Generate generate = authService.login(request, login);
      
          // refreshToken은 서버에서 쿠키 저장(HttpOnly 설정하기 위함)
          CookieUtils.addCookie(response, "refreshToken", generate.getRefreshToken(), JwtProvider.REFRESH_TOKEN_EXPIRE_TIME / 1000);
          
          // 쿠키 응답 설정
          Cookie cookie = new Cookie("refreshToken", generate.getRefreshToken());
          cookie.setPath("/");
          cookie.setSecure(true); // Secure true 설정
          cookie.setHttpOnly(true); // HttpOnly true 설정
          cookie.setMaxAge(만료시간 설정);
          response.addCookie(cookie);
      
          return ResponseEntity.ok(generate);
      }
    • 프론트엔드에서는 accessToken을 store에 저장하여, API 요청 시 Authorization 헤더에 포함시킵니다.
  2. 페이지 새로고침 및 페이지 이동
    • 페이지 새로고침: 로컬 변수의 accessToken이 사라지므로, 쿠키에 저장된 refreshToken을 사용하여 새로운 accessToken을 요청합니다.
    • 페이지 이동: 로컬 변수의 accessToken이 만료되었는지 확인 후, 만료된 경우 refreshToken으로 accessToken을 재발급 받습니다.
  3. API 요청 시 토큰 관리
    • AccessToken 만료 체크: Axios 인터셉터를 설정하여 API 요청 시 accessToken의 유효성을 확인합니다.
    • 토큰 재발급: 만약 accessToken이 만료된 경우, Spring 서버로 재발급 요청을 보내고, 새로운 토큰으로 실패한 요청을 재시도합니다.
    방법
    • $axios.onError 인터셉터를 설정하여 서버에서 JWT 만료 시 응답하는 EXPIRED_JWT 에러코드에 반응합니다.
    • 새로운 accessToken과 refreshToken을 저장하고, 실패한 요청을 다시 시도합니다.
      export default function ({ $axios, store, redirect }) {
      	let isTokenRefreshing = false;
      	let refreshSubscribers = [];
      
      	const onTokenRefreshed = accessToken => {
      		refreshSubscribers.map(callback => callback(accessToken));
      		refreshSubscribers = [];
      	};
      
      	const addRefreshSubscriber = callback => {
      		refreshSubscribers.push(callback);
      	};
      
      	const retryOriginalRequests = originalRequest => {
      		return new Promise(resolve => {
      			addRefreshSubscriber(accessToken => {
      				originalRequest.headers.Authorization = `Bearer ${accessToken}`;
      				resolve($axios(originalRequest));
      			});
      		});
      	};
      
      	$axios.onRequest(config => {
      		const BYPASS_LIST = ['/api/auth/login', '/api/auth/silentReissue'];
      		if (BYPASS_LIST.includes(config.url)) {
      			return;
      		}
              
      		const accessToken = store.state.auth.accessToken;
      		if (!accessToken) {
      			location.href = '/login';
      		}
      		config.headers.Authorization = `Bearer ${accessToken}`;
      	});
      
      	$axios.onResponse(response => {});
      
      	$axios.onError(async error => {
      		const {
      			config,
      			response: { status, data },
      		} = error;
      		const originalRequest = config;
      
      		// console.log(error.response);
      
      		if (status === 401) {
      			if (data.apierror && data.apierror.errorCode === 'EXPIRED_JWT') {
      				if (!isTokenRefreshing) {
      					// isTokenRefreshing이 false인 경우에만 token refresh 요청
      					isTokenRefreshing = true;
      					try {
      						const refreshData = await store.dispatch(
      							'auth/refreshtoken',
      						);
      
      						isTokenRefreshing = false;
      
      						const retryOriginalRequest =
      							retryOriginalRequests(originalRequest);
      
      						// 새로운 토큰으로 지연되었던 요청 진행
      						onTokenRefreshed(refreshData.accessToken);
      						return retryOriginalRequest;
      					} catch (error) {
      						console.error(error);
      						isTokenRefreshing = false;
      					}
      				}
      
      				// token이 재발급 되는 동안의 요청은 refreshSubscribers에 저장
      				return retryOriginalRequests(originalRequest);
      			} else {
      				// redirect('/login');
      				location.href = '/login';
      			}
      		}
      		return Promise.reject(error);
      	});
      }

3. 서버(SPRING) 측의 보안 조치

마지막으로 서버 측에서는 다음과 같은 보안 조치를 고려해야 합니다

  1. HTML/JavaScript 이스케이프 처리
    • 사용자 입력이나 외부 데이터를 출력할 때 HTML 및 JavaScript 이스케이프 처리를 수행하여 XSS 공격을 방어합니다.
  2. 외부 URL 필터링
    • 외부에서 오는 URL을 필터링하여 불필요한 외부 접근을 차단합니다.
  3. CORS 설정
    • Cross-Origin Resource Sharing(CORS) 정책을 설정하여 안전한 도메인에서만 요청을 허용합니다.
반응형

'VUE' 카테고리의 다른 글

vue.js v-slot 사용 방법  (0) 2023.04.23