// 에드센스

출처: https://kr.freepik.com/free-vector/web-hosting-server-rack-isometric-icon-of-database-and-data-center-blockchain-digital-technology_4102271.htm

인터넷을 돌아다니다 사용자가 증가하면 서버를 어떻게 확장시킬지에 관한 글이 올라와있는것을 확인했다.

그냥 읽고만 넘어가기엔 너무 정리가 잘 돼있기에 내용을 다시 훑어보며 포스팅으로 녹여보고자 한다.

 

출처는 맨 밑에 접은글에 표기하였다.


1. 대규모 트래픽이 됐다.

사용하던 서비스가 대박이 나서 늘어난 트래픽을 서버가 견딜 수 없어한다. 이때 어떻게 해야할까?

간단하게 두 가지 방법을 통해 우리의 서버를 견고히 할 수 있을것 이다.

  • 더 성능 좋은 서버로 업그레이드
  • 서버를 여러대 추가로 설치

성능 좋은 서버로 업그레이드하는 방법이 Scale Up이고,

서버의 개수를 늘리는 방법이 Scale Out이다.

 

 

Scale Up

서버의 CPU나 메모리 등을 추가하여 서버 자체의 성능을 향상시키는 방법.

 

장점

  • 서버의 장비만 추가 or 교체하면 되기에 구축과 설계가 간단.
  • 별도의 컨트롤러나 네트워크 인프라 등 비용이 발생하지 않는다.
  • 추가적인 별도의 서버가 생기는 것이 아니기에 데이터 정합성 문제가 없다.

 

단점

  • 부품 업그레이드로는 성능 향상의 한계가 있다.
  • 하나의 서버에서 모든 트래픽을 감당하기에 장애극복 기능이 떨어진다.
  • 매번 장비 업그레이드를 할 때 마다 교체 비용이 든다.

 

 

 

 

Scale Out

서버 자체의 성능 향상이 아닌 서버의 대수를 늘리는 방법.

 

장점

  • 확장에 유연하다.
  • 특정 서버에 트래픽이 몰리는 것을 분산하여 부하를 막을 수 있다.
  • 하나의 서버에 문제가 생겨도 다른 서버가 있으므로 가용성을 높일 수 있다.

 

단점

  • 설계와 구현이 복잡하고 이에 따른 관리 비용도 증가한다.
  • 데이터 불일치 문제가 발생한다.

 

이 데이터 불일치 문제가 오늘 해결할 핵심 문제다.

서버1에서 로그인 처리를 했다고 이걸 서버2에서 알고있을까?

Scale Out 방식에서는 데이터 정합성에 대한 문제를 고민해야한다.

 

 

 

 

 


2. 다중 서버 환경에서 Session 관리

Scale Out을 통해 서버를 늘리면 다음 그림과 같은 상태가 된다.

 

각 서버마다 세션 저장소를 각자 보유하고 있고 이를 서로 공유하지 않기에 데이터 정합성 문제가 발생한다.

 

 

개선1. Sticky Session

 

Sticky Session 방식은 사용자가 한번 사용했던 서버를 이후 요청시 계속 사용하는 것이다.

즉 로드밸런서에서 User1이 WAS1에서 세션을 생성했다는 사실을 알게되면 User1의 요청은 이후부터 WAS1로만 보내지는 것이다. 

User1은 동일한 서버만 사용하게 되기에 정합성 문제가 해결되긴 한다. 

 

하지만,

 

고정된 세션을 사용한다는 것은 특정 서버에 트래픽이 몰릴 수 있다는 것을 의미한다. Scale Out을 한 의미가 없어진다.

 

 

 

 

 

개선2. Session Clustering

한마디로 표현하면, 여러 서버들을 동기화 하는 것이다.

Tomcat 9.0 Document를 보면 톰캣이 세션 클러스터링을 구현하는 방법으로 DeltaManager를 사용하여 all-to-all 세션 복제 방식을 제안한다.

 

이렇게 한 서버의 상태를 다른 세션 저장소에서 복제하여 반영하기에 어떤 서버로 로그인 요청을 보내도 데이터 정합성 문제는 발생하지 않게된다.

 

하지만 이 방법도 문제가 있어보인다. 

 

세션 저장소에 데이터가 입력될 때 마다 모든 서버에 똑같이 반영해 줘야하기에 서버 수에 비례하여 네트워크 트래픽이 증가한다. 따라서 너무 많은 서버를 가진 상태에서는 오히려 비효율적이다.

 

 

Tomcat에서는 이를 또 개선하기 위해 BackupManager를 활용한 Primary Secondary 세션 복제 방식을 제시한다.

 

얘도 간단하게 말하자면, Primary 서버에서 다른 서버로 모든 정보를 복제하는 것이 아닌 Key만 복제하게 하고 이를 통해 세션 전체를 복제하는 것보다 메모리 사용면에서 개선을 한 것이다.

 

 

 

 


3. 출제자의 의도

그래서 어떤 방법을 쓰라는 것일까? 정답은 물론 없지만 사람들이 "흔히" 쓰는 방법은,

기존 서버들이 가지고 있던 로컬 세션 저장소를 사용하는 것이 아닌 제 3의 저장소를 사용하는 것이다.

 

이렇게 하면

  • Sticky Session처럼 트래픽이 몰리는 현상이 일어나지 않고,
  • 서버 하나에 장애가 발생해도 다른 서버로 대체 가능하여 가용성을 확보할 수 있고,
  • 제 3의 통일된 스토리지에 세션을 저장하기에 데이터 정합성을 해결할 수 있다.

 

 

이때 어떤 세션 스토리지가 적당할지 요구사항을 정리해보자면,

  • Read/Write가 빈번하게 이루어지기에 Disk 기반 데이터베이스는 느린 IO속도로 인해 부적합.
  • In-Memory 데이터베이스를 사용한다면 메모리에서 Read/Write를 하기에 속도면에서 적합.
  • 데이터베이스 전원이 끊기면 데이터가 사라지는 In-Memory 데이터베이스이지만 세션들이 사라지는 것은 다시 로그인만 하면 되기에 치명적이지 않다.

즉, In-Memory 데이터베이스를 제 3의 세션 스토리지로 활용하는 것이 좋다.

이에는 Redis와 Memcached가 있는데 Redis는 이후에 따로 포스팅 할 예정이므로 이정도만 알고 넘어가자.

 

 

 

4. JWT 연전연승?

JWT를 처음 공부할 때 이런 상황(서버가 다수 존재할 때)에서 유용하게 쓰인다고 들었는데 정말 그럴 것 같다.

 

 

 

 

 

 

참고:

더보기

https://media.fastcampus.co.kr/insight/why-blockchain-is-hard/

이 카테고리는 Ground X에서 진행한 한양대학교 일반대학원 블록체인 융합학과 강의와 그 외의 참고자료를 보고 정리하는 곳입니다. 강의 동영상은 여기서 볼 수 있습니다.

 

너무 빈약한 지식이기에 이 글을 신뢰하지는 마세요.. 혹시 지나가시다가 잘못된 점 발견하시면 피드백 부탁드립니다.


1. 블록체인의 상태

 

