Refresh Token 을 사용하는 이유

  • Access Token 만을 통한 인증 방식의 문제는 만일 제 3자에게 탈취당할 경우 보안에 취약하다는 점이다. Access Token은 발급된 이후, 서버에 저장되지 않고 토큰 자체로 검증을 하며 사용자 권한을 인증하기 때문에, Access Token이 탈취되면 토큰이 만료되기 전 까지, 토큰을 획득한 사람은 누구나 권한 접근이 가능해 지기 때문이다.
  • JWT는 발급한 후 삭제가 불가능하기 때문에, 접근에 관여하는 토큰에 유효시간을 부여하는 식으로 탈취 문제에 대해 대응을 하여야 한다.
  • 이처럼 토큰 유효기간을 짧게하면 토큰 남용을 방지하는 것이 해결책이 될 수 있지만, 유효기간이 짧은 Token의 경우 그만큼 사용자는 로그인을 자주 해서 새롭게 Token을 발급받아야 하므로 불편하다는 단점이 있다. 그렇다고 무턱대고 유효기간을 늘리자면, 토큰을 탈취당했을 때 보안에 더 취약해지게 된다.
  • 이때 “그러면 유효기간을 짧게 하면서  좋은 방법이 있지는 않을까?”라는 질문의 답이 바로 Refresh Token이다. 이름이 다르지만 형태 자체는 Refresh Token은 Access Token과 똑같은 JWT다.
  • 단지 Access Token은 접근에 관여하는 토큰이고, Refresh Token은 재발급에 관여하는 토큰 이므로 행하는 역할이 다르다고 보면 된다.

 

Access Token과 Refresh Token의 사용방법과 흐름

  • 처음에 로그인을 했을 때, 서버는 로그인을 성공시키면서 클라이언트에게 Access Token과 Refresh Token을 동시에 발급한다.
  • 서버는 데이터베이스에 Refresh Token을 저장하고, 클라이언트는 Access Token과 Refresh Token을 쿠키, 세션 혹은 웹스토리지에 저장하고 요청이 있을때마다 이 둘을 헤더에 담아서 보낸다.
  • 이 Refresh Token은 긴 유효기간을 가지면서, Access Token이 만료됐을 때 새로 재발급해주는 열쇠가 된다.
  • 따라서 만일 만료된 Access Token을 서버에 보내면, 서버는 같이 보내진 Refresh Token을  DB에 있는 것과 비교해서 일치하면 다시 Access Token을 재발급하는 원리이다.
  • 그리고 사용자가 로그아웃을 하면 저장소에서 Refresh Token을 삭제하여 사용이 불가능하도록 하고 새로 로그인하면 서버에서 다시 재발급해서 DB에 저장한다.

 

Access / Refresh Token 재발급 원리

1. 기본적으로 로그인 같은 과정을 하면 Access Token과 Refresh Token을 모두 발급한다.

이때, Refresh Token만 서버측의 DB에 저장하며, Refresh Token과 Access Token을 쿠키 혹은 웹스토리지에 저장한다.

2. 사용자가 인증이 필요한 API에 접근하고자 하면, 가장 먼저 토큰을 검사한다.

이때, 토큰을 검사함과 동시에 각 경우에 대해서 토큰의 유효기간을 확인하여 재발급 여부를 결정한다.

  • case1 : access token과 refresh token 모두가 만료된 경우  에러 발생 (재 로그인하여 둘다 새로 발급)
  • case2 : access token은 만료됐지만, refresh token은 유효한 경우  refresh token을 검증하여 access token 재발급
  • case3 : access token은 유효하지만, refresh token은 만료된 경우  access token을 검증하여 refresh token 재발급
  • case4 : access token과 refresh token 모두가 유효한 경우  정상 처리

3. 로그아웃을 하면 Access Token과 Refresh Token을 모두 만료시킨다.

 

Refresh Token 을 Redis 에서 사용하고 있는데 시간이 만료되었을 때 바로바로 제거가 되게끔 처리가 되려면 ?

  • @RedisHash(value = "refreshToken", timeToLive = 초단위시간)
  • 위와 같이 일정 기간을 정해서 시간이 다했을때 자동으로 삭제되도록 할 예정이고, 또한 로그아웃 API가 완성되면 로그아웃시에도 토큰이 삭제되도록 할 예정입니다.

 

