GameController

  • 웹소켓을 사용하기 때문에 @PostMapping 이 아닌 @MessageMapping 을 사용한다.
  • @PathVariable 대신 @DestinationVariable 을 사용한다.
@Slf4j
@RequiredArgsConstructor
@RestController
public class GameController {

    private final GameService gameService;

    // 게임 시작
    @MessageMapping("/game/{gameRoomId}/start")
    public ResponseEntity<?> gameStart(@DestinationVariable Long gameRoomId,
                                       GameDto gameDto) {
        gameService.gameStart(gameRoomId, gameDto);
        return ResponseUtil.response(StatusCode.GAME_START);
    }

 

KeywordRepository

  • SELECT * FROM table ORDER BY RAND(); 는 DB에서 랜덤으로 값을 추출해오는 쿼리문이다. 이 쿼리문을 이용하여 아래와 같이 작성하였다. limit 숫자 를 붙임으로써 숫자만큼의 데이터 혹은 원하는 데이터 만큼의 레코드를 가져올 수 있다.
  • @Query("select * from keyword(테이블 이름) k where k.category(테이블 안의 Colum) = :category order by rand() limit 4(4개 불러오기)")
  • 그리고 findTop4ByCategory 라는 JPA 쿼리를 통해 지정된 카테고리를 갖고있는 키워드가 4개만 조회되도록 했다. 
  • List<Keyword> findTop4ByCategory(@Param("category") String category);
public interface KeywordRepository extends JpaRepository<Keyword, Long> {
    @Query(value = "select * from keyword k where k.category = :category order by rand() limit 4", nativeQuery = true)
    List<Keyword> findTop4ByCategory(@Param("category") String category);

    @Query(value = "select * from keyword k where k.category = :category order by rand() limit 3", nativeQuery = true)
    List<Keyword> findTop3ByCategory(@Param("category") String category);
}

 

GameService ( 게임 시작 API )

1. 수정 전 코드

  • 수정 전 작성한 코드는 키워드 엔티티 안에 카테고리가 있고, 카테고리 엔티티는 따로 없는 상태로 구현한 로직이다.
  • 구현한 로직의 순서는 다음과 같다.
    • 카테고리 엔티티가 없기 때문에 바로 카테고리 조회가 불가능 했다. 그래서 가장 먼저 키워드 하나를 랜덤으로 조회해서 그 해당 키워드 카테고리를 가져온다. 랜덤 조회는 Math.random() 메소드를 이용했다.
    • 그 다음 방금 가져온 카테고리를 가진 모든 키워드들을 List<Keyword>에 담아 주었다.
    • 참여인원에 따라 keywordRepository에서 findTop4ByCategory(category) / findTop3ByCategory(category) 를 이용하여 랜덤으로 4개, 3개의 키워드를 가져온다.
    • 그다음 키워드와 게임 참여 인원을 키:밸류 로 맺어줄 HashMap<> keywordToMember 을 하나 만들어 준다. 
    • 게임참여 인원의 목록이 담긴 List<> memberListNickname 을 만들어 준다.
    • for문을 돌면서 랜덤으로 가져오 키워드를 게임 참여한 멤버에 배당한다.
    • 게임 정보를 담아주는 gameStartSet리포지토리에 반영된 데이터를 저장해 준다.
        // 멤버들에게 뿌려지게 될 키워드 전체 목록 불러오기
        List<Keyword> keywordList1 = keywordRepository.findAll();

        // 랜덤으로 키워드 하나 뽑기
        Keyword keyword1 = keywordList1.get((int) (Math.random() * keywordList1.size()) + 1);

        // 위에서 랜덤으로 뽑은 키워드의 카테고리
        String category = keyword1.getCategory();

        // 같은 카테고리를 가진 키워드 리스트 만들기
        List<Keyword> keywordList;

        if (gameRoomAttendees.size() == 4) {
            // 참여 멤버가 4명 이라면, 랜덤으로 키워드 4장이 담긴 리스트를 만들어 준다.
            keywordList = keywordRepository.findTop4ByCategory(category);
        } else if (gameRoomAttendees.size() == 3) {
            // 참여 멤버가 3명 이라면, 랜덤으로 키워드 3장이 담긴 리스트를 만들어 준다.
            keywordList = keywordRepository.findTop3ByCategory(category);
        } else {
            throw new CustomException(NOT_ENOUGH_MEMBER);
        }

        HashMap<String, String> keywordToMember = new HashMap<>();