블록체인은 트랜잭션으로 변화하는 상태 기계라고 할 수 있다.

  • 첫블록을 블록0이라고 했을때 이 블록은 초기값이 none이다.
  • 그리고 Changes(트랜잭션)가 일어나며 최종값이 정해진다.
  • 다음 블록1은 블록0의 최종값을 초기값으로 잡고 시작한다. 이 후로 쭉 이어진다.
  • 트랜잭션의 목적은 블록체인의 상태를 변경하는 것

 

 

어카운트(계정)

  • 상태는 어카운트라고 하는 오브젝트들로 구성돼 있다.
  • 각 오브젝트는 주소와 상태변화 내역을 가지고있다.
  • 어카운트는 EOA(External Owned Accounts)와 CA(Contract Account)로 구분된다.
  • EOA는 통상 사용자가 사용하는 어카운트다.

 

 

트랜잭션은 어카운트를 생성하거나 변경한다.

트랜잭션의 sender와 recipient가 있는데 recipient(받는이)가 누구냐에 따라서 트랜잭션의 목적이 나뉜다.

  • 기존에 존재하지 않던 주소로 트랜잭션을 발생시키면 새로운 컨트랙트 배포
  • 기존에 존재했던 주소로 트랜잭션을 발생시키면 보내면 그냥 토큰 교환.
  • CA주소로 트랜잭션을 발생시키면 컨트랙트 실행

 

 

 


2. 가스비

트랜잭션을 참여자들이 받고, 트랜잭션을 블록에 넣기 위해 검증, 실행, 블록으로 묶어서 저장을 해준다.

이 과정에서 비용이 발생한다.

  • 트랜잭션을 받기위해 네트워크를 열어놓는 비용
  • 트랜잭션을 받고 블록에 넣기까지 연산하는 비용
  • 블록으로 묶어서 저장하는 비용

이 비용은 sender가 부담한다. 토큰 보내는 사람이 수수료를 부담하는 그런 느낌.

이 가스비는 블록을 생성한 노드(트랜잭션 처리한 노드)가 수집한다.

 

 

즉, 트랜잭션을 처리하는데 필요한 자원(computing, power, storage)을 비용으로 전환한 것이 가스

 

 

이더리움은 소모되는 가스비를 미리 트랜잭션에 명시하는데 각 노드들은 이것을 보고 가스비가 높은 트랜잭션을 먼저 처리한다. 돈을 많이 준다니깐.

 

 

하지만 결국 하는 연산은 똑같기에 클레이튼은 가스비를 미리 명시하지 않도록 구현돼 있기에 사용자들끼리 경쟁을 하지 않고 선입선출식으로 트랜잭션을 처리하게 된다.

 

 

  • 노드(마이너)는 트랜잭션을 받는다.
  • 트랜잭션 속의 sender를 확인해서 sender의 어카운트에 유효한 만큼의 balance가 있는지 확인한다.
  • balance가 없으면 트랜잭션 거절
  • balance가 충분하면 트랜잭션을 체결(이때 가스비를 받는다)

 

 

 

 


3. 서명

내가 아닌 다른 사람이 나의 어카운트로 트랜잭션을 보내면 그 비용을 내가 지불해야하는 문제가 발생한다.

그렇기에 어느 특정 어카운트를 사용해서 트랜잭션을 발생시키려면 특정 어카운트로 검증할 수 있는 서명이 있어야한다. 트랜잭션은 항상 서명과 함께 움직인다.

 

 

비트코인

  • 공개키로 sender의 주소를 도출해서 sender를 확인.
  • 이걸로 서명도 검증
  • 증명 과정은 간단하지만 sender주소, sender의 공개키, sender의 서명 등 많은 정보를 트랜잭션에 담아야 하기에 비효율

 

 

이더리움

  • 트랜잭션에 sender의 주소 없이 서명만 넣는다.
  • 서명에서 공개키를 도출할 수 있는 어떤 함수가 있다.(노드가 할 수 있다)
  • 도출한 공개키로 만든 주소가 실존하면 검증완료.
  • 하지만 서명에서 공개키를 도출하는 과정에서 연산이 너무 많다. 느려짐.
  • 이게 이더리움이 성능 안좋다고 하는 이유의 3할 정도.

 

 

클레이튼

  • 이더리움 기반으로 만든 클레이튼은 공개키 도출 과정을 병렬화 시켜보았다. 연산량이 줄긴 줄었다.
  • 그래도 문제였기에 그냥 sender의 주소를 포함시켰다. 자세한 이유는 이후 강의에서 아라보자.

 

 

 


4. 트랜잭션

트랜잭션은 이런 구조를 가진다. 

{
    nonce: 1,
    from: '0xd5wef4gr5e4we1wx3e1rg5ce4w1wc3ce4w5a',
    to: '0x5e4f6w58df4e5461xcs21fed54e84x6qw5w1c2',
    value: 10
}
  • from과 to의 값은 내가 아무 텍스트를 입력한 것이다. 
  • 어카운트 기반 블록체인이 nonce값을 사용한다. 어카운트가 몇 번째 트랜잭션을 보냈는지 의미.

 

 

 

이더리움 트랜잭션은 다음과 같은 구조를 가진다.

{
    nonce: '0x01',
    gasPrice: '0x4a817c800',
    gas: '0x5208',
    value: '0xde0b6b3a764000',
    
    to: '0x3563535353535353535353535353535353535',
    v: '0x25',
    r: '0x4sd5f4ef4s2a4cd5dc3s1.......75735sdf7s35sa5',
    s: '0x4s85cs5dc45sc4s54dc2sd5.......sdc54sd5c4d5cs8'
}
  • from이 없다. 160비트 아끼기 위해
  • gas는 instruction을 몇개까지 실행하겠다. 라는 의미
  • gasPrice는 각 gas마다 얼마를 지불할지. 를 의미
  • gas x gasPrice는 총 가격이 나온다.
  • gas x gasPrice의 값 이상이 balance에 있어야 실행된다.
  • v,r,s는 전자서명 부분.

 

 

 

클레이튼 트랜잭션은 다음과 같은 구조를 가진다.

{
    type: 'VALUE_TRANSFER',
    nonce: '0x01',
    gasPrice: '0x4a817c800',
    gas: '0x5208',
    value: '0xde0b6b3a764000',
    
    to: '0x3563535353535353535353535353535353535',
    from: '0x5df4w5e4w6efe8fe1w3e2dwef512sd1f3s2g',
    
    v: '0x25',
    r: '0x4sd5f4ef4s2a4cd5dc3s1.......75735sdf7s35sa5',
    s: '0x4s85cs5dc45sc4s54dc2sd5.......sdc54sd5c4d5cs8'
}
  • 타입과 from이 생겼다.
  • to가 CA인지, EOA인지, 없는지에 따라 트랜잭션의 목적이 바뀐다고 위에 말했었는데,
  • type에 미리 트랜잭션의 목적을 명시해둔다.
  • gasPrice를 사용자가 바꿀 수 없다. (이더리움 트랜잭션 가스비의 1/10 미만)

 

 

 

 


5. 정리

사용자 -> Node

  • 사용자는 트랜잭션을 생성, 서명하여 Node(마이너)에게 전달
  • 이때 데이터 구조를 온전하게 전달하고자 RLP알고리즘으로 트랜잭션 직렬화
  • 사용자와 Node가 같은 프로토콜로 통신하는 것이 중요

 