Access Token과 Refresh Token을 어떻게 관리할지

1. 서버단에서는 리프레시 토큰을 Redis에 저장한다.

  • Redis에 저장하는 이유는 아래와 같다
    • 빠른 액세스 속도로 사용자 로그인시 (리프레시 토큰 발급시) 병목이 되지 않는다.
    • 또한 리프레시 토큰은 발급된 후 일정 시간 이후 만료되어야 한다. 리프레시 토큰을 RDB등에 저장하면, 스케줄러등을 사용하여 주기적으로 만료된 토큰을 만료 처리하거나 제거해야한다. 하지만, 레디스는 기본적으로 데이터의 유효기간(time to live)을 지정할 수 있다. 이런 특징들은 리프레시 토큰을 저장하기에 적합하다.
    • @RedisHash(value = "refreshToken", timeToLive = 초단위시간)
  • 물론 JWT와 같은 클레임 기반 토큰을 사용하면 리프레시 토큰을 서버에 저장할 필요가 없다. 하지만, 사용자 강제 로그아웃 기능, 유저 차단, 토큰 탈취시 대응을 해야한다는 가정으로 서버에서 리프레시 토큰을 저장하도록 구현하였다.

2. 프론트 단에서는 두가지의 경우가 있다. 어떤게 좋은지는 잘 모르겠다.

  • 쿠키에 보관
  • localStorage 보관

localStorage에 저장하는 경우

  • cookie의 httpOnly 옵션도 XSS 공격을 완벽히 막을 수 없다. 어차피 XSS 방어는 필수적이므로 cookie의 장점이 매력적이게 보이지 않는다.
  • cookie를 사용한다면 백엔드 api에 내가 사용하는 cookie를 위한 설정을 요구해야한다. 백엔드와 조율이 잘 되는 상태면 cookie를 사용해도 문제 없지만 서드파티 api의 경우 거의 불가능하므로 localStorage가 더 좋을 것 같다.
  • mdn은 저장소로 쿠키를 추천하지 않는다. 대신 ModernStorage(localStorage와 sessionStorage)를 추천한다.

cookie를 지지하는 경우

  • CSRF 공격은 다루기 쉬운 반면 프론트엔드 크기가 크면 클수록 XSS 공격을 막기위한 작업은 많아지므로 쿠키 사용을 추천
  • 쿠키는 별도로 헤더에 담지 않아도 요청을 보낼때 자동으로 담아서 보내지기 때문에 매번 서버로의 요청에 담아야하는 토큰이라는 성격과 잘 맞고, 코드도 더 간결하게 작성할 수 있다

 

Refresh Token의 한계점

리프레시토큰을 도입해서 액세스토큰의 공격범위를 줄인다고해도, 리프레시토큰마저 탈취당하면 도로묵이고.

그래서 JWT로그인을 도입할 때는 이 한계점이 명확하다는것을 유의해서 작업해야 할 것 같다.

 

또다른 큰 단점은, 임의적으로 생성된 JWT를 서버에서 임의적으로 삭제할 수 없다는 점이다. 

사실 이 점이 더 큰 단점이라고한다.

 

만약 리프레시토큰이 탈취당한다면 어떤 액션을 취해야할지? 

아니면 클라이언트측이 갖고있는 토큰을 아예 탈취도 못하도록 보안에 더 신경쓰거나.

아니면 탈취당했더라도 금새 리프레시토큰을 재발급할 수 있게 한다거나?

아무리 생각해봐도 뭐라도 하나 털린순간 피해를 당하긴 하는데,얼마나 줄이냐의 문제같다.

 


[ 참고 자료 ]

 

 

Spring Boot와 Redis를 사용하여 Refresh Token 구현하기

배경 바로 직전에 작성한 Access Token의 문제점과 Refresh Token 글에서 Refresh Token이 무엇인지 글로 알아보았다. 하지만, 글만 읽어서는 공부를 끝냈다고 할 수 없다. 실제로 코드를 작성해야 지식을

hudi.blog

 

JWT는 어디에 저장해야할까? - localStorage vs cookie

이번에 지하철 미션을 만들면서 JWT를 클래스 property에 저장했었는데 리뷰어 분께 해당 부분을 피드백 받으면서 어디에 JWT를 저장하는 것이 좋을까 에 대해 고민해보게 되었다. 0. 기본 지식 JWT Js