        // 웹소켓으로 방에 참가한 인원 리스트 전달을 위한 리스트
        // 닉네임만 필요하기에 닉네임만 담음
        List<String> memberNicknameList = new ArrayList<>();

        for (GameRoomAttendee gameRoomAttendee : gameRoomAttendees) {
            memberNicknameList.add(gameRoomAttendee.getMemberNickname());
        }

        //게임룸 멤버한테 키워드 배당
        for (int i = 0; i < gameRoomAttendees.size(); i++) {
            keywordToMember.put(memberNicknameList.get(i), keywordList.get(i).getWord());
        }

        GameStartSet gameStartSet = GameStartSet.builder()
                .roomId(gameRoomId)
                .category(category)
                .keywordToMember(keywordToMember)
                .round(0)
                .spotNum(0)
                .winner("")
                .build();

        // StartSet 저장
        gameStartSetRepository.save(gameStartSet);

 

2. 수정 후 코드

  • 위에 작성된 코드는 모든 DB를 조회하는 로직이라 좀 더 효율적인 코드를 위해 Category라는 Enum 클래스를 만들어 주었다.
  • 아래는 변경된 서비스 부분이다.
  • 키워드 전체 목록을 가져와서 랜덤으로 하나 뽑아주는 로직을 제거하였다.
  • Enum 클래스 안에 만들어 놓은 .getRandom().name() 메소드를 이용하여 Enum으로 저장되어 있는 카테고리 중 하나를 랜덤으로 가져오게 하였다.

	// 카테고리 랜덤으로 가져오기 (변경된 부분)
        String category = Category.getRandom().name();

        // 같은 카테고리를 가진 키워드 리스트 만들기
        List<Keyword> keywordList;

        if (gameRoomAttendees.size() == 4) {
            // 참여 멤버가 4명 이라면, 랜덤으로 키워드 4장이 담긴 리스트를 만들어 준다.
            keywordList = keywordRepository.findTop4ByCategory(category);
        } else if (gameRoomAttendees.size() == 3) {
            // 참여 멤버가 3명 이라면, 랜덤으로 키워드 3장이 담긴 리스트를 만들어 준다.
            keywordList = keywordRepository.findTop3ByCategory(category);
        } else {
            throw new CustomException(NOT_ENOUGH_MEMBER);
        }

 

Category

  • getRandom() 메소드를 이용해 랜덤으로 값을 가져온다.
@Getter
public enum Category {

    인물, 동물, 음식, 만화, 영화, 악기;

    public static Category getRandom(){
        return values()[(int)(Math.random()* values().length)];
    }
}

 


[ 참고 자료 ]

 

 

 

Docker란 ? 

  • Docker는 애플리케이션을 신속하게 구축, 테스트 및 배포할 수 있는 소프트웨어 플랫폼
  • Docker는 소프트웨어를 컨테이너라는 표준화된 유닛으로 패키징하며, 이 컨테이너에는 라이브러리, 시스템 도구, 코드, 런타임 등 소프트웨어를 실행하는 데 필요한 모든 것이 포함되어 있다.
  • Docker를 사용하면 환경에 구애받지 않고 애플리케이션을 신속하게 배포 및 확장할 수 있다.
  • Docker를 사용하면 컨테이너를 매우 가벼운 모듈식 가상 머신처럼 다룰 수 있다. 또한 컨테이너를 구축, 배포, 복사하고 한 환경에서 다른 환경으로 이동하는 등 유연하게 사용할 수 있어, 애플리케이션을 클라우드에 최적화하도록 지원한다.
  • 도커에서 가장 중요한 개념은 컨테이너와 함께 이미지라는 개념이다.

 

컨테이너(Container)

  • 컨테이너는 격리된 공간에서 프로세스가 동작하는 기술이다. 가상화 기술의 하나지만 기존방식과는 차이가 있다.
  • 기존의 가상화 방식은 주로 OS를 가상화하였다. 우리에게 익숙한 VMware나 VirtualBox같은 가상머신은 호스트 OS위에 게스트 OS 전체를 가상화하여 사용하는 방식이다. 이 방식은 여러가지 OS를 가상화(리눅스에서 윈도우를 돌린다던가) 할 수 있고 비교적 사용법이 간단하지만 무겁고 느려서 운영환경에선 사용할 수 없었다.
가상머신과 도커

 