Node -> 사용자

  • 올바른 트랜잭션 수신시 트랜잭션을 해시로 반환
  • 트랜잭션 체결 시 Receipt(영수증)반환(소요된 gas, 트랜잭션 해시, 인풋 등이 기록돼있다)

 

 

 

 

참고:

더보기

 

https://media.fastcampus.co.kr/insight/why-blockchain-is-hard/

이 카테고리는 Ground X에서 진행한 한양대학교 일반대학원 블록체인 융합학과 강의와 그 외의 참고자료를 보고 정리하는 곳입니다. 강의 동영상은 여기서 볼 수 있습니다.

 

너무 빈약한 지식이기에 이 글을 신뢰하지는 마세요.. 혹시 지나가시다가 잘못된 점 발견하시면 피드백 부탁드립니다.


1. 블록체인이란?

  • 블록은 정보다.
  • 정보를 블록이라고 하는 단위로 저장하고 이 블록들을 이어붙혀서 체인 형태로 저장하는 기술.
  • 블록은 더하기만 가능하고 수정과 삭제가 힘들다.
  • 블록은 [내 블록의 해시값], [이전 블록의 해시값], [내 데이터]로 구성된다.
  • 조금더 자세히 말해보면, 블록체인은 데이터 분산 처리 기술이다.
  • 블록은 개인과 개인의 거래 장부의 역할을 하고, 이를 조작하려면 모든 사람의 장부를 조작해야한다.

 

출처 : 한국전자통신연구원

 

기존 거래 방식

은행이 모든 거래 내역을 가지고있다. A가 B에게 10만원을 송금한다고 하면 은행이 중간역할을 한다.

A가 B에게 10만원을 보냈다는 사실을 증명해줘야 하기 때문이다.

 

우리는 오직 은행만을 신뢰하게 된다. 증명해 줄 곳이 은행뿐이니까. 그렇기에 이 은행에 문제가 생기면 A는 돈을 보냈다는 사실을 증명할 수 없고 결국 돈을 잃는다.

 

 

 

블록체인 방식

블록체인도 거래 내역을 저장하고 증명한다. 단, 은행처럼 한 곳이 전담하는게 아닌 여러명이 나눠서 맡는다.

한 네트워크에 10명의 참여자가 있다면 A가 B에게 10만원을 보낸 내역을 10개의 블록으로 생성해 10명 모두에게 전송, 저장한다. 나중에 거래내역을 확인할 때는 블록으로 저장한 데이터들을 연결해 확인한다. 

 

보안이 탄탄한 한 명이 모든 장부를 관리하는 것이 아닌 모두가 장부를 관리한다. 그렇기에 장부를 조작하려면 한 개만 조작해선 될게 아니다.

 

 

 

 


2. 블록체인의 활용

비트코인의 구조 출처: http://www.smallake.kr/

화폐(Currencies)

  • 이체와 화폐의 기능을 수행하는 전자 화폐.
  • 규제, 감독, 법제화 등 관련된 공인성은 없음
  • 예) 비트코인, 라이트코인, 다크코인, 피어코인, 도기코인 등

자산 등록(Asset Registry)

  • 자산 등록을 블록체인에 기록 하고, 개인키로 자산의 소유권을 주장하는 장부 기능.
  • 블록의 크기가 화폐용 블록보다 상대적으로 크며, 네트워크 성능 저하와 고비용 수반함.
  • 예) 컬러드코인, 옴니, 카운터파티 등

응용 플랫폼(Application Platform)

  • 네트웍상에 존재하는 블록체인에 응용프로그램을 개발하고 작동시키는 플랫폼 역할
  • 아직까지 서비스 초기이기 때문에 취약점이 다수 존재
  • 예) 이더리움, 에리스, NXT 등

자산 중심(Assent Centric)

  • 화폐, 자원. 주식, 채권의 거래를 일부 사용자만 볼 수 있는 공유장부에 기술함. (비트코인은 공개된 장부)
  • 외환 거래, 송금, 결제, 이체를 목적으로 두고 있음.
  • 예)리플, 스텔라

 

 

 

 


3. 해시함수

해시는 전에도 정리했던 적이 있다.

https://llshl.tistory.com/17?category=942551 

 

[Java] 해시/해시테이블이란?

사실 자료구조 카테고리에 맞는 게시글이지만 아직 자료구조 카테고리가 없고 앞으로 딱히 만들 계획이 없기에, 그리고 구현을 자바로 했기에 자바 카테고리에 넣었다! 그냥 그런걸로 하자 ㅎ

llshl.tistory.com

 

  • 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하는 함수,
  • 한 개의 인풋은 한 개의 아웃풋만을 가진다.
  • 조금만 인풋이 달라도 아웃풋은 크게 바뀐다.
  • 블록체인에는 Keccak256을 많이 쓴다 카더라(아직 잘 모름)
  • [이전 블록의 해시값]을 통해서 이전 블록을 포인팅하기에 블록의 순서를 알 수 있다.

 

 

 

 


4. 블록 생성 과정

블록 생성 시간 == 거래 소요 시간

  • 블록체인상에서의 장부 작성은 블록체인 네트워크에 참여하고 있는 사람들(이를 노드라고 한다)이 그 기록이 참인지 거짓인지 과반수의 동의를 얻어야 장부에 기록이 된다.
  • 이 노드들은 전 세계적으로 분산되어있고 전세계를 아우르는 하나의 거대한 장부를 형성한다. 따라서 신뢰가 필요가 없이 합의를 통해서 기록이 작성된다. 
  • 가장 많고 가장 빨리 작업을 한 사람이 블록에 기록하고 블록을 생성할 수 있는 투표권을 더 많이 갖는다고 할 수 있다. (작업증명)
  • 합의 알고리즘은 여러 방법이 있고 3개만 알아보자.

 

 

 


5. 합의 알고리즘

합의 알고리즘이란,

  • P2P 네트워크에서 정보의 지연과 미 도달을 통해 잘못된 정보 or 정보의 중복이 발생할 수 있다.
  • 이때 어떤 블록이 정당한지 검토하고 체인에 연결하기 위해 참가자들의 함의를 얻기 위한 알고리즘이다.

 

 

PoW

  • Proof Of Work, 비트코인, 비트코인 캐시, 라이트코인 등이 있다.
  • 누구나 참여 가능.
  • 풀기 어려운 문제를 빨리 해결한 사람에게 블록을 생성할 수 있는 권한을 주고 그 보상으로 코인을 제공하는 알고리즘
  • [앞 블록의 해시] + [Nonce] = [어떤 해시 값]을 만족하는 Nonce를 찾는 문제이다.
  • 난이도라는게 존재하는데 난이도가 4라면 어떤 해시 값은 4개의 0으로 시작하는 값이어야 한다.
  • 즉, Nonce를 빠르게 돌리면서 4개의 0으로 시작하는 해시값이 나오면 Nonce를 찾았다고 할 수 있다.
  • 연산량의 51퍼센트를 한 명의 참여자가 소유하면 블록을 조작할 수 있다. 
  • Nonce를 찾기 위해 전기를 너무 많이 써야한다.

 

 

