반응형
웹 애플리케이션에서 JWT(JSON Web Token)를 활용하여 인증과 권한 부여를 처리하는 것은 매우 일반적입니다. 이 글에서는 NuxtJS와 Spring을 사용하여 JWT를 관리하는 과정과 관련된 보안 고려사항을 설명하겠습니다.
1. JWT 토큰 저장 위치 및 보안 고려사항
JWT 토큰은 크게 두 가지, 즉 accessToken과 refreshToken으로 나뉩니다. 각각의 저장 위치와 보안 측면을 고려할 때, 다음과 같은 옵션이 있습니다
- LocalStorage
- 장점: 데이터 접근이 용이하여 편리합니다.
- 단점: JavaScript에서 접근할 수 있어 XSS(Cross-Site Scripting) 공격에 취약합니다.
- 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 토큰 관리
로그인 및 토큰 발급 처리
- 로그인 과정
- 사용자가 아이디와 비밀번호를 입력하여 로그인 시도하면, 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 헤더에 포함시킵니다.
- 페이지 새로고침 및 페이지 이동
- 페이지 새로고침: 로컬 변수의 accessToken이 사라지므로, 쿠키에 저장된 refreshToken을 사용하여 새로운 accessToken을 요청합니다.
- 페이지 이동: 로컬 변수의 accessToken이 만료되었는지 확인 후, 만료된 경우 refreshToken으로 accessToken을 재발급 받습니다.
- 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) 측의 보안 조치
마지막으로 서버 측에서는 다음과 같은 보안 조치를 고려해야 합니다
- HTML/JavaScript 이스케이프 처리
- 사용자 입력이나 외부 데이터를 출력할 때 HTML 및 JavaScript 이스케이프 처리를 수행하여 XSS 공격을 방어합니다.
- 외부 URL 필터링
- 외부에서 오는 URL을 필터링하여 불필요한 외부 접근을 차단합니다.
- CORS 설정
- Cross-Origin Resource Sharing(CORS) 정책을 설정하여 안전한 도메인에서만 요청을 허용합니다.
반응형
'VUE' 카테고리의 다른 글
vue.js v-slot 사용 방법 (0) | 2023.04.23 |
---|