  • 전가상화든 반가상화든 추가적인 OS를 설치하여 가상화하는 방법은 어쨋든 성능문제가 있었고 이를 개선하기 위해 프로세스를 격리 하는 방식이 등장했다.
  • 하나의 서버에 여러개의 컨테이너를 실행하면 서로 영향을 미치지 않고 독립적으로 실행되어 마치 가벼운 VMVirtual Machine을 사용하는 느낌을 준다. 실행중인 컨테이너에 접속하여 명령어를 입력할 수 있고 apt-get이나 yum으로 패키지를 설치할 수 있으며 사용자도 추가하고 여러개의 프로세스를 백그라운드로 실행할 수도 있다. CPU나 메모리 사용량을 제한할 수 있고 호스트의 특정 포트와 연결하거나 호스트의 특정 디렉토리를 내부 디렉토리인 것처럼 사용할 수도 있다.
  • 새로운 컨터이너를 만드는데 걸리는 시간은 겨우 1-2초로 가상머신과 비교도 할 수 없이 빠르다.

 

이미지(Image)

  • 이미지는 컨테이너 실행에 필요한 파일과 설정값등을 포함하고 있는 것으로 상태값을 가지지 않고 변하지 않는다(Immutable).
  • 컨테이너는 이미지를 실행한 상태라고 볼 수 있고 추가되거나 변하는 값은 컨테이너에 저장된다. 같은 이미지에서 여러개의 컨테이너를 생성할 수 있고 컨테이너의 상태가 바뀌거나 컨테이너가 삭제되더라도 이미지는 변하지 않고 그대로 남아있다.
  • 이미지는 컨테이너를 실행하기 위한 모든 정보를 가지고 있기 때문에 더 이상 의존성 파일을 컴파일하고 이것저것 설치할 필요가 없다.
  • 새로운 서버가 추가되면 미리 만들어 놓은 이미지를 다운받고 컨테이너를 생성만 하면 된다. 한 서버에 여러개의 컨테이너를 실행할 수 있고, 수십, 수백, 수천대의 서버도 문제없다.

 

Docker를 사용해야 하는 이유

  • Docker를 사용하면 코드를 더 빨리 전달하고,
  • 애플리케이션 운영을 표준화하고,
  • 코드를 원활하게 이동하고,
  • 리소스 사용률을 높여 비용을 절감할 수 있다.
  • Docker를 사용하면 어디서나 안정적으로 실행할 수 있는 단일 객체를 확보하게 된다.
  • Docker의 간단한 구문을 사용해 완벽하게 제어할 수 있다. 폭넓게 도입되었다는 것은 Docker를 사용할 수 있는 도구 및 상용 애플리케이션의 에코시스템이 강력하다는 의미이다.

 

Docker 설치 : https://docs.docker.com/desktop/install/mac-install/

 

Install on Mac

 

docs.docker.com

 

Docker image 받는 hub : https://hub.docker.com/

 

Docker Hub Container Image Library | App Containerization

Deliver your business through Docker Hub Package and publish apps and plugins as containers in Docker Hub for easy download and deployment by millions of Docker users worldwide.

hub.docker.com


[ 참고 자료 ]

 

 생활코딩 Docker 입구 수업

Refresh Token 저장 위치

  • 크게는 두 경우로 저장할 수 있는데 Backend DB, Redis 등의 Storage에 저장하거나 Client측에 저장할 수 있다.
  • Backend에 저장할 경우 JWT의 원래 도입 배경인 서버를 Stateless하게 유지하려는 노력과 상반 될 수 있다.
  • Client측에서는 Local Storage 혹은 Cookie에 저장할 수 있다.

 

Front

토큰 기반 인증에서는 로그인한 사용자의 상태 정보를 클라이언트 단의 저장소(쿠키, 로컬 스토리지, 세션 스토리지 중 하나)에 토큰의 형태로 저장한다. 그렇다면 토큰은 쿠키, 로컬 스토리지, 세션 스토리지 중 어디에 저장하는 게 좋을까? 정답이 있는 것은 아니지만, 세션 스토리지에 저장하는 경우는 많지 않고 보통은 쿠키나 로컬 스토리지에 저장한다. 그리고 이 둘은 각각 장단점이 존재한다.

 

1. Local Storage