PoS

  • Proof Of Stake, 대시, 네오 등이 있다.
  • 자신이 네트워크에 얼마만큼의 지분을 가지고 있는지로 판단. 즉 가지고있는 재산에 비례
  • 지분을 많이 가지고 있으면 높은 확률로 다음 블록을 제안할 수 있다.
  • 중앙 집중화를 예방하고 PoW보다 에너지를 절약한다.
  • 코인을 독식한 사람이 너무 강한 권력을 지니게 된다.
  • 그렇기에 PoW와 연계하여 사용한다고 한다.

 

 

BFT

  • PoW와 PoS의 단점인 *파이널리티의 불확실성을 해결한 것
  • 정해진 순번에 따라서 다음 블록을 누가 제안할지 정함
  • 네트워크가 동기화 되어있다. 즉 네트워크 참여자가 누구인지 다 알고있기에 새로운 참여자가 들어가면 작업을 멈추고 동기화를 진행해야한다.
  • 좀 느림

 

* 블록체인이 분기하게 되는 경우 긴 체인이 올바른 것으로 판단한다. 짧은 체인이 버려지는 경우 트랜잭션이 없었던 일이 될 수 있다.

 

 

 

 


6. Public vs Private

  • 누구든지 기록된 정보를 자유롭게 읽을 수 있는가?
  • 명시적인 등록 또는 자격 취득 없이도 블록체인 네트워크에 기록할 수 있는가?

라는 질문에 예라고 답하면 Public/공개형 블록체인이라고 한다.

반대로 정보가 공개돼 있지 않고 미리 자격을 얻은 사람만이 정보를 기록할 수 있다면 Private/비공개형 블록체인이라고 한다.

 

퍼블릭 블록체인은 누구나 접근 가능하고 익명이다.

프라이빗 블록체인은 정보를 비밀로 공유하고 아무나 못들어온다. 또 익명이 아니다.(기업 특화)

 

 

 

 

 

 

참고:

더보기

https://medium.com/@kimjunyong/5-%EB%B8%94%EB%A1%9D%EC%B2%B4%EC%9D%B8-%ED%95%A9%EC%9D%98-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-1%ED%8E%B8-pow-pos-dpos-21f8e3b2c22a

 

블록체인 합의 알고리즘 알아보기 1편(PoW, PoS, DPoS)

안녕하십니까 블록체인 알려주는 남자 Ryan KIM 입니다.

medium.com

https://medium.com/b-ock-chain/pow-%EC%99%80-pos-%EC%9D%98-%EC%A0%95%EC%9D%98-962a36d0979

 

PoW 와 PoS 의 정의

안녕하세요 이번에 PoW 와 PoS에 대한 포스팅을 맡게된 Seonmi입니다 :)  앞으로 잘부탁드릴게요!

medium.com

https://www.markany.com/kr/portfolio-posts/%EC%9D%80%EA%B7%BC-%EC%9E%98-%EB%AA%A8%EB%A5%B4%EB%8A%94-%EB%B8%94%EB%A1%9D%EC%B2%B4%EC%9D%B8-%EA%B0%9C%EB%85%90/

 

다 아는 것 같은데 은근 잘 모르는 블록체인의 개념 - 마크애니

다 아는 것 같은데 은근 잘 모르는 블록체인의 개념 ‘블록체인’하면 어떤 게 떠오르세요? 많은 분들이 ‘코인’, 가상화폐를 제일먼저 떠올리실 것 같습니다. 재작년 소위 비트코인광풍으로

www.markany.com

https://wooaoe.tistory.com/19

 

[신기술]블록체인 이란? - 블록체인 개념 이해하기(퍼블릭,프라이빗,컨소시엄,하이브리드)

블록체인은 무엇일까요? 블록체인 이란? 블록체인은 데이터 분산 처리 기술이다. 즉, 네트워크에 참여하는 모든 사용자가 모든 거래 내역 등의 데이터를 분산, 저장하는 기술을 지칭하는 말이다

wooaoe.tistory.com

https://www.youtube.com/playlist?list=PLKqrwxupttYEcJhWAw0E_5RVpDD9LD6Q- 

 

Klaytn 클레이튼 스마트계약과 탈중앙앱 강의

이 수업은 한양대학교 일반대학원(석사과정) 블록체인 융합학과에서 한 학기동안 진행하게 된 블록체인 플랫폼 클레이튼을 활용한 스마트계약과 탈중앙앱 개발에 대한 수업입니다. 이 강의를

www.youtube.com

 

 

이전에 구현했던 JWT 로그인에 Refresh Token을 추가해보았다.

전체 코드는 여기에 올렸다.


1. Refresh Token이란

지난 포스팅에선 Access 토큰만 사용하여 인터셉터가 적용된 자원에 접근해보았다. 

하지만 이 Access 토큰을 사용하여 요청과 응답을 받는 과정에서 우리는 이 토큰을 탈취당할 수 있다.

토큰은 [헤더]+[페이로드]+[비밀키]로 암호화 되어있지만 성능 좋은 컴퓨터로 비밀키를 알아내면 우리는 모든 정보와 권한을 빼앗기에 된다.

 

이를 보완하고자 Refresh 토큰의 개념이 사용된다.

  • Access 토큰의 유효기간은 매우 짧게
  • Refresh 토큰의 유효기간은 길게

해주는 것이 포인트다.

 

 

사용자는 Access 토큰을 통해서만 자원에 접근이 가능하고 Refresh 토큰은 소용이 없다.

그럼 언제 Refresh 토큰을 사용하는가?

유효기간이 짧은 Access 토큰이 만료가 되면 Refresh 토큰을 확인하여 검증 후 Access 토큰을 재발급해준다.

 

 

즉,

  • Access 토큰을 통해서만 자원에 접근이 가능, But 유효기간이 매우 짧다(탈취를 당해도 이미 사용할 수 없는 상태)
  • Refresh 토큰은 유효기간이 길기에 탈취당할 수도 있지만 Refresh 토큰은 오직 Access 토큰을 재발급하는 용도(Refresh 토큰 자체로는 별 쓸모가 없다.)

 

 

 

 


2. 인증 과정

 

위 사진은 로그인으로 설명이 돼있는데 회원가입으로 설명하는 것이 흐름을 알아보기에 조금 더 좋을 것 같아서 조금 바꿔서 설명하겠다.

 

  1. 사용자 회원가입 정보 입력.
  2. 유효한 데이터가 들어왔다면 회원가입 처리(DB 등록)
  3. 사용자 정보와 권한이 들어가 있는 Access 토큰과 Refresh 토큰 발급 (이때 Refresh 토큰은 DB에 저장한다.)
  4. 클라이언트는 두 종류의 토큰을 받는다.
  5. 이후 사용자가 데이터를 요청할 때마다 Access 토큰을 동봉하여 보낸다.
  6. 서버는 사용자로부터 전달된 Access 토큰이 유효한지만 판단한다(어자피 사용자의 권한과 정보는 토큰에 자체적으로 있다.)
  7. Access 토큰이 유효하면 사용자의 요청을 처리해서 반환해준다.
  8. 이때, 유효기간이 짧은 Access 토큰이 만료됐다고 해보자.
  9. 사용자는 만료된 Access 토큰으로 데이터 요청을 보낸다.
  10. 서버에서는 토큰에 대한 유효성 검사를 통해 만료된 토큰임을 확인한다.
  11. 클라이언트에게 "너의 토큰은 만료되었으니 갱신하기위해 Refresh 토큰을 보내라" 라고 응답한다.
  12. 클라이언트는 Access 토큰 재발급을 위해 Access 토큰과 Refresh 토큰을 전송한다.
  13. 전달받은 Refresh 토큰이 그 자체로 유효한지 확인하고, 3번에서 DB에 저장해 두었던 원본 Refresh 토큰과도 비교하여 같은지 확인한다.
  14. 유효한 Refresh 토큰이면 Access 토큰을 재발급 해준다.
  15. 만약 Refresh 토큰도 만료됐다면 로그인을 다시하고 Access 토큰과 Refresh 토큰을 새로 발급해준다.

 

