개발

JWT Access Token, Refresh Token 일련의 과정

고양이양말 2022. 5. 18. 18:41

1. 사용자가 username으로 access token, refresh token 발급 요청(로그인)

2. 서버가 access token, refresh token 발급해 주고 username, access token, refresh token은 DB에 저장(사용자는 refresh token을 쿠키나 어딘가에 저장)

3. 사용자가 access token으로 api들 호출(header에 담아서)

4. 서버가 access token 만료 여부 확인하고 api 수행

4-1. access token 만료시 사용자에 Unauthorized error return

4-2. 사용자가 access token, refresh token을 서버에 보내 재발급 요청

4-3. 서버가 전달받은 token들을 검증하고, refresh token과 token에 담긴 username으로 DB에 저장된 access token, refresh token을 비교하여 동일한 경우 access token과 refresh token 재발급(신규 access token, refresh token을 DB에 저장)

4-4. 사용자가 신규 access token으로 api 재호출

(보통 access token은 db에 저장하지 않는데, 보안을 강화할 수 있고 어차피 refresh token DB 접근시에 같이 사용할 것이므로 부하도 없을 듯)

 

* 사용자가 refresh token을 호출하는 방안

1. 사용자가 api 호출 -> api에서 만료 검사하여 만료시 사용자에게 만료 return -> 사용자가 refresh token으로 재발급 요청 -> 서버에서 재발급하여 access token과 refresh token return -> 사용자가 신규 access token으로 api 재호출

-> 한 화면에서 비동기로 여러 api 호출시 문제 발생!

2. access token이 만료 직전 시간마다 사용자가 refresh token으로 재발급 요청 -> 재발급 -> 이후 사용자가 신규 access token으로 api 사용

3. 페이지 로드시마다 refresh token으로 재발급 요청 -> 재발급 -> 이후 사용자가 신규 access token으로 api 사용

4. (front가 전혀 관여하지 않는 방법)로그인 시 사용자와 서버간 쿠키 공유. 사용자가 api 호출 -> 서버에서 쿠키 내의 access token과 refresh token으로 검사

 

token이 필요한 이유가 front server와 back server, module server등이 분리된 경우 세션 공유 대신 로그인 유지를 위해 채택하는 방법이기에, 4번은 다른 도메인간 쿠키 공유가 가능해야 한다. 하지만 다른 도메인간 쿠키 공유는 권장하지 않는 방법("SameSite=None" 필요).

구글은 모바일 크롬 브라우저에서 서드파티 쿠키 사용을 차단시킬 예정이라고 한다.

(2022년 5월 현재 발표로는 2023년 말 예정)

(서드파티 쿠키 : 사용자 도메인이 아닌 제3자의 도메인이 발행한 쿠키)

 

XSS 공격 : <input>이나 url에 javascript를 이용해 스크립트 실행 (front에서 방어)

CSRF 공격 : 다른 사이트에서 api 호출 (server에서 방어(도메인 검사 등))

1. localstorage에 session id, access token, refresh token 저장시 XSS 취약.

2. 쿠키에 session id, access token, refresh token 저장시 XSS, CSRF 취약.

3. 쿠키에는 refresh token만 저장하고 access token은 로컬 변수로 받아서 이용시 CSRF 방어 가능(access token을 스크립트에 삽입할 수 없을거기 때문).

+쿠키에 secure를 추가하고 https 방식을 이용하여 httponly로 쿠키 노출 방어

onLoginSuccess(res) {
	react : axios.defaults.headers.common['Authorization'] = `Bearer ${res.accessToken}`;
    jquery : $.ajaxSetup({headers:{'Authorization':res.accessToken}});
    또는 $.ajaxSetup({beforeSend:function(xhr){xhr.setRequestHeader('Authorization', res.accessToken)}});
    
    refreshToken은 저장소에 저장(local storage, cookie 등.. access token이 보호됐기 때문에 어디든)
}
login 성공시 onLoginSuccess(res)
refreshToken으로 재발급 요청 성공시 onLoginSuccess(res)

onLoginSuccess를 통해야지만 정상동작 할 수 있다.

 

 

 

 

 

 

 

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
...
	public Boolean isTokenExpired(String token) {
		try {
            final Claims claims = Jwts.parser().setSigningKey(secret.getBytes(Charset.forName("UTF-8"))).parseClaimsJws(token).getBody();
        	final Date expiration = Claims::getExpiration.apply(claims);
			return expiration.before(new Date());
		} catch(ExpiredJwtException e) {
			return true;
		} catch(Exception e) {
			return false;
		}
	}

 

참고 https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0

 

🍪 프론트에서 안전하게 로그인 처리하기 (ft. React)

localStorage냐 쿠키냐 그것이 문제로다

velog.io

 

 

순수 html의 경우 메뉴 이동시 브라우저가 새로고침 된다. token을 refresh 하는 기간을 잡기 위해선

페이지 로드시 만료일 검사하고, 만료일이 아니면 만료일까지 setTimeout을 걸어놓자.

const JWT_EXPIRED_TIME = 30*60*1000;
// api call시 refresh 체크를 하면 여러개의 api를 호출할 때 꼬일 수 있다
// 화면 그릴때와 settimeout을 이용해보자
// access-token 만료 1분 전에 로그인 연장
const refreshCheck = () => {
  const tokenTime = unescape(get_cookie("tokenTime"));  // 로그인 시 로그인 시간을 tokenTime에 넣어놓는다
  console.log(tokenTime);
  if (tokenTime !== null) {
    if (new Date()-new Date(tokenTime) >= JWT_EXPIRED_TIME - (1*60*1000)) {
      console.log('refresh token 발급 필요');
      refreshTokenCall();  // token refresh 함수
    }

    setTimeout(() => {
      refreshCheck();
    },JWT_EXPIRED_TIME - (1*60*1000));
  }
}

document.addEventListener("DOMContentLoaded", function(){
    refreshCheck();
});