  • local storage 는 구현이 쉽지만 자바스크립트로 접근이 너무 쉬워서 XSS 공격에 취약하고 보안상 문제 소지가 많다.
  • 로컬 스토리지와 같은 웹 스토리지의 데이터는 매 요청마다 서버에게 자동으로 전송되는 것이 아니기 때문에 CSRF 공격에 상대적으로 안전하다.
  • 다만, HttpOnly 옵션으로 XSS 공격을 방지할 수 있는 쿠키와 달리 웹 스토리지는 그러한 보안 옵션들을 설정할 수 없다. 
  • 따라서 이스케이프 처리 등으로 XSS 공격에 대한 대응을 면밀히 해줄 필요가 있다. XSS 취약점이 발생하면 동일한 도메인 내에서 CSRF 공격까지 발생할 수도 있기 때문에 더욱 주의해야 한다.
  • 다행히 대부분의 현대 웹 어플리케이션이나 라이브러리들은 자동 이스케이프 처리를 해주는 경우가 많다. 하지만 어찌 됐든 한 번 설정해주면 그만인 쿠키에 비해서는 신경 써줄 부분이 많다는 단점이 있다.

 

  • HTTPOnly 와 Secure 옵션을 사용하고 CSRF 공격에 대비를 하면 어느정도 보안을 할 수 있다.
  • 클라이언트 단에 저장되는 만큼 토큰은 탈취되기 쉽다. 따라서 보안을 더욱 강화하는 것이 중요한데, 다행히도 쿠키는 보안을 위한 몇몇 옵션들을 설정하는 것이 가능하다.
    • 먼저, XSS 공격에 의해 쿠키가 탈취당할 수도 있으므로 HttpOnly 옵션을 설정하여 JavaScript로는 해당 쿠키에 접근할 수 없도록 해야 한다.
    • 다음으로, CSRF 공격에 의해 의도치 않게 쿠키가 전송되는 것을 막기 위해 SameSite 옵션도 Lax 혹은 Strict로 설정해줘야 한다.
    • 또한, Secure 옵션도 설정하여 HTTPS 프로토콜을 사용할 때만 쿠키가 전송될 수 있도록 해야 한다.

 

Back(Server Side)

서버 issue를 백엔드에서 관리하기 용이하다. 반대로 access token을 재발급하기 위한 API 요청이 많아질 수 있다.

 

1. Session

  • 세션에 저장하고 세션 만료 주기를 늘리는 방식. 사용자가 많은 경우를 고려하면 사용하지 않는 것이 좋다. 또한 JWT 이용 목적에 적합하지 않다.

2. DB

  • refresh token 를 데이터베이스에 저장한 후 index 값을 쿠키나 로컬스토리지에 저장하는 방법. 클라이언트 단에서 refresh token 을 노출하지 않는 추세로 바뀌는 중. refresh token을 DB에 저장해야만 탈취 당했을 때 해당 토큰을 폐기할 수 있다.(RTR기법)

 

내가 고려하고 있는 도입 방식

Refresh Token을 백엔드 서버에 저장

refresh token을 백엔드 서버에 저장해두고 클라이언트는 refresh token에 대한 정보를 쿠키로 가지고 있는 방식이다. 새로고침했을 때 access token을 html 자원과 함께 받기 위해 반드시 백엔드 서버를 거쳐야한다.

 

장점

  • 서버 issue를 백엔드 서버에서 대부분 처리하기 때문에 관리에 용이하다.
  • 사용자가 많아짐에 따라 생기는 서버 issue를 백엔드에서 모두 처리하기 때문에 관리 포인트가 프론트 서버, 백엔드 서버 둘로 나뉘지 않고 백엔드 서버 한 곳에서 처리된다. 따라서 서버 issue 관리에 용이하다.

단점