더 간단한 흐름도로 확인하면 다음과 같다.

 

 

 

Refresh 토큰을 통해

  • Access 토큰의 유효기간을 짧게 잡을 수 있다.(탈취 방지)
  • Access 토큰의 유효기간에 짧음에도 불구하고 Refresh 토큰(유효기간 김)이 만료될때까지 추가적인 로그인을 안해도 된다. 마치 세션이 유지되는 것 처럼.

 

 

또 이러한 JWT 방식은

  • 서버가 다수 존재하는 환경에서 유용하다. 세션을 사용하면 모든 서버에서 세션 내용을 공유해야 하기 때문
  • 또한 매 요청시마다 DB 조회를 안하고 토큰 자체만으로 사용자의 정보와 권한을 알 수 있기에 병목현상을 방지한다.

 

 

 

 


3. 구현

구조

 

 

 

우리의 목표는 이 API에 접근하는 것

    @PostMapping("/user/test")
    public Map userResponseTest() {
        Map<String, String> result = new HashMap<>();
        result.put("result","success");
        return result;
    }

 

 

 

이는 인터셉터로 막혀있다

@Component
@RequiredArgsConstructor
public class JwtTokenInterceptor implements HandlerInterceptor {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        System.out.println("JwtToken 호출");
        String accessToken = request.getHeader("ACCESS_TOKEN");
        System.out.println("AccessToken:" + accessToken);
        String refreshToken = request.getHeader("REFRESH_TOKEN");
        System.out.println("RefreshToken:" + refreshToken);

        if (accessToken != null && jwtTokenProvider.isValidAccessToken(accessToken)) {
            return true;
        }

        response.setStatus(401);
        response.setHeader("ACCESS_TOKEN", accessToken);
        response.setHeader("REFRESH_TOKEN", refreshToken);
        response.setHeader("msg", "Check the tokens.");
        return false;
    }
}

 

 

 

회원가입을 하면 Access 토큰과 Refresh 토큰 발행

    // JWT 토큰 생성
    public String createAccessToken(String userId) {
        Claims claims = Jwts.claims();// JWT payload 에 저장되는 정보단위
        claims.put("userId", userId);
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + ACCESS_TOKEN_VALID_TIME)) // 만료시간 설정
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

클라이언트에서 이렇게 토큰을 받는다.

 

 

 

Access 토큰을 통해 데이터 요청

잘 받아졌다.

시간이 지나 Access 토큰이 만료되면 

아무 응답도 받지 못함과 동시에 토큰이 만료됐음을 확인할 수 있다.

 

 

 

Refresh 토큰을 사용하여 Access 토큰 재발급

Access 토큰은 재발급 된 거, Refresh 토큰은 기존의 것. 근데 Refresh 토큰도 매번 재발급 해주면 보안에 좋다.(Refresh Token Rotation)

    public TokenResponse issueAccessToken(HttpServletRequest request){
        String accessToken = jwtTokenProvider.resolveAccessToken(request);
        String refreshToken = jwtTokenProvider.resolveRefreshToken(request);
        //accessToken이 만료됐고 refreshToken이 맞으면 accessToken을 새로 발급
        if(!jwtTokenProvider.isValidAccessToken(accessToken)){  //클라이언트에서 토큰 재발급 api로의 요청을 확정해주면 이 조건문은 필요 없을 것 같다.
            System.out.println("Access 토큰 만료됨");
            if(jwtTokenProvider.isValidRefreshToken(refreshToken)){     //들어온 Refresh 토큰이 자체적으로 유효한지
                System.out.println("Refresh 토큰은 유효함");
                Claims claimsToken = jwtTokenProvider.getClaimsToken(refreshToken);
                String userId = (String)claimsToken.get("userId");
                Optional<User> user = userRepository.findByUserId(userId);
                String tokenFromDB = authRepository.findByUserId(user.get().getId()).get().getRefreshToken();
                System.out.println("tokenFromDB = " + tokenFromDB);
                if(refreshToken.equals(tokenFromDB)) {   //DB의 원본 refresh토큰과 지금들어온 토큰이 같은지 확인
                    System.out.println("Access 토큰 재발급 완료");
                    accessToken = jwtTokenProvider.createAccessToken(userId);
                }
                else{
                    //DB의 Refresh토큰과 들어온 Refresh토큰이 다르면 중간에 변조된 것임
                    //예외발생
                    System.out.println("Refresh Token Tampered");
                }
            }
            else{
                //입력으로 들어온 Refresh 토큰이 유효하지 않음
            }
        }
        return TokenResponse.builder()
                .ACCESS_TOKEN(accessToken)
                .REFRESH_TOKEN(refreshToken)
                .build();
    }

Refresh 토큰을 사용하여 Access 토큰을 재발급하는 메소드인데 뭔가 내용이 많아보이지만 사실상 간단하다.

 

 

  • Access 토큰이 만료됐다면
  • Refresh 토큰은 유효하다면
  • DB에 저장돼있는 Refresh 토큰 원본과 지금 들어온 Refresh 토큰이 일치한다면

Access 토큰을 재발급 해주는 흐름이다.

 

 

또, 이 글을 작성하면서 알게된 사실인데 Refresh 토큰의 페이로드에는 사용자 정보를 넣지 않는게 좋다고 한다. 생각해 보니 그런 것 같다. Refresh 토큰은 유효기간이 길기에 탈취될 수 있고(그 자체로는 뭘 할순 없지만) 탈취되어 내용을 까보면 치명적이진 않더라도 사용자의 정보가 노출되어 버리기 때문인 것 같다. 수정해야겠다.

 

 

 

 


4. Refresh 토큰에 대한 고찰

로그인이나 회원가입을 하고 Refresh 토큰을 클라이언트에게 반환하는 것을 보안상 문제로 인해 지양해야 한다는 주장이 있다. 카카오는 작년 업데이트를 통해 Javascript를 통한 카카오 로그인 기능에서 response 값에서 Refresh 토큰을 제외하기로 했다.

 

 

https://devtalk.kakao.com/t/javascript-api-sdk-refresh-token/105942

 

[공지] JavaScript 키를 이용한 API/SDK 사용 시 refresh token 응답 필드 제거 안내

카카오 데브톡. 카카오 플랫폼 서비스 관련 질문 및 답변을 올리는 개발자 커뮤니티 사이트입니다.