velog.io

 

 

리프레시 토큰이 필요한가?

로그인이 필요한가? 오쿠 프로젝트를 시작할 때, 로그인을 도입하느냐 마느냐에 대한 이야기가 나왔다. 로그인 기능이 없다면, 클라이언트에서 보내는 요청에 대해 유저를 식별할 수 없다. (HTTP

tuigun.tistory.com

 

Docker 설치

 

설치 확인

  • 터미널창을 열고 docker -v 입력
  • 버전 정보가 뜬다면 설치 완료!

 

Docker Command 명령어

 

버전 확인  $ docker -v
이미지 다운로드  $ docker pull [이미지 명]
다운로드된 이미지 목록  $ docker images
컨테이너 생성  $ docker create [옵션] [이미지 명]
컨테이너 생성 및 실행  $ docker run [옵션] [이미지 명]
컨테이너 실행  $ docker start [컨테이너 명]
컨테이너 재실행  $ docker restart [컨테이너 명]
컨테이너 접속  $ docker attach [컨테이너 명]
컨테이너 정지  $ docker stop [컨테이너 명]
실행중인 컨테이너 목록  $ docker ps
정지된 컨테이너 목록  $ docker ps -a
컨테이너 명 변경  $ docker rename [기존 컨테이너 명] [새로운 컨테이너 명]
컨테이너 삭제  $ docker rm [컨테이너 명]

 

컨테이너 생성 

docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]

docker run --name "컨테이너이름" "이미지"

docker run -it --name test-ubuntu ubuntu:20.04 /bin/bash
  • 컨테이너 내부에 들어가기 위해 터미널창을 실행하고 키보드 입력을 위해 -it 옵션을 준다.
  • 추가적으로 프로세스가 종료되면 컨테이너가 자동으로 삭제되도록 --rm 옵션도 추가 한다.
  • --rm 옵션이 없다면 컨테이너가 종료되더라도 삭제되지 않고 남아 있어 수동으로 삭제 해야 한다.
-d detached mode (백그라운드 모드)
-p 호스트와 컨테이너의 포트를 연결
-v 호스트와 컨테이너의 디렉토리를 연결
-e 컨테이너 내에서 사용할 환경변수 설정
--naem 컨테이너 이름 설정
--rm 프로세스 종료시 컨테이너 자동 제거
-it -i와 -t를 동시에 사용한 것으로 터미널 입력을 위한 옵션
--network 네트워크 연결
/bin/bash 명령어로 쉘 실행

 

 

※ 터미널창이 아니더라도 Docker GUI를 이용하면 더 편리하게 사용할 수 도 있다. 

 

 

 


[ 참고 자료 ]

 

생활코딩 Docker 

https://www.opentutorials.org/course/128/8657

 

Docker - 생활코딩

소개 가상 머신처럼 독립된 실행환경을 만들어주는 도구. 마치 운영체제에 운영체제를 설치하는 것처럼 실행 된다. 하지만 운영체제는 실제로 설치되지 않기 때문에 설치 용량이 적고 빠르다. 

www.opentutorials.org

https://acdongpgm.tistory.com/234

 

[Docker] . 도커 기본 명령어 정리

도커 기본 명령어 이미지 다운로드 docker pull ubuntu:20.04 *docker run 할때 이미지가 없으면 자동으로 찾아서 다운로드함.(도커 허브에 있는 이미지의 경우) 이미지 목록 출력 docker images 이미지 삭제 do

acdongpgm.tistory.com

https://www.youtube.com/watch?v=IiNI6XAYtrs 

 

사전 지식 

코딩의 과정

더보기
  • 컴파일

첫번째로 우리가 만든 코드를 컴파일 한다.

컴파일이란 우리가 만든 프로그래밍 언어를 기계가 이해할 수 있는 기계의 언어로 번역하는 것이다.

우리가 사용한 java, c와 같은 프로그래밍 언어는 기계가 이해할 수 없다. 이렇게 개발자의 편의를 위해 개발자의 언어로 작성한 프로그래밍 언어를 컴파일러가 컴파일 해 기계가 이해할 수 있는 언어로 번역해준다.

  • 빌드

다음은 컴파일된 기계의 언어를 사용자에게 보여주기 위해 빌드하여 완성된 상품, 소프트웨어 가공물로 만든다.