  • 새로 고침하면 항상 프론트 서버를 거쳐 백엔드 서버까지 요청이 전달된다.
  • token이 프론트 서버에 따로 저장되지 않기 때문에 access token을 얻기 위해서는 항상 백엔드 서버까지 요청이 전달되어야 한다. 요청 횟수, 요청에 드는 간접비용 등을 고려해봤을 때 비효율적일 수 있다.

 

Redis에 Refresh Token을 저장하는 이유

레디스는 key-value 쌍으로 데이터를 관리할 수 있는 데이터 스토리지이다. 데이터베이스라고 표현하지 않은 이유는 기본적으로 레디스는 in-memory로 데이터를 관리하므로, 저장된 데이터가 영속적이지 않기 때문이다.

데이터가 HDD나 SDD가 아니라 RAM에 저장하므로 데이터를 영구적으로 저장할 수 없는 대신, 굉장히 빠른 액세스 속도를 보장받을 수 있다. 빠른 액세스 속도와 휘발성이라는 특징으로 보통 캐시의 용도로 레디스를 사용한다. Refresh Token의 저장소로 레디스를 선택한 이유도 위와 같다. 빠른 액세스 속도로 사용자 로그인시 (리프레시 토큰 발급시) 병목이 되지 않는다.

 

또한 리프레시 토큰은 발급된 후 일정 시간 이후 만료되어야 한다. 리프레시 토큰을 RDB등에 저장하면, 스케줄러등을 사용하여 주기적으로 만료된 토큰을 만료 처리하거나 제거해야한다. 하지만, 레디스는 기본적으로 데이터의 유효기간(time to live)을 지정할 수 있다. 이런 특징들은 리프레시 토큰을 저장하기에 적합하다.

 

그리고 리프레시 토큰은 실수로 제거되어도 다른 데이터에 비해 덜 치명적이다. 최악의 경우가 모든 회원들이 로그아웃 되는 정도이다.

물론 JWT와 같은 클레임 기반 토큰을 사용하면 리프레시 토큰을 서버에 저장할 필요가 없다. 하지만, 사용자 강제 로그아웃 기능, 유저 차단, 토큰 탈취시 대응을 해야한다는 가정으로 서버에서 리프레시 토큰을 저장하도록 구현하는게 좋다.

 


[ 참고 자료 ]

https://www.cloudflare.com/ko-kr/learning/ssl/why-is-http-not-secure/

 

HTTP와 HTTPS 차이점

  • HTTPS 는 암호화 및 인증이 포함된 HTTP 이다 . 
  • 두 프로토콜의 유일한 차이점은 HTTPS가 TLS ( SSL )를 사용하여 일반 HTTP 요청 및 응답을 암호화하고 해당 요청 및 응답에 디지털 서명한다는 것.
  • 결과적으로 HTTPS는 HTTP보다 훨씬 더 안전하다.
  • HTTP를 사용하는 웹사이트는 URL에 http://가 있고 HTTPS를 사용하는 웹사이트는 https://가 있다.

 

HTTP란?

  • HTTP는 Hypertext Transfer Protocol의 약자이며 네트워크를 통해 데이터를 전송하는 데 사용되는 프로토콜이다. 
  • 웹 사이트 콘텐츠 및 API 호출을 포함하여 인터넷을 통해 전송되는 대부분의 정보는 HTTP 프로토콜을 사용한다. 
  • HTTP 메시지에는 요청과 응답이라는 두 가지 주요 종류가 있다.

 

HTTP 요청/응답

  • HTTP 요청은 사용자가 웹 속성과 상호 작용할 때 사용자의 브라우저에서 생성된다. 
  • 예를 들어 사용자가 하이퍼링크를 클릭하면 브라우저는 해당 페이지에 나타나는 콘텐츠에 대한 일련의 "HTTP GET" 요청을 보낸다. 
  • 누군가 Google에서 "HTTP가 무엇인가요?"라고 검색하면 어떤한 페이지는 검색 결과에 표시되며 링크를 클릭하면 브라우저가 페이지를 렌더링하는 데 필요한 정보를 얻기 위해 일련의 HTTP 요청을 생성하고 보낸다.
  • 이러한 HTTP 요청은 모두 origin server 또는 proxy caching server로 이동하며 해당 서버는 HTTP 응답을 생성한다. 
  • HTTP 응답은 HTTP 요청에 대한 응답을 의미한다.

 

HTTP 요청의 모습

HTTP 요청은 HTTP 프로토콜을 따르는 일련의 텍스트 줄이다. GET 요청은 다음과 같다.

  • 사용자의 브라우저에서 생성된 이 텍스트 섹션은 인터넷을 통해 전송된다. 
  • 문제는 연결을 모니터링하는 모든 사람이 읽을 수 있는 일반 텍스트로 위와 같이 전송된다는 것
  • 이는 사용자가 웹사이트나 웹 애플리케이션을 통해 민감한 데이터를 제출할 때 특히 문제가 된다.
  • 오고가는 데이터는 암호, 신용 카드 번호 또는 양식에 입력된 기타 데이터일 수도 있고 HTTP에서는 이 모든 데이터가 누구나 읽을 수 있도록 일반 텍스트로 전송된다. 

  • 웹 사이트에서 HTTPS 대신 HTTP를 사용하는 경우 세션을 모니터링하는 모든 사람이 모든 요청과 응답을 읽을 수 있다. 
  • 즉, 헤커는 요청 또는 응답의 텍스트를 읽고 누군가가 요청하거나 보내거나 받는 정보를 정확히 알 수 있다.

 

HTTPS란?