devtalk.kakao.com

 

 

현재 나의 자그마한 뇌로는 구체적으로 어떻게 위협이 될 수 있는지 잘 모르겠다만 아무래도 민감한 정보를 클라이언트에 노출하는 것이다보니 이런 결정을 한 것 같다.

 

이 뿐만 아니더라도 JWT에 대한 정보를 찾다보면 클라이언트로 발급한 Refresh 토큰을 어디에 저장해야 하는지에 대한 고민을 가진 사람들이 많았다.

 

그 중 어떤 분이 Refresh 토큰을 토큰 자체로 반환하지 말고 DB에 저장된 곳의 인덱스(정수 or 해시값)만 반환한다면, 클라이언트 측에서는 무의미한 인덱스 숫자만 알게 되는 것이기에 보안적으로 조금 더 좋다는 의견을 제시했다.

https://doogle.link/jwt-%ED%98%B9%EC%9D%80-oauth2-%EC%9D%98-refresh-%ED%86%A0%ED%81%B0%EC%9D%84-%EC%96%B4%EB%94%94%EB%8B%A4-%EC%A0%80%EC%9E%A5%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C/

 

JWT 혹은 OAuth2 의 refresh 토큰을 어디다 저장해야 할까? | 두글 블로그

요즘 네이버로그인, 카카오 로그인이나 구글 로그인등등 소셜 미디어(Social media) 사용자 로그인 처리를 하다보니 로그인된 상태가 끊임없이 유지되는 것을 구현해야 되더군요. 그러려면 결국 리

doogle.link

 

 

상당히 멋진 아이디어라고 생각됐기에 나름대로 구현을 시도해보았다. 

결과부터 말하면 실패.

 

 

    //refresh 토큰의 인덱스를 통한 검증
    public TokenIndexResponse issueAccessIndexToken(HttpServletRequest request){
        String accessToken = jwtTokenProvider.resolveAccessToken(request);
        Long refreshTokenIndex = jwtTokenProvider.resolveRefreshIndexToken(request);
        if(jwtTokenProvider.isOnlyExpiredToken(accessToken)) {  //만료만 된 토큰이라면
            Optional<Auth> findAuth = authRepository.findById(refreshTokenIndex);
            String refreshTokenFromDB = findAuth.get().getRefreshToken();
            Claims claimsToken = jwtTokenProvider.getClaimsToken(refreshTokenFromDB);
            String userId = (String)claimsToken.get("userId");
            if (jwtTokenProvider.isValidRefreshToken(refreshTokenFromDB)) {
                System.out.println("Access 토큰 재발급 완료 by 인덱스");
                accessToken = jwtTokenProvider.createAccessToken(userId);
            } else {
                //예외발생
                System.out.println("Refresh Token Tampered");
            }
        }
        return TokenIndexResponse.builder()
                .ACCESS_TOKEN(accessToken)
                .REFRESH_TOKEN_INDEX(refreshTokenIndex)
                .build();
    }

 

 

위에서 Refresh 토큰의 페이로드에는 사용자 정보를 담지 않는게 좋다고 했는데

Refresh 토큰 대신 인덱스를 반환하는 이 방식에서는 Refresh 토큰은 DB상에만 존재하게 되니까 상관없을 것 같다.

저 코드대로 해보면 되긴 된다. 근데 이제 매우 안전하지 않다는게 문제다.

구체적으론, 어떤 사용자가 요청한 것인지 확신을 못한다.

 

 

Attempt 1

  • 요청은 Access 토큰과 Refresh 토큰의 id(인덱스) 값
  • Access 토큰이 Expire만 됐고 다른 정보는 유효하다면, Refresh 토큰의 인덱스를 통해 Refresh 토큰 조회 후 Access 토큰 재발급

But,

  • 다른 사람의 만료된 Access 토큰과 아무 인덱스 숫자를 요청하고 운좋게 해당 인덱스에 누군가의 Refresh 토큰이 들어있었다면 그 사람의 권한을 가진 Access 토큰이 제 3자에게 발급돼 버린다.

 

 

 

 

 

Attempt 2

  • Refresh 토큰을 저장하는 Auth 테이블에 원래 Refresh 토큰 필드만 있었는데 Access 토큰 필드도 추가해 준다.
  • Auth 테이블에는 [id], [Access 토큰], [Refresh 토큰] 이렇게 3개의 필드가 있는 것.
  • 요청받은 Access 토큰(만료된거)과 Refresh토큰의 인덱스번호가 동시에 일치하는 경우에만 Access 토큰 속 사용자가 올바른 사용자라고 판단하여 Access 토큰을 재발급한다.
  • 즉, 입력된 인덱스(예를들어 2)에 해당하는 DB의  Access 토큰이 입력된 Access 토큰과 같은지 확인하고 같다면 Access 토큰 속 사용자가 맞다고 판단했다.

But,

  • Access 토큰의 원본과 비교한다는 점에서 Access 토큰의 조작을 막을 수 있다해도, 다른 사람이 Access 토큰을 탈취하여 무작위 인덱스 번호를 쭉 갈겨버린다면 뚫리기 마련이다.

 

 

 

뭔가 처음엔 될 것 같아서 시도해보면 터무니없는 방법임을 알게 된다. 자괴감온다. ㅠㅠ

그래도 고민을 하면서 JWT에 대한 이해는 깊어졌다.

 

 

 


5. 결론

그래서 Refresh 토큰을 클라이언트로 보내도 되는가? 안된다면 어떻게 해결하는가?

 

개인적으로는 Refresh 토큰 그대로 클라이언트로 전송해도 괜찮을 것 같다. 그리고 그게 제일 무난한 방법인 것 같다.

클라이언트 측에서 HTTPOnly 옵션과 Secure 코딩을 적용한 쿠키에 토큰을 보관하는 방법이 여러 기술 블로그들의 주류였다.

 

 

그래도 더 개선해보면

Refresh Token Rotation 방법이 있다.

Access 토큰의 짧은 수명이 다하고 Refresh 토큰을 통해 재발급할때, Refresh 토큰도 바로 재발급 해버리는 것이다.

 

그렇게 되면 Refresh 토큰의 조작여부를 쉽게 파악할 수 있고 Refresh 토큰 자체도 클라이언트에서 조금 더 안전하다.

(계속 바뀌니까)

 

 

 

 

 

 

참고:

더보기

이번에는 branch 생성/전환/삭제와 main 브랜치에서의 merge를 해보겠다.


1. 브랜치란

SW 개발 시 개발자들은 소스코드를 공유하게 된다. 이때, 여러 개발자들이 동시에 다양한 작업을 할 수 있게 만들어 주는 기능이 바로 브랜치(Branch) 다. 각자 독립적인 작업 영역(저장소) 안에서 마음대로 소스코드를 변경할 수 있다. 이렇게 분리된 작업 영역에서 변경된 내용은 나중에 원래의 버전과 비교해서 하나의 새로운 버전으로 만들어 낼 수 있다.

 

즉, 병렬적으로 작업할 수 있고 작업 후에 main으로 병합을 할 수 있는 서브 작업공간 같은 개념

 

 

 


2. Branch 국룰

