티스토리 뷰
과거 다른 블로그에서 쓴 글을 퍼왔습니다.
Introduction
내가 관리하는 API 서버는 다양한 타입의 클라이언트들로 오는 요청을 처리하고 있다. 기본적인 oauth 인증을 이용하는 웹 어플리케이션 뿐만 아니라, Windows server application, aws lambda 등 많은 어플리케이션이 HMAC 인증을 사용하고 있다. DRF (django Rest Framework)의 Permission Class 로 구현되지 않고, view 로직에 signature 검증 로직이 있는 상태다. 즉, 아무 인증 없이 django 의 Authentication 을 통과하여 view 로직 초반에서 signature를 검증한다. 나는 이 signature 검증 로직을 view 함수 밖으로 끄집어내어 permission 검증에서 해결하고 싶었다.
Signature?
aws를 조금이라도 이용해본 개발자라면, Authorization: AWS4-HMAC-SHA256 와 같은 것을 봤을거다. 실제로 aws HTTP API를 이용할 때 아래와 같은 요청을 보내게 되는데, 마치 아무런 인증처리가 없는 것처럼 보인다.
GET <https://iam.amazonaws.com/?Action=ListUsers&Version=2010-05-08> HTTP/1.1
Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
content-type: application/x-www-form-urlencoded; charset=utf-8
host: iam.amazonaws.com
x-amz-date: 20150830T123600Z
우리는 두번째 줄에 있는 Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7 를 살펴봐야한다. 저 SHA-256 해시값이 어디서 튀어나왔는지 확인해볼 필요가 있다.
AWS 공식 문서 (https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html)를 살펴보면 이런 python 예제를 확인할 수 있다. 처음에 나온 예제는 EC2 API 중 DescribeRegions 를 위해 API Request 를 만드는 과정을 설명한다.
def sign(key, msg):
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
def getSignatureKey(key, dateStamp, regionName, serviceName):
kDate = sign(('AWS4' + key).encode('utf-8'), dateStamp)
kRegion = sign(kDate, regionName)
kService = sign(kRegion, serviceName)
kSigning = sign(kService, 'aws4_request')
return kSigning
"""생략"""
# ************* TASK 3: CALCULATE THE SIGNATURE *************
# Create the signing key using the function defined above.
signing_key = getSignatureKey(secret_key, datestamp, region, service)
# Sign the string_to_sign using the signing_key
signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest()
단지 열심히 secret_key, datestamp, region, service 4개의 string으로 해싱하는 모습이다. "보내는 사람", "보내는 시간", "사용하는 서비스 리전", "서비스 이름"으로 해싱한 값으로 Authentication 처리를 한다. 그리고 300초의 datestamp 유효성 검사까지 포함된다.
정확한 이론은 https://ko.wikipedia.org/wiki/HMAC 를 참고하도록 하자.
class IsSignatureValidV2
Confluence 에 케케묵은 이 문서, 처리할 때가 됐지.
django 와 python 이 아무리 성숙한 써드파티 라이브러리 환경이 조성되어있다 하지만, 깃헙 스타 숫자가 낮거나 커밋이 활발하지 않다면 사용하기 꺼려지는게 사실이다. 그리고, 이렇게 밖에 굴러다니는 라이브러리를 쓰는데에는 엄청난 비용이 든다. signature 인증을 사용하는 API들이 꽤 많기 때문에 우리의 AS-IS 를 통째로 뜯어고쳐야 했기 때문이다. 남들이 만들어놓은 써드파티 라이브러리를 사용하는 것이 인증 체계를 직접 구현하는 것보다 훨씬 더 큰 비용이 든다.
골자는 아래와 같다. django restframework로 구현된 ViewSet에서 아래와 같이 간단하게 해결하는 것이다.
class ViewSet(viewsets.ViewSet):
queryset = Model.objects.filter(is_enabled=True)
serializer_class = Serializer
throttle_classes = (AnonRateThrottle,)
permission_classes = [IsSignatureValidV2]
단지 permission_classes 를 한 줄로 지정함으로 해결할 수 있게 만드는 것이다. (사실 저 Permission Class 이름에 V2 가 붙은건... 디자이너들의 "최종", "진짜 최종" 과 같은 거랄까.. 🧐)
이번 포스팅에서는 구현의 흐름정도만 봐주시면 좋을 것 같다.
from rest_framework.permissions import BasePermission
class IsSignatureValidV2(BasePermission):
def __init__(self) -> None:
self.data = dict()
self.signature = None
self.private_key_name = None
self.timestamp_expired_in = 30
def has_permission(self, request, view) -> bool:
self.set_data(request, view)
self.enforce_signature()
self.enforce_private_key()
self.enforce_timestamp()
self.validate_timestamp()
self.validate_signature()
return True
request, view를 통하여 signature hashing 에 필요한 데이터를 받아서, signature를 검증하고, timestamp 가 30초 이상 지나 만료되었는지 유효성 검사하는 인증과정을 거친다. 이 모든 메소드를 거쳐 예외가 발생되지 않으면 True를 반환한다. has_permission 은 View와 Serializer 검증이 들어가기 전에 호출되며 True / False 만 반환하여야 한다. False 가 반환되면 그 API는 인증이 실패하게 된다. (401 UNAUTHORIZED)
def enforce_private_key(self):
if not self.private_key_name:
return
if self.private_key_name not in self.data:
ee = APIException()
ee.status_code = status.HTTP_400_BAD_REQUEST
ee.detail = const.SIGNATURE_ERROR_400_PRIVATE_KEY_NAME
raise ee
def enforce_timestamp(self):
if self.data.get('timestamp', None) is None:
ee = APIException()
ee.status_code = status.HTTP_400_BAD_REQUEST
ee.detail = const.SIGNATURE_ERROR_400_SIGNATURE_REQUIRED
raise ee
def enforce_signature(self):
if self.data.get('signature', None) is None:
ee = APIException()
ee.status_code = status.HTTP_400_BAD_REQUEST
ee.detail = const.SIGNATURE_ERROR_400_SIGNATURE_REQUIRED
raise ee
private key와 timestamp, signature 가 요청에 있는지 검증하는 메소드다. 단순하다. set_data() 를 통해 request, view에서 받아온 데이터에 필요한 것들이 있는지 최소한으로 검사한다.
def validate_signature(self):
if self.data['signature'] != self.calculate_signature():
ee = APIException()
ee.status_code = status.HTTP_400_BAD_REQUEST
ee.detail = const.SIGNATURE_ERROR_400_INVALID_SIGNATURE
raise ee
단지 self.data로 calculate_signature()를 통해 계산된 signature 값을 request 속의 signature와 같은지 비교한다.
'Python' 카테고리의 다른 글
파이썬의 모든 변수는 객체(Object) : C와의 차이점은? (0) | 2019.07.25 |
---|