java에서는 maven, gradle과 같은 빌드 도구를 이용하면 컴파일과 함께 소스코드 파일을 .jar, .war 와 같은 산출물로 변환하는 빌드도 함께 할 수 있다.

  • 배포

이렇게 만들어진 산출물을 각각의 서버에서 동작하도록 하여 상품을 사용자들에게 공개하는 것이 배포이다. 최종적으로 만들어진 상품을 배포해 사용자에게 사용하게 하는 것이 우리의 목적이자 개발을 하는 이유가 되는 것이다.

프로젝트를 개발하고 배포를 진행할 때 많은 이들이 CI/CD에 대해 언급하곤 한다. 이 때 말하는 CI/CD란 무엇이고 왜 적용해야할까?

 

CI/CD

 

 

사용자에게 우리가 만들어낸 프로젝트를 배포했는데 어떠한 동작이 올바르게 동작하지 않아 문제가 발생했다고 가정해보자.

개발자들에게는 비상이 걸릴 것이다. 급한 프로젝트일수록 더욱 빠르게 문제를 수정해야만 한다. 수정을 했으면 다시 컴파일, 빌드, 배포하는 과정을 통해 수정된 코드가 제대로 동작하는지 테스트하고 검증할 필요가 있다. 이 과정들은 시간도 많이 걸리고 실수하기도 쉽다.

수정된 코드에 문제가 다시 생기면 또 다시 과정을 반복해야한다. 이를 위해서 CI/CD가 생겨났다.

 

CI (Continuous Integration)

  • CI는 지속적 통합이라는 뜻으로 개발을 진행하면서도 품질을 관리할 수 있도록 하는 것
  • 여러 명이 하나의 코드에 대해서 수정을 진행해도 지속적으로 통합하면서 관리할 수 있음을 의미한다.
  • CI가 나오기 전까지는 개발을 끝마치고 배포가 되어야만 코드에 오류는 없는지, 올바르게 동작하는지를 검증하며 코드 품질을 관리할 수 있었다. 하지만 개발자가 직접 코드를 병합하고 빌드, 테스트를 검증하는 것은 시간이 많이 소요될 뿐만 아니라 귀찮고 그 양도 프로젝트의 크기가 커질수록 많아질 수밖에 없다. 이를 자동화하면 개발자가 빌드와 테스트를 직접 하지 않고도 수정한 코드를 브랜치에 병합하기만 하면 자동으로 빌드와 테스트를 검증할 수 있다.
  • 개발자가 단위별로 구현한 부분을 병합할 때마다 자동화된 빌드와 테스트가 트리거되어 실행된다. 결과를 통해 우리는 어떤 부분에서 문제가 있는지 배포 전에 확인할 수 있고, 배포가 완성된 후에야 버그를 수정할 수 있던 기존의 문제를 빠르고 정확하게 해결할 수 있다
  • 결과적으로 버그를 신속하게 찾아 해결할 수 있을 뿐 아니라, 소프트웨어 품질을 개선하고 새로운 소프트웨어 업데이트를 검증하고 릴리즈하는데에 걸리는 시간을 단축할 수도 있다.

 

CI의 간단한 순서는 아래와 같다.

  1. 개발자가 구현한 코드를 기존 코드와 병합한다.
  2. 병합된 코드가 올바르게 동작하고 빌드되는지 검증한다.
  3. 테스트 결과 문제가 있다면 수정하고 다시 1로 돌아간다. 문제가 없다면 배포를 진행한다.

 

CD(Continuous Deployment)

  • 이제 지속적 통합을 거친 코드에 대해서 신뢰할 수 있고 바로 배포할 수 있다.
  • CD는 지속적 배포로 소프트웨어가 항상 신뢰 가능한 수준에서 배포될 수 있도록 관리하자는 개념으로 지속적 제공(Continuous Delivery)로 사용되기도 한다.
  • 지속적 제공은 CI를 통해서 새로운 소스코드의 빌드와 테스트 병합까지 성공적으로 진행되었다면, 빌드와 테스트를 거쳐 github과 같은 저장소에 업로드하는 것을 의미한다.
  • 지속적 배포는 이렇게 성공적으로 병합된 내역을 저장소뿐만 아니라 사용자가 사용할 수 있는 배포환경까지 릴리즈하는 것을 의미한다.
  • 대표적인 CI/CD의 방법으로는 Travis와 Jenkins가 있다.

 

 