변수명, 클래스명에서 암묵적인 규칙이 있듯 브랜치도 어느정도 정형화된 용도와 규칙이 있다. 처음 알았다.

 

  • Main (Main Branch)
  • Develop (Main Branch)
  • Feature/<Issue_number> or <Feature_name> / <Short Description>
  • Release/<version_number>
  • Hotfix/<Issue_number> or Issue/<Issue_number>

 

 

 

Main Branch

메인 브랜치는 배포할 수 있는 브랜치다.

즉 최종적인 상태를 의미하는 공간으로 최상위 브랜치다.

 

명명규칙: 보통 main 그대로 쓴다고 한다.

 

 

 

Develop Branch

다음 출시 버전을 개발하는 브랜치

main에서 분기되어 기능 개발을 위한 브랜치들의 병합을 위해 사용됨.

이 브랜치에서 기능을 병합하고 버그를 수정하여 배포 가능한 상태가 되면 main으로 병합한다.

 

명명규칙: 보통 develop 그대로 쓴다고 한다.

 

 

 

Feature Branch

기능을 개발하는 브랜치

새로운 기능이나 버그 수정이 필요할 때마다 develop 브랜치에서 분기된다.

여기서의 작업은 공유할 필요가 없기 때문에 주로 자신의 로컬 저장소에서 관리한다.

 

명명규칙: feature/기능요약 (ex. feature/login)

 

 

 

Release Branch

이번 출시 버전을 준비하는 브랜치

feature 브랜치에서 기능 개발 후 develop 브랜치에서 병합을 하는 과정을 반복하여,

최종적인 버그 수정이나 문서 추가 등 실질적으로 Release하기 직전에 하는 단계를 위한 브랜치

 

명명규칙: release/X.X.X 혹은 release-X.X.X

 

 

 

Hotfix Branch

출시 버전에서 발생한 버그를 수정하는 브랜치

갑작스럽게 수정해야하는 경우에 main에서 수정하지 않고 Hotfix 브랜치로 분기하고 수정 후 main으로 병합한다.

main에서 다시 배포 후에는 develop 브랜치에도 병합해준다.

 

명명규칙: hotfix-X.X.X

 

 

 


3. 모범 예시

 

 

 

 


4. 실습

1. main 브랜치에 최초 커밋을 한다.

어차피 브랜치가 중점이기에 그냥 txt파일로 연습한다.

 

 

 

2. Develop 브랜치 추가하기

현재는 main 브랜치 뿐이다.

 

 

develop 브랜치를 만들어주자.

git branch "브랜치이름"

 

 

develop 브랜치에서 로그인 기능을 만든다고 가정한 feature 브랜치도 만들어 준다.

git branch feature/login

 

 

feature/login 브랜치에 로그인 모듈을 만들어서 푸시 후 develop 브랜치로 merge 해보자

 

feature/login 브랜치로 일단 push 하고

 

develop 브랜치로 이동하여 feature/login을 merge했다.

 

두번의 과정에서 모두 첫 커밋이라 upstream을 설정하라고 에러가 발생했다.

push 뒤에 -set--upstream 옵션을 붙혀주면 된다.

 

 

새로 생성한 브랜치에서 작업이 끝나면 

git branch -d feature/login

명령어로 브랜치를 제거해준다. 이때 merge되지 않은 정보들이 있다면 경고문이 뜨며 -D로 옵션을 바꿔서 진행하라고 알려준다.

 

 

 

 


5. 결과

브랜치 3개가 잘 만들어졌다.

 

 

develop 브랜치로 merge한 feature/login 브랜치의 내용도 잘 들어갔다.

 

 

 

 

 

참고:

어제 AWS 관련 인증 키들을 깃허브 리포지토리에 올려놓고 있었다는 사실을 알게 됐다. 깃허브에서 지우고 다시 커밋되지 않도록 .gitignore에 등록하였지만 리포지토리 히스토리상에는 이미 수 없이 남아있었다. 이를 해결하기위해 BFG Repo-Cleaner를 사용했다.


1. BFG Repo-Cleaner 설치

https://rtyley.github.io/bfg-repo-cleaner/

 

BFG Repo-Cleaner by rtyley

$ bfg --strip-blobs-bigger-than 100M --replace-text banned.txt repo.git an alternative to git-filter-branch The BFG is a simpler, faster alternative to git-filter-branch for cleansing bad data out of your Git repository history: Removing Crazy Big Files Re

rtyley.github.io

다운로드된 jar파일을 작업할 프로젝트의 로컬 최상위 경로에 저장했다.

 

 

 

2. 문제의 히스토리

이렇게 이미 삭제한 내용임에도 히스토리 상에서 공개가 되기에 이 부분을 제거해 주자.

 

 

git clone --mirror [리포지토리 주소]

java -jar [jar파일 경로] --delete-files [이 클래스에 연관된 커밋 히스토리를 제거]

git reflog expire --expire=now --all && git gc --prune=now --aggressive

git push -f origin main

 

 

 

나의 경우엔 다음과 같았다.

git clone --mirror https://github.com/llshl/fabinet.git

java -jar "C:\Users\lsh97\Desktop\IntelliJ WorkSpace\fabinet-gradle\bfg-1.14.0.jar" --delete-files AwsConfig.java

git reflog expire --expire=now --all && git gc --prune=now --aggressive

git push -f origin main

파일 경로상에 공백문자가 있을 경우 전체 경로를 ""로 감싸주면 된다.

 

 

 

이 결과 AwsConfig.java 파일에 관련된 히스토리는 삭제된다.

 

(사진은 AwsConfig가 아닌 다른 클래스긴 하지만 여기에도 인증코드가 있었고 잘 지워진 모습이다.)

 

 

 

 

참고:

'Git' 카테고리의 다른 글

[Git] Branch 이름 변경하기  (0) 2022.04.26
[Git] git stash  (0) 2021.11.24
[Git] Branch와 Merge  (0) 2021.08.14
[Git] git bash를 사용한 버전관리 시작하기  (0) 2021.08.13

오늘 깃을 만지다 실수로 졸업작품 코드를 몽땅 날려버릴뻔 했다. 

reset을 통해 복구를 했긴 했다만 참으로 아찔한 상황이었다.

고로 깃을 한번 체계적으로 짚고 넘어가볼까 한다. 

이 포스팅을 시작으로 여러 상황을 재현해보며 연습해보자.


1. 리포지토리 생성과 git init

CLI로 연습하는게 좋을 것 같아서 IDE의 git 플러그인을 사용하지 않고 깃배시를 설치했다.

최초 설치 후 계정설정이 필요한데 나의 경우는 먼 옛날에 해두었기에 생략한다.

 

echo "# git-test-repo" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin [리포지토리 주소]
git push -u origin main

깃허브에서 리포지토리를 처음 생성하면 제시해주는 스크립트를 통해 Readme.md 파일을 생성하여 최초로 커밋 시켜준다.

 

 

 

 

 

이제 이 리포지토리에서 여러가지 깃 기능들을 시험해보자!

(simbean과 함께)

'Git' 카테고리의 다른 글

[Git] Branch 이름 변경하기  (0) 2022.04.26
[Git] git stash  (0) 2021.11.24
[Git] Branch와 Merge  (0) 2021.08.14
[Git] BFG Repo-Cleaner를 사용한 민감한 히스토리 삭제  (0) 2021.08.14