  • HTTPS의 S는 "보안(Secure)"을 의미한다. 
  • HTTPS는 TLS(또는 SSL)를 사용하여 HTTP 요청 및 응답을 암호화하므로 공격자는 텍스트 대신 겉보기에 무작위로 보이는 암호화된 여러 문자를 보게 된다.

 

헤커는 아래와 같은 데이터 대신에

 

다음과 같은 것을 보게 된다.

t8Fw6T8UV81pQfyhDkhebbz7+oiwldr1j2gHBB3L3RFTRsQCpaSnSBZ78Vme+DpDVJPvZdZUZHpzbbcqmSW1+3xXGsERHg9YDmpYk0VVDiRvw1H5miNieJeJ/FNUjgH0BmVRWII6+T4MnDwmCMZUI/orxP3HGwYCSIvyzS3MpmmSe4iaWKCOHQ==

 

HTTPS에서 TLS/SSL은 HTTP 요청 및 응답을 어떻게 암호화하는가?

  • TLS는 공개키 암호화 라는 기술을 사용한다 .
  • 공개키(public key)와 개인키(private key)의 두 가지 키가 있으며 공개키는 서버의 SSL 인증서를 통해 클라이언트 장치와 공유된다. 
  • 클라이언트가 서버와의 연결을 열면 두 장치는 공개키 및 개인키를 사용하여 세션키(session keys) 라고 하는 새로운 키에 동의하여 둘 사이의 추가 통신을 암호화한다.
  • 그런 다음 모든 HTTP 요청과 응답은 이 세션키로 암호화되므로 통신을 가로채는 사람은 일반 텍스트가 아닌 임의의 문자열만 볼 수 있다.

 

HTTPS는 웹 서버를 인증하는 데 어떻게 쓰이는가?