[ 참고 자료 ]

 

https://www.youtube.com/watch?v=sIPU_VkrguI 


https://tecoble.techcourse.co.kr/post/2021-08-14-ci-cd/

 

CI/CD가 뭔가요? - 이론편

tecoble.techcourse.co.kr

https://www.redhat.com/ko/topics/devops/what-is-ci-cd

 

CI/CD(CI CD, 지속적 통합/지속적 배포): 개념, 툴, 구축, 차이

CI/CD는 애플리케이션의 통합 및 테스트 단계부터 제공 및 배포까지 애플리케이션 라이프사이클 전체에서 지속적인 자동화와 지속적인 모니터링을 제공하는 것을 뜻합니다.

www.redhat.com

 

 

1. 백엔드는 더 분발해야 한다. 너무 스코프가 작음    인프라 부분 강화하기

  • API명세 : Swagger 사용
  • 통합배포 : 깃헙 액션 사용
  • 배포 자동화 :  ( 깃헙 액션 ) CI/CD 사용
  • 무중단 배포 : Docker 사용

 

2. 개인 피드백 

  • 리프레시 토큰 관련/ 인증관련해서 더 공부하기 ( 무조건 면접 질문으로 나옴 )
  • 백에서 관리할지 프론트로 보낼지 연구해보기 

 

3. 지난 금요일 https 배포후 발생했던 오류에 대한 질문 

  • 로컬환경이랑 배포환경이 다르기때문에 오류가 날 수 있음 도커를 사용해야하는 이유
  • SSL 설정을 바꾼다거나 해야할 듯. ( 적용에 문제가 있을 듯 )

 

4. 웹소켓 세션을 서버 인메모리에 저장하고 있는데, 어떤게 좋은 방법인지에 대한 질문에 대한 답변

  • 지금 처럼 사용해도 되지만 서비스가 커져서 서버를 여러개 사용할 경우 한계가 있음
  • 네트워크 기반 스토리지에 돌리는게 맞다. 레디스에 저장하는 걸로 바꾸는게 좋음
  • 로컬캐시는 실제로 많이 사용하지만 성능적으로 보완해야 할 부분이 많다.

 

5. 스프링 3대 요소는 면접 단골 질문

  1. IOC/ DI
  2. AOP
  3. PSA
  4. Solid 객체지향 설계방법 

 

6. 추가 피드백 

  • Repository - service 단에 사용하는 쿼리를 분리해야한다.
  • 서비스단에서 너무 많이 DB를 조회하는 것은 좋지 않음. 특히 이 부분이 중복될 경우 따로 클래스를 만들어 분리하는게 좋음.

문제 상황

  • 생각보다 WebRTC 시그널링 연결이 오래 걸렸는데, http인 로컬 서버로만 했을 때는 문제없던 기능들이 https 서버로 환경을 바꿔주었더니 틈만나면 아래와 같은 에러 혹은 403, 404, 405에러, Error parsing HTTP request header 에러를 비롯한 아래와 같은 다양한 에러들을 뱉어냈다. 

 

에러 1
에러 2

 

원인

  • 현재 프론트엔드는 AWS S3로 배포를 했고, 백엔드는 AWS EC2로 각각 서버를 배포한 상태 
  • 배포한 주소는 다르지만 AWS에서 같은 인증서를 사용했다.
  • 그런데, 인증서에 백엔드 도메인만 등록하고 프론트 도메인 주소를 등록해 주지 않아서 발생한 오류!
  • 즉, 프론트엔드 도메인은 https 반영이 안되어 있고 백엔드 도메인만 https 반영이 되어 있어서 충돌이 났던 것 같다.(추측)

 

해결 방법

  • 사용하는 인증서에 백엔드 주소(api.namoldak.com), 프론트엔드 주소(namoldak.com) 모두 등록해 주었다.
  • AWS > Route 53 > 호스팅 영역 > 레코드

 

 

  • AWS Certificate Manager > 인증서

 

 

 

  • 아래에 레코드 생성을 클릭한 다음 프론트엔드 도메인 주소와 백엔드 도메인 주소 모두 사용하는 인증서에 등록해 주었더니 https와 관련된 모든 에러가 해결되었다 ! 

 

 

+ Recent posts