지난 포스팅에서 JWT가 무엇인지 알아보았다. 그럼 실제로 구현해보자.

https://llshl.tistory.com/26

 

[Web] JWT란?

1. JWT란? Json Web Token의 줄임말이다. 두 개체에서 JSON객체를 사용하여 가볍고 자가수용적인 방식으로 정보를 안정성 있게 전달해 주는 인증 방식이다. 2. 세션과의 차이점 세션 보통 로그인을 구현

llshl.tistory.com


1. JWT 테스트 시나리오

  1. 이메일과 비밀번호를 받아 회원 등록
  2. 로그인하면 JWT 토큰 반환
  3. JWT토큰을 Client 헤더에 등록하고, 요청 시 Spring Security에서 제한을 걸어놓은 리소스의 접근을 확인

 

전체 코드는 깃헙에 업로드 해놓았다.

참고로 이 코드는 

여기

블로그에서 참고했다. 설명이 아주 잘 돼 있으니 참고하면 좋다.

 

 

 

주요 코드만 살펴보자.

 

 

 


2. JWT를 사용해 로그인 기능을 구현해보자

 

회원가입 컨트롤러

    // 회원가입
    @PostMapping("/join")
    public Long join(@RequestBody Map<String, String> user) {
        return userRepository.save(User.builder()
                .email(user.get("email"))
                .password(passwordEncoder.encode(user.get("password")))
                .roles(Collections.singletonList("ROLE_ADMIN")) // 최초 가입시 USER 로 설정
                .build()).getId();
    }

그냥 별거없다. 이메일과 비밀번호를 입력받아서 Jpa의 save를 해준다. 참고로 데이터베이스는 H2를 썼다.

이렇게 포스트맨의 바디에 회원가입 정보를 담아서 요청을 보냈다.

 

 

 

 

로그인 컨트롤러

    // 로그인
    @PostMapping("/login")
    public String login(@RequestBody Map<String, String> user) {
        User member = userRepository.findByEmail(user.get("email"))
                .orElseThrow(() -> new IllegalArgumentException("가입되지 않은 E-MAIL 입니다."));
        if (!passwordEncoder.matches(user.get("password"), member.getPassword())) {
            throw new IllegalArgumentException("잘못된 비밀번호입니다.");
        }
        return jwtTokenProvider.createToken(member.getUsername(), member.getRoles());
    }

로그인 컨트롤러에서는 입력받은 이메일과 비밀번호가 데이터베이스에 있는 정보와 일치하는지 확인하고

일치한다면 JWT 토큰을 만들어서 반환해준다.

토큰에 사용자의 이메일(getUsername)과 권한 정보를 넣어줄 것이기에 파라미터로 넘겨준다.

회원가입한 정보 그대로 로그인 요청을 보낸다. 그러면 응답으로 JWT토큰이 돌아왔다!

 

 

 

 

JWT 토큰 생성

@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
    private String secretKey = "llshlllshlllshlllshl";

    // 토큰 유효시간 30분
    private long tokenValidTime = 30 * 60 * 1000L;
    private final UserDetailsService userDetailsService;

    // 객체 초기화, secretKey를 Base64로 인코딩한다.
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    // JWT 토큰 생성
    public String createToken(String userPk, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위
        claims.put("roles", roles); // 정보는 key / value 쌍으로 저장된다.
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, secretKey)  // 사용할 암호화 알고리즘과
                // signature 에 들어갈 secret값 세팅
                .compact();
    }

    // JWT 토큰에서 인증 정보 조회
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    // 토큰에서 회원 정보 추출
    public String getUserPk(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

    // Request의 Header에서 token 값을 가져옵니다. "X-AUTH-TOKEN" : "TOKEN값'
    public String resolveToken(HttpServletRequest request) {
        return request.getHeader("X-AUTH-TOKEN");
    }

    // 토큰의 유효성 + 만료일자 확인
    public boolean validateToken(String jwtToken) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }
}

 

 

이 클래스의 createToken 메소드를 통해 토큰을 생성한다.

이렇게 생성된 JWT 토큰은 이후 사용자의 요청마다 헤더에 담아져 함께 전달된다.

 

 

이 토큰을 디코딩해보면

우리가 토큰에 넣어주었던 사용자의 정보가 잘 나오는것을 확인 할 수 있다.

이건 https://jwt.io/ 여기서 할 수 있다.

 

 

 

권한이 있어야 접근 가능하도록

토큰이 권한을 가져다 주는지 확인하기 위해 Spring Security의 WebSecurityConfig 클래스를 설정해준다.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.headers().frameOptions().disable();
        http.authorizeRequests()
                .antMatchers("/h2-console/**").permitAll(); // 누구나 h2-console 접속 허용
        http
                .httpBasic().disable() // rest api 만을 고려하여 기본 설정은 해제하겠습니다.
                .csrf().disable() // csrf 보안 토큰 disable처리.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 기반 인증이므로 세션 역시 사용하지 않습니다.
                .and()
                .authorizeRequests() // 요청에 대한 사용권한 체크
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                .anyRequest().permitAll() // 그외 나머지 요청은 누구나 접근 가능
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
                        UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests(); // 권한요청 처리 설정 메서드

        // JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 전에 넣는다
    }

 

configure 메소드에서 user와 admin으로 시작하는 요청에는

각각 USER와 ADMIN의 권한이 있어야만 접근 가능하도록 제한을 걸어두었다.

 

 

 

권한은 어떻게 확인?

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 헤더에서 JWT 를 받아옵니다.
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
        // 유효한 토큰인지 확인합니다.
        if (token != null && jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효하면 토큰으로부터 유저 정보를 받아옵니다.
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            // SecurityContext 에 Authentication 객체를 저장합니다.
            SecurityContextHolder.getContext().setAuthentication(authentication);
            System.out.println("토큰 유효하다");
        }
        chain.doFilter(request, response);
    }
}

이 필터가 요청을 가로채어 JWT 토큰이 유효한지 판단한다. 토큰 검증 후 유효하다면 다시 요청을 진행시킨다.

 

 

 

확인해보자

    @PostMapping("/user/test")
    public Map userResponseTest() {
        Map<String, String> result = new HashMap<>();
        result.put("result","user ok");
        return result;
    }

    @PostMapping("/admin/test")
    public Map adminResponseTest() {
        Map<String, String> result = new HashMap<>();
        result.put("result","admin ok");
        return result;
    }

접근 제한을 걸어둔 이 두개의 리소스에 요청을 보내어 결과가 잘 받아지는지 확인해보자.

회원가입시 기본적으로 얻는 권한은 USER다.

 

 

 

 

Headers에 우리가 설정해둔 키인 X-AUTH-TOKEN에 대응되게 value로 JWT 토큰을 넣어서 요청을 보내주면 올바른 응답이 돌아옴을 확인할 수 있다. 즉 인증이 되었다는 것.

 

 

 

 

 

당연하게도 토큰을 빼고 요청을 보내면 응답을 받을 수 없다. 

 

 

 

 

또한 USER권한으로는 ADMIN에 접근이 안됨을 확인할 수 있다.

 

 

 

 

참고:

+ Recent posts