  • HTTP에서는 신원 확인이 없으며 신뢰 원칙을 기반으로 한다. 그러나 현대 인터넷에서는 인증이 필수적입니다.
  • ID 카드가 개인의 신원을 확인하듯이 개인키는 서버 신원을 확인한다. 
  • 클라이언트가 origin server와 채널을 열 때(예: 사용자가 웹 사이트를 탐색할 때) 웹 사이트 SSL 인증서의 공개키와 일치하는 개인키를 소유하면 서버가 실제로 웹 사이트의 합법적인 호스트임을 증명한다. 
  • 이렇게 하면 다음과 같이 인증이 없을 때 발생할 수 있는 여러 공격을 방지하거나 차단하는 데 도움이 된다.
    • On-path attacks
    • DNS hijacking
    • BGP hijacking
    • Domain spoofing

또한 SSL 인증서는 인증서를 발급한 인증 기관에서 디지털 서명된다. 이를 통해 서버가 자신이 주장하는 사람인지 확인할 수 있다.

 

 


[ 참고 자료]

https://www.cloudflare.com/ko-kr/learning/ssl/why-is-http-not-secure/

 

[ AWS로 https 설정하는 법 참고자료 ]

 

https://velog.io/@server30sopt/EC2-HTTPS%EB%A1%9C-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0

 

EC2 HTTPS로 연결하기

👾 작성자: 이승헌🐬 작성자의 한마디: https로 연결은 눈감고 한다우선 해당 글을 포스팅하기에 앞서 http에서 굳이 https로 연결하는 이유에 대해 이야기해보려 합니다.HTTP는 Hyper Transfer Protocol의

velog.io

https://pgmjun.tistory.com/69

 

[AWS] EC2 도메인 연결 및 HTTPS 적용하기

EC2 도메인 연결 & EC2 HTTPS 적용 안녕하세요 오늘은 AWS EC2에 도메인을 연결하고 HTTPS까지 적용해보는 시간을 갖도록 하겠습니다. 이 글은 이전에 생성한EC2가 이미 있다는 가정하에 HTTPS와 도메인

pgmjun.tistory.com

 

Access Token의 문제점

사용자의 잦은 로그아웃 경험

현재 진행중인 나몰닭 프로젝트는 Access Token 만을 사용하여 사용자를 인증한다. Access Token 유효 기한인 1시간이 지나면 유저는 로그아웃되고 다시 로그인을 진행해야 한다. 게임을 하다 중간에 방을 나왔을 때 로그인을 다시해야 한다면 굉장히 불편한 서비스 경험이 될 것이다. 그렇다고 유효 기간을 길게 한다면 아래와 같은 보안상의 문제가 발생한다.

 

보안 문제

Access Token은 JWT이므로 그 자체로 인증 정보를 모두 가지고 있어서 탈취되면 위험한 상황이 발생할 수 있다.  토큰 기반 인증 방식에서 토큰은 세션과 다르게 stateless 하다. 서버가 상태를 보관하고 있지 않다는 이야기이다. 서버는 한번 발급한 토큰에 대해서 제어권을 가지고 있지 않다. 즉, 토큰이 탈취될 경우 서버에서 토큰이 만료될때까지 기다리는 것 말고는 막을 방법 없이 사용자 계정의 제어권을 해커에게 내어줄 수 밖에 없다는 것이다. 

 

Refresh Token 이란?

목적

Refresh Token의 목적은 Access Token의 유효 기간을 짧고, 자주 재발급 하도록 만들어 보안을 강화하면서도 사용자에게 잦은 로그아웃 경험을 주지 않도록 하는 것이다.

Access Token은 리소스에 접근하기 위해서 사용되는 토큰이라면, Refresh Token은 기존에 클라이언트가 가지고 있던 Access Token이 만료되었을 때 Access Token을 새로 발급받기 위해 사용한다.

Refresh Token은 서버에 저장되기 때문에(stateful) refresh token이 해커에 의해 탈취당했다고 판단되었을 때 서버에서 refresh token을 삭제함으로써 강제 로그아웃을 시킬 수 있다. 이런 특징을 이용해서 access token + refresh token의 조합을 구성하면 access token의 경제적인 장점 refresh token의 보안적인 장점을 둘 다 챙길 수 있다.

 

유효 기간

Refresh Token은 Access Token 대비 긴 유효 기간을 갖는다. Refresh Token을 사용하는 상황에서는 일반적으로 보안적으로 취약한  Access Token의 유효기간은 30분 이내, Refresh Token의 유효기간은 처리 비용이 많이 들기 때문에 2주 정도로 설정한다고 한다. 유효 기간은 서비스 성격에 따라 적절하게 설정 해야한다.

 

Access Token + Refresh Token 인증 과정

https://tansfil.tistory.com/59

 

1. 사용자가 ID , PW를 통해 로그인한다.

2. 서버에서는 회원 DB에서 값을 비교한다. 

3~4. 로그인이 완료되면 Access Token, Refresh Token을 발급한다. 이때 일반적으로 회원DB에 Refresh Token을 저장한다.

5. 사용자는 Refresh Token은 안전한 저장소에 저장 후, Access Token을 헤더에 실어 요청을 보낸다.

6~7. Access Token을 검증하여 이에 맞는 데이터를 보낸다.

8. 시간이 지나 Access Token이 만료된다.

9. 사용자는 이전과 동일하게 Access Token을 헤더에 실어 요청을 보낸다.

10~11. 서버는 Access Token이 만료됨을 확인하고 권한없음을 신호로 보낸다.

 

** Access Token 만료가 될 때마다 계속 과정 9~11을 거칠 필요는 없습니다.

 사용자(프론트엔드)에서 Access Token의 Payload를 통해 유효기간을 알 수 있습니다. 따라서 프론트엔드 단에서 API 요청 전에 토큰이 만료됐다면 바로 재발급 요청을 할 수도 있습니다.

 

12. 사용자는 Refresh Token과 Access Token을 함께 서버로 보낸다.

13. 서버는 받은 Access Token이 조작되지 않았는지 확인한후, Refresh Token과 사용자의 DB에 저장되어 있던 Refresh Token을 비교한다. Token이 동일하고 유효기간도 지나지 않았다면 새로운 Access Token을 발급해준다.

14. 서버는 새로운 Access Token을 헤더에 실어 다시 API 요청을 진행한다. 

 

Refresh Token의 한계

Access Token을 즉시 차단할 방법의 부재

아무리 Refresh Token이 Access Token의 유효기간을 짧게 만들어 줄 수 있다고 하더라도, 탈취된 Access Token이 유효한 그 짧은 시간 동안에 악용될 수 있다는 위험성이 존재한다.

 

Refresh Token 그 자체를 탈취 당할 가능성

해커에게 Refresh Token 자체를 탈취 당하면 해커는 마음껏 Access Token을 발행할 수 있다. 서버 DB에서 Refresh Token을 저장해 직접 추적하는 방법을 사용하면 조금이나마 피해를 줄일 수 있겠지만, 피해가 확인되기 전까진 탈취 여부를 알 방법이 없다.

RTR을 사용한다면 Refresh Token을 1회 사용하고 버리게 되어 더 안전하게 사용할 수 있지만, 사용하지 않은 Refresh Token을 탈취당하면 해커는 1회 한정으로 Access Token을 발급받을 수 있다.

즉, 이러나 저러나 Refresh Token을 탈취 당할 위험성이 존재한다. 따라서 클라이언트는 XSS, CSRF 공격으로부터 Refresh Token이 탈취되지 않도록 안전하게 보관해야한다.

 

더보기

Refresh Token을 서버에 저장할 시 고려해볼 사항

보안 - 정상적인 사용자의 ip가 아니라면 refresh token을 삭제하도록 하기

refresh token은 서버에 저장된다. 그래서 보안적인 문제가 생기면 refresh token을 삭제함으로써 특정 사용자를 강제 로그아웃시킬 수 있다. 최초 로그인 한 ip를 서버에 저장하고, refresh token을 통한 access token 갱신 요청이 다른 ip로부터 온다면 비정상적인 접근으로 판단하고 refresh token을 삭제하여 강제 로그아웃시킨다.

그런데 이 방식도 UX적으로 단점이 있다. 요즘 항상 같은 곳에서만 컴퓨터를 사용하는 유저가 얼마나 되겠는가. 지금 글을 작성하는 시점에도 많은 사람이 카페에서 노트북을 사용하고 있는데 ip가 달라진다고 해서 강제로 로그아웃시킨다면 사용자 경험에 좋지 않을 것이다.

-->  네이버나 카카오톡처럼 ip가 달라지는 경우 보안 알림을 사용자에게 띄워 준다. 여기서 “아니요” 를 선택한다면 로그아웃되는 구조로 풀 수 있을 것 같다.

 

UX - 사용 중에 access token이 만료된다면 조용히 재발급 받기

만약 어떤 사용자가 새로 고침하지 않고 화면을 2시간 이상 띄워놓는 경우를 가정해보자. 가장 흔한 경우는 글을 작성하는 경우가 있다. 이 경우에는 access token이 만료되었다는 사실을 사용자가 모르게 하는 것이 가장 자연스러운 경험일 것이다.

 

방법 1) 요청 보낼 때 token 만료 에러가 발생하면 token을 새로 발급받기

인가가 필요한 요청을 보낼 때 token 만료 에러가 발생하면 token을 새로 발급받고, 새로 발급받은 token으로 다시 요청을 다시 보내는 것이다.

장점

  • 사용자가 인가가 필요한 작업을 요청할 때만 token이 재발급되어 경제적이다.

단점

  • 인가가 필요한 요청에 대한 에러처리, 재요청 로직이 추가로 요구된다.
  • token이 만료된 경우 인가가 필요한 요청을 결과적으로 2번 보내야한다.
  • token이 만료된다면 첫번째 요청은 실패할 것이고, 새로운 token을 발급받아 두번째 요청을 보내야 한다. 큰 문제가 되지는 않을테지만 단점이라 할 수 있다.

방법 2) 일정 시간마다 새로운 access token을 발급 받기

access token 만료 기간이 2시간이라면 1시간 55분 정도에 새로운 token을 발급한다. 이 방법을 사용하면 요청 관련 로직을 수정하지 않아도 되어 편리하다. 사용자가 사용하지 않을 때에도 백그라운드에서 계속 새로운 token을 발급받는다는 단점이 있을 수 있지만, 이것도 브라우저 포커스가 되어있는지를 감지해서 최적화도 가능하므로 큰 문제가 되지 않을 것으로 생각한다.

장점

  • setTimeout으로 token 재발급 로직만 작성해주면 요청 관련 로직 수정이 필요 없다.

단점

  • 시간과 관련하여 token을 재발급 받기 때문에 특정 시간이 지나면 무조건 재발급 요청을 보내어 비효율적일 수 있다.

 

 


[ 참고 자료]

 

 

refresh token 도입기

❗ SSR 상에서 refresh token을 도입하면서 느낀 것들을 작성한 글입니다. ❗ SSR에서 로그인이 어떻게 이루어지는지 궁금하시면 여기를 참고해주세요! 도입 계기 - 2시간이 지나면 로그인이 풀린다!

tecoble.techcourse.co.kr

 

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

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

hudi.blog

 

+ Recent posts