* 해당 포스트는 <파이썬 알고리즘 인터뷰> 공부 후 정리 목적으로 작성되었습니다. *

 

leetcode 5번 ( https://leetcode.com/problems/longest-palindromic-substring/ )

 

풀이방법 1.

투 포인터를 사용하면 된다. 보통 투 포인터는 입력의 양 끝에서 중앙으로 모이는 형태를 생각하지만, 반대로 특정 기준점에서 양쪽으로 퍼져 나가는 형태도 가능하다. 

 

left, right 포인터를 어느 지점을 기준으로 양쪽으로 퍼져 나갈지를 정하는 시작점이라고 보자. 

 

left 포인터는 문자열의 0번째 인덱스부터 len(string)-2번째 인덱스까지를 시작점으로 둘 수 있다. 반면 right 포인터는 문자열의 1번째 인덱스부터 len(string)-1번째 인덱스까지를 시작점으로 둘 수 있다. 

 

left, right 포인터를 각각 시작점으로 정했다면 해당 시작점을 기준으로 left 포인터는 왼쪽으로 한 칸씩, right 포인터는 오른쪽으로 한 칸씩 이동하며 확장한다. left와 right 포인터의 값이 같다면 계속 확장하고, 그렇지 않다면 지금까지의 left와 right 포인터 사이에 위치한 문자열을 반환한다. 

 

문자열을 확장하며 팰린드롬을 찾아서 리턴하는 로직과, 가장 큰 팰린드롬을 찾는 로직을 구분하면 코드가 더 간결할 것이다. 따라서 앞의 로직은 함수 내부의 함수로 따로 선언해 주자.

교재에서는 expand(left, right)라는 내부 함수로 선언했다. 

 

이때 문자열 슬라이싱 인덱스가 [a:b]일 경우, 실제로 반환되는 문자열은 a번째 인덱스부터 b-1번째 인덱스이므로 주의해야 한다. 

또한, left와 right 포인터의 현재 위치는 포함하지 않고, 그 사이에 있는 문자열만 리턴해야 하므로, expand() 함수는 다음과 같이 리턴한다. 

return string[left+1, right]

 

또한 팰린드롬의 길이는 홀수일 수도, 짝수일 수도 있다. 홀수일 경우 가운데 문자 하나로부터 양쪽으로 확장해야 하고, 짝수일 경우 나란히 있는 문자 두 개를 기준으로 양쪽으로 확장해야 한다. 

따라서 홀수 팰린드롬을 찾으려면 left와 right 포인터가 1칸 차이여야 하고, 짝수 팰린드롬을 찾으려면 두 포인터가 2칸 차이여야 하겠다. 

expand(i, i+1)	# odd
expand(i, i+2)	# even

 

문제에서는 가장 긴 팰린드롬을 반환하라고 했으므로, 가장 긴 팰린드롬을 저장할 변수도 필요하다. 

여러 개의 변수를 파라미터로 받을 수 있는 max() 함수를 사용하되 기준을 정하지 않으면 가장 긴 팰린드롬이 아니라 알파벳 순서 상 가장 먼저인 팰린드롬이 리턴될 것이다. 

따라서 max() 함수에도 key 파라미터 값으로 len 함수를 할당해서, 알파벳 순서가 아니라 팰린드롬의 길이를 기준으로 하도록 바꿔 준다. 

result = max(result, expand(i, i+1), expand(i, i+2), key=len)
return result

 

* 해당 포스트는 <파이썬 알고리즘 인터뷰> 공부 후 정리 목적으로 작성되었습니다. *

 

leetcode 49번 ( https://leetcode.com/problems/group-anagrams/ )

 

풀이방법 1. 

주어진 입력을 그대로 사용하지 않고 변형한 뒤 사용하면 쉽게 풀 수 있는 문제였다. 

특히 리스트가 주어졌지만 리스트의 인덱스를 리턴하는 문제가 아니라면 입력으로 주어진 리스트의 순서를 바꿔도 된다는 생각을 해 보면 좋겠다. 

 

문자열 A와 B가 서로 애너그램일 경우, 문자열 A의 모든 문자를 단 한 번씩 사용해서 문자열 B를 나타낼 수 있으며, 그 반대도 성립한다. 

서로 애너그램인 문자열들은 여러 개가 있을 수 있으며, 그 문자열들을 하나의 리스트로 묶기 위해서는 그 문자열들을 다른 문자열들과 구분할 수 있어야 한다. 

 

특정 애너그램을 구성하는 문자열들은 모두 같은 문자열로 구성되어 있다. 해당 문자열을 알파벳 순서로 정렬하면, 각 애너그램에 따른 unique한 값을 얻을 수 있다. 

즉 속한 애너그램이 다르면 문자열의 값이 다르게 나오기 때문에 서로 애너그램인지 아닌지를 구분할 수 있는 것이다. 

 

이 원리를 이용하고, dictionary 자료형을 사용하면 각 애너그램들을 묶어서 저장할 수 있다. 

key 값으로는 문자열을 정렬한 값이 할당되고, value 값으로는 해당 key 값으로 나타낼 수 있는 문자열 리스트가 할당된다. 

 

 

다만 문자열을 정렬하기 위해서 필요한 sort()와 sorted() 중, sorted()는 iterable 자료형은 모두 매개변수로 받을 수 있기 때문에 문자열도 매개변수로 받을 수 있다.

(각 인덱스 값을 돌아가면서 리턴하는 것이 가능하므로, 문자열도 iterable이다)

그러므로 sorted()를 사용해서 문자열을 리스트로 변환한 뒤, 다시 join을 사용해서 리스트를 문자열로 변환한다. 

 

다만 join은 A와 B를 합치는 함수이므로, 대상이 될 A 문자열이 필요하다. 

공백 문자열에다가 join을 사용하면 해당 리스트만 문자열로 반환할 수 있다. 

key = "".join(sorted(string))
dicts[key].append(string)

 

이후 values() 메소드를 사용해서 dictionary 안에 속한 모든 값 리스트를 리턴하면 된다. 

 

* 해당 포스트는 <파이썬 알고리즘 인터뷰> 공부 후 정리 목적으로 작성되었습니다. *

 

leetcode 819번 ( https://leetcode.com/problems/most-common-word/ )

 

풀이방법 1. 

리스트 컴프리헨션(list comprehension)과 Counter 자료구조를 사용한다. 

 

list comprehension

list comprehension이란 기존에 있는 리스트의 값을 사용해서 새로운 리스트를 만드는 것이다. 

우선 주어진 문자열을 대소문자로 변환하고 특수문자를 제거한 뒤, 문자열 리스트로 만드는 작업이 필요하다. 

그 이후에는 각 문자열이 banned_list에 있는 문자열이 아닌지를 확인해서 리스트에 추가 및 정렬하는 작업이 필요하다. 

 

list comprehension을 통해서 간단한 코드로 주어진 문자열을 조건에 맞는 문자열 리스트로 만들 수 있다. 

lists = [elem for elem in re.sub(r"[\W]", " ", string).lower().split() if elem not in banned_list]

 

해당 코드에서는 string의 문자 중 영문자가 아닌 문자들은 전부 공백으로 바꾸고, 모든 문자열을 소문자로 바꾼다. 이후 공백을 기준으로 나눠서 단어 리스트를 리턴한 뒤, 리스트 안의 각 단어가 banned_list에 없다면 lists에 추가하는 형태로 새 리스트를 만든다. 

 

lower() : 기본으로 제공되는 메소드로, 문자열의 모든 대문자를 소문자로 바꾼다. 

split(sep) : 문자열을 sep(문자)를 기준으로 나눠서 리스트 형태로 리턴한다. sep가 없을 경우 공백으로 나눈다. 

 

re.sub()에서 d, w, W 등 특정 문자는 \와 결합될 경우 단순 알파벳이 아니라 포괄적인 의미로 사용된다. 

\d : 모든 숫자

\w : 모든 영문자

\W : 영문자가 아닌 모든 문자(\w의 반대)

 

counter

Counter 객체는 dictionary의 하위 클래스이며 collections 라이브러리를 통해 사용할 수 있다.

dictionary 클래스를 상속받아서 대부분의 기능은 유사하지만, 몇 가지의 차이점이 있다. 

words = ["hi", "hello", "heee"]
counter = collections.Counter(words)

 

Counter 객체를 초기화할 때 매개변수로 리스트를 받을 수 있다. 그러면 리스트 내부의 각 단어가 key, 해당 단어의 등장 빈도 수가 value로 저장된다. 즉 단어 리스트를 Counter의 매개변수로 넣은 뒤 특정 단어의 value 값을 조회하면 해당 단어가 몇 번 등장했는지를 알 수 있다. 

 

또한 기존의 dictionary는 찾는 단어(key)가 없을 경우 ValueError을 발생시켰지만 collections.defaultdict()은 None을, collections.Counter()은 0을 리턴한다. 

 

또한 Counter의 most_common(n) 메소드를 사용하면 빈도수가 가장 높은 단어를 최대 n쌍 반환한다. 리스트 형태로 반환하며, 각 원소는 tuple 형태이다.

이를 이용해서 가장 빈도수가 높은 단어 하나를 리턴할 수 있다. 

return counter.most_common(1)[0][0]

 

 

참고한 포스트

https://docs.python.org/3/library/collections.html#collections.Counter

https://www.w3schools.com/python/python_lists_comprehension.asp

 

 

 

* 해당 포스트는 <파이썬 알고리즘 인터뷰> 공부 후 정리 목적으로 작성되었습니다. *

 

leetcode 937번 ( https://leetcode.com/problems/reorder-data-in-log-files/ )

 

풀이방법 1. 

파이썬에서 기본으로 제공하는 정렬함수 sort를 이용하고, key 파라미터를 이용해서 정렬 기준을 함수나 람다표현식(lambda expression)으로 제공한다. 

 

sort() 함수는 리스트 자료형에만 사용할 수 있다. 만약 문자열을 sort() 함수로 정렬하고 싶다면 문자열을 리스트로 바꾼 뒤, 해당 리스트를 정렬하고, 다시 join() 등의 함수를 사용해서 리스트를 문자열로 바꾸는 작업이 필요하다.

또한 sort() 함수는 리스트 내부를 정렬한 뒤 값을 리턴하지 않는다. 

 

반면 sorted() 함수는 리스트를 포함한 iterable(for loop를 사용해서 자신이 가진 원소들을 한 번씩 반환할 수 있는 자료구조)에 사용할 수 있다. 여기에는 set, dictionary 등도 포함된다. 

또한 sorted() 함수는 주어진 iterable 내부를 정렬하지 않고, 새로 iterable을 만든 뒤 정렬해서 값을 리턴한다. 

 

sort(key=None, reverse=False)

sort(), sorted() 함수는 key와 reverse 파라미터 값을 선택적으로 받을 수 있다. 

reverse=True이면 값을 내림차순으로 정렬하고, reverse=False면 값을 오름차순으로 정렬한다. 기본값은 reverse=False이다. 

key 파라미터의 값으로는 람다식이나 함수가 올 수 있다. 람다식도 결국은 함수의 일종이니, 함수만 올 수 있는 셈이다. 

 

람다식은 이름 없는 익명 함수와 같으며, lambda 키워드를 사용해서 선언할 수 있다. 

lambda 변수이름 : 함수 식

lambda x : x+10			# 1 variable
lambda [x, y] : x+y		# multiple variables

 

로그 파일 정렬도 로그 파일 문자열을 단순 abc 정렬한 순서가 아니라, 두 번째 단어 이후의 문자 순서대로 정렬하고, 모든 문자열이 같을 경우만 첫 번째 단어를 고려하여 결정한다. 따라서 이 경우 sort() 함수에서 key 파라미터 값에다가 (두 번째 단어 이후의 문자열 + 첫 번째 단어의 문자열)을 리턴하는 함수를 할당하면, 해당 기준에 맞게 정렬할 수 있다. 

letters.sort(key=lambda x:(x.split[1:], x.split[0]))

 

참고한 포스트

https://docs.python.org/3/howto/sorting.html

https://docs.python.org/3/reference/expressions.html

https://www.pythonlikeyoumeanit.com/Module2_EssentialsOfPython/Iterables.html -> iterable의 정의 참고

 

* 해당 포스트는 <파이썬 알고리즘 인터뷰> 공부 후 정리 목적으로 작성되었습니다. *

 

leetcode 344번 ( https://leetcode.com/problems/reverse-string/ )

 

풀이방법 1. 

투 포인터를 사용한다. left 포인터는 문자열의 맨 처음에, right 포인터는 문자열의 맨 끝에 두고 다중 할당 방식을 이용해서 바꾼다. 

 

파이썬에서 두 변수의 값을 바꿀 경우, 다른 언어들과는 달리 다중 할당을 이용해야 한다. 그렇지 않으면 예상하던 결과가 나오지 않을 수 있다. 파이썬에서는 다른 언어들과 달리 한 변수가 다른 변수에 할당될 때 값을 복사(call by value)하지 않고 값을 참조(call by reference)하기 때문이다. 

 

값을 복사하는 다른 언어의 경우, 임시로 값을 저장할 다른 변수를 만들어서 swap 한다. 

int a = 1; int b = 2;

// swap
int temp = a;
a = b;
b = temp;
System.out.println(a);	# 2
System.out.println(b);	# 1

 

반면 값을 참조하는 파이썬의 경우, 다중 할당을 이용한다. 

a = 1
b = 2
# swap
a, b = b, a
print(a, b)	# 2, 1

 

풀이방법 2. 

파이썬에서 기본으로 제공하는 함수를 사용한다. 

string.reverse()

text = "hello"
print(text.reverse())	# olleh

 

문자열이므로 기본 함수를 사용하지 않고 문자열 슬라이싱으로도 풀 수 있다. 

text = "hello"
print(text[::-1])	# olleh

 

* 해당 포스트는 <파이썬 알고리즘 인터뷰> 공부 후 정리 목적으로 작성되었습니다. *

 

- leetcode 125번 ( https://leetcode.com/problems/valid-palindrome/ )

 

풀이방법 1. 

문제에서는 팰린드롬은 영문자와 숫자만을 대상으로 한다고 했으므로, 조건에 맞는 문자들만 리스트에 넣는다. 

이후 리스트의 맨 앞과 맨 뒤의 원소를 각각 제거한 다음, 두 원소의 값이 같은지를 비교한다. 

값이 하나라도 다르면 앞뒤가 똑같지 않은 것이므로 False를, 전부 같으면 True를 리턴한다. 

 

파이썬에서 기본으로 제공되는 isalnum() 메소드를 사용하면 특정 문자가 alphanumeric(영문자와 숫자)인지 아닌지를 간단하게 구분할 수 있다. 

파이썬에서 제공하는 기본 메소드들은 내부적으로 속도가 훨씬 빠른 C언어를 통해 구현되어 있다. 따라서 직접 파이썬의 for loop로도 코드를 만들 수 있겠지만 기본 메소드를 사용하는 것이 더 속도가 빠르다. 

 

풀이방법 2. 

더 효과적인 데크(deque) 자료형을 사용한다. 

데크와 리스트의 차이점은 맨 앞의 원소를 제거하는 popleft(), pop(0) 메소드에 걸리는 시간이다. 

데크는 투 포인터라서 pop()와 popleft()이 모두 O(1)인 반면, 리스트는 pop()에는 O(1)이 소요되지만 pop(0)에는 리스트 전체 중 0번째 인덱스를 직접 찾는 셈이 되므로 O(n)이 소요된다. 

 

데크 자료형은 외부 라이브러리가 아닌 collections 라이브러리를 사용하며, 간단하게 선언할 수 있다.

deque = collections.deque()

 

collections 라이브러리에는 deque 외에도 편리한 기능이 갖춰진 기본 자료구조를 갖고 있기 때문에 공식문서를 참고해서 알아두면 좋겠다. 

https://docs.python.org/3/library/collections.html

 

풀이방법 3. 

정규식으로 조건에 맞는 문자들만 필터링한 다음, 문자열 슬라이싱으로 비교하는 방법도 있다. 

특히 문자열의 경우 슬라이싱(slicing)을 사용할 수 있는지 고려해보자. 생각보다 다양한 슬라이싱 방법들이 많다. 

 

정규식으로 문자열을 필터링하고 싶은 경우 re 라이브러리를 사용한다. 

https://docs.python.org/3/library/re.html

 

re.sub(정규식 패턴, 대체할 문자열, 원본 문자열)

re 라이브러리의 sub 메소드는 원본 문자열 중 패턴에 일치하는 문자열을 대체할 문자열로 바꾸는 역할을 한다. 

result = re.sub(r"[a-zA-Z]", "eee", "abc123def")
print(result)	# 'eee123eee'

 

문자열 슬라이싱

가장 기본적인 방법으로는 문자열의 특정 왼쪽 인덱스부터 특정 오른쪽 인덱스까지를 가져올 수 있다. 

string[left: right] 은 string 변수의 left 번째 인덱스부터 (right-1)번쨰 인덱스까지를 가져온다. 

string = "little red riding hood"
print(string[7:10])	# red

 

또는 인덱스에 음수를 사용하면 오른쪽 인덱스부터 왼쪽 인덱스까지, 역수 방향으로 문자열을 가져올 수 있다. 

text = "olleh"
print(text[-2:-4])	# el

 

문자열 전체를 뒤집거나, n칸씩 뛰어넘어서 문자열을 가져오는 것도 가능하다. 

text = "hello"
print(text[::-1])	# olleh

 

또는 문자열 슬라이싱으로 기존 문자열을 복사할 수도 있다. 이는 기존의 문자열의 값을 가져오고 싶으나 참조하게 되는 문제로 원본 값을 가져오지 못할 때, 참조가 아니라 값을 복사하기 위해서 사용할 수 있다. 

text = "text"
print(text[:])	# text

 

 

참고한 포스트

https://ponyozzang.tistory.com/335

 

해당 게시물은 유튜브 생활코딩 채널의 WEB2-OAuth 강의를 듣고 작성한 포스트입니다.

 

WEB2 - OAuth 2.0 : 1.수업소개 - YouTube

 

강의 목표

OAuth 개념 이해하기

 

 

1. OAuth의 개념


OAuth를 사용하는 대표적인 예시로는 소셜로그인이 있다. 하지만 소셜로그인만이 전부가 아니다.

OAuth는 다른 서비스(보통은 신뢰할 수 있는 서비스. ex) google, facebook)와 원래 서비스를 연동한다. 
그러려면 사용자가 사용하는 해당 서비스 계정에 접근할 수 있도록 허가를 받아야 한다. 

가장 쉬운 방법은 사용자의 개인정보(아이디, 비밀번호) 등을 전달받아서 이를 SNS 계정에 접근할 때 이용하는 것이다.
그러나 사용자 입장에서는 자신의 개인정보를 처음 보는 서비스에게 맡기는 것은 불안하고, 보안상 문제가 있을 수 있다. 

OAuth는 이런 방법 말고, 토큰(Token)을 사용해서 안전하게 서로 다른 두 서비스가 상호작용할 수 있도록 해 준다. 

OAuth의 장점

1. 사용자의 실제 개인정보를 본래 사이트에서 사용하지 않는다. 즉 보안 면에서의 장점이 있다. 
2. 액세스 토큰으로 이용할 수 있는 SNS 서비스를 제한할 수 있다.

바로 위의 언급한 방법처럼 사용자의 실제 개인정보를 통으로 넘겨주게 되면, 해당 사이트에서 사용자 계정의 모든 권한을 갖는다는 점에서 보안 문제가 있다. 반면 액세스 토큰으로는 할 수 있는 일과 없는 일이 제한되어 있다. 마찬가지로 보안에서의 장점이 있다.

 

OAuth를 사용하는 방법

사용자의 개인정보를 사용해서, 사용하려는 다른 서비스의 사이트에서 로그인한다. 

사용자가 로그인하면 사용자의 실제 개인정보 대신 액세스 토큰을 발급하고, 본래 사이트에서 그 토큰으로 다른 서비스와 상호작용할 수 있다. 

 

2. 역할

 

Oauth에 등장하는 3개의 주체

resource owner : 사용자 
resource server : 기존 사이트에서 제어하고 싶은 자원을 갖고 있는 서버
client : 리소스 서버의 자원을 이용하려는 사이트

OAuth 공식 문서에서는 리소스 서버(resource server)를 resource serverauthentication server로 분리한다. 
resource server : 인증에 필요한 데이터를 갖고 있는 서버
authentication server : 인증 처리 및 작업을 하는 서버


*간단하게 보면 둘을 묶어서 그냥 리소스 서버로 보기도 한다. 


3. 등록


리소스 서버에 클라이언트를 등록하는 절차

클라이언트가 리소스 서버의 리소스를 사용하려면 사전에 미리 등록(register)을 해야 한다.

(Create app 이라는 과정으로도 나온다.)

등록에 필요한 정보

client id : 리소스 서버에서 개별 클라이언트에게 부여하는 id. 노출이 되어도 상관없다. 
client secret : 클라이언트가 리소스 서버에 자신을 인증할 때 사용하는 비밀번호. 노출되어서는 안 된다. 
authorized redirect urls : 리소스 서버는 이 url로 클라이언트에게 authorization code를 보낸다. 
만약 이 url이 아닌 다른 url에서 리다이렉트(redirect)요청이 들어온다면, 리소스 서버는 해당 요청에 대해서 응답을 보내지 않는다. 

실제로 등록하는 방법

사용하려는 리소스 서버 서비스의 developers 사이트에 가서, create app 또는 비슷한 메뉴를 찾아보자. 

 

ex)
Facebook : developers facebook 사이트에서 create app 메뉴 선택
Google : cloud platform 사이트에서 select/create project 메뉴 선택


4. Resource Owner의 승인


리소스 서버에 클라이언트가 사전 등록 작업을 마쳤다고 해 보자. 

클라이언트 서비스에서 리소스 오너의 정보로 리소스 서버의 서비스나 리소스를 이용하려면 추가적인 작업이 필요하다. 

우선 리소스 오너의 승인이 필요하다. 

ex. 리소스 유저가, 클라이언트 사이트가 나의 정보를 가지고 리소스 서버 사이트의 특정 기능을 사용하는 것을 승인

 

그 다음엔 리소스 서버의 승인도 필요할 것이다. 

ex. 리소스 서버 사이트에서 액세스 토큰과 같이 보낸 요청을 승인

과정

우선 등록이 완료된 이후, 클라이언트와 리소스 서버가 oauth에 필요한 어떤 정보를 갖고 있는지를 보자. 

클라이언트가 리소스 서버에 등록했을 때 사용한 client id, client secret, redirect URL의 정보를 둘 다 갖고 있다. 

 

만약 리소스 오너가 클라이언트 사이트를 이용하면서 소셜로그인 등의 '리소스 서버의 리소스를 필요로 하는 서비스'를 사용하려고 한다면, 흔한 소셜로그인 버튼 등이 나타날 것이다. 

리소스 오너가 그 버튼을 누르면(http 요청을 하면), 클라이언트 사이트는 응답과 함께 리다이렉트 URL을 리턴한다. 

리다이렉트 URL 예시

https://resource.server/?client_id=1&scope=B,C&redirect_url=https://client/callback



리다이렉트 URL은 리소스 서버와 클라이언트가 공통으로 가진 3개의 정보(client_id, client_secret, scope)중에서 client_secret을 제외한 2개의 정보를 쿼리 스트링으로 포함하고 있다. 

해당 url을 받은 리소스 오너는 받은 리다이렉트 url으로 리소스 서버에게 GET 요청을 보낸다.

(POST 처럼 따로 데이터를 담아서 보내지는 않는다.)

 

만약 해당 리소스 오너가 이미 리소스 서버에 로그인이 되어 있지 않은 경우(관련 토큰이 헤더에 없는 경우), 리소스 서버는 리소스 유저에게 리소스서버 로그인을 요청한다. 

참고로 로그인을 하지 않은 상태에서는 리소스 서버는 url 파라미터인 client_id, redirect_uri, scope는 아직 보지 않는다. 

여기서 리소스 오너가 로그인을 하면 다음 단계로 넘어간다. 

또는 이미 로그인이 되어 있었다면(관련 토큰을 헤더에 같이 넣어서 보냈다면), 리소스 서버는 이때 client_id와 redirect_uri를 확인한다. 

(만약 리소스 서버가 갖고 있는 client_id, redirect_uri 파라미터가 맞지 않는다면, 리소스 서버는 여기서 응답을 종료한다.)

만약 클라이언트가 보낸 값이 리소스 서버의 값과 일치한다면, 리소스 서버는 리소스 유저에게 클라이언트 사이트에서 scope에 해당하는 권한을 클라이언트에게 부여해도 되는지를 물어본다. 

(선택하는 작은 폼이 뜰 것이다.)

 

클라이언트가 해당 폼에서 allow를 누르면, 리소스 서버에는 리소스 유저의 id해당 리소스 유저가 허용한 scope 변수의 값이 저장된다. 

 

저장하는 이유는, 앞으로 해당 리소스 유저의 액세스 토큰으로 리소스 서버의 서비스나 리소스를 이용할 때, 해당 유저가 어떤 scope의 권한을 허용했는지를 알 수 있게 정보를 저장하는 것이다. 


5. Resource Server의 승인

 

리소스 유저가 scope에 대한 권한을 클라이언트에게 부여하는 걸 허용했다고 해도, 리소스 서버가 바로 액세스 토큰을 부여하지는 않는다. 

절차가 하나 더 있다.

리소스 유저가 scope에 대한 권한을 승인하고, 리소스 서버가 user_id와 scope를 저장한 이후, 리소스 서버는 리소스 유저에게 authorization_code를 리턴한다. 

authorization_code : 리소스 서버가 클라이언트를 인증하는 임시 비밀번호 역할을 한다. 

authorization_code는 아까 리소스 유저가 리소스 서버에게 보냈던 redirect_uri 파라미터 주소의 뒤에 쿼리 스트링을 추가한 형식으로 붙어서 보내진다. 

https://client/callback/?code=3

 

이 정보를 리소스 유저에게 응답으로 보낼 때, 리소스 서버는 이 정보를 헤더의 Location 파라미터에 넣어서 보낸다. 

그러면 리소스 유저의 브라우저는 location 파라미터에 있는 주소로 리다이렉트를 하게 된다. 

그러면 리소스 유저는 클라이언트에게 해당 url로 GET 요청을 보내는 셈이다. 

(redirect_uri의 도메인이 클라이언트의 도메인이기 때문이다)

그러면 클라이언트는 이제 authorization_code의 값도 알게 된다. 

이제 클라이언트는 리소스 유저를 통하지 않고, 리소스 서버에게 직접 요청을 보낼 수 있다. 

쿼리 스트링 파라미터로 url에 값을 넣은 형식이고, GET 방식으로 요청을 보낸다. 

 

url 예시

https://resource.server/token?grant_type=authorization_code&code=3&redirect_url=https://client/callback&client_id=1&client_secret=2

 

해당 url에는 grant_type, code, redirect_url, client_id, client_secret 정보가 포함되어 있다. 

이 중에는 2개의 비밀번호(auth_code, client_secret) 정보도 포함되어 있다.

+

강의에서 다루진 않았지만, 리소스 서버가 클라이언트를 인증하는 방법은 authorization code로 인증하는 방법 말고도 여러 개가 있다고 한다. 그래서 어떤 인증방법을 사용하는지에 대한 정보를 알려주기 위해서 

grant_type=authorization_code

라는 쿼리스트링이 붙는다. 

 


개인적인 Q

더보기

client_secret은 노출되면 안 되는 정보라고 했는데 저렇게 url 쿼리스트링으로 보내도 괜찮은 걸까?

 

리소스 정보는 자신의 DB에서 client_secret, auth_code 정보가 클라이언트가 보낸 정보와 일치하는지를 보고(ex. client_secret=2인 계정의 auth_code는 3이 맞는지), client_id, redirect_url 등의 나머지 정보가 맞는지도 확인한다. 

이제 다음 단계에서 액세스 토큰을 지급한다.


6. 액세스 토큰 발급

 

이제는 리소스 서버가 클라이언트에게 직접 액세스 토큰을 발급한다. 

해당 액세스 토큰은 리소스 서버와 클라이언트의 DB에 각각 저장되며, 각 리소스 유저마다 당연히 액세스 토큰의 값이 다르다. 

처음에 리소스 유저가 한번 '클라이언트에서 자신의 리소스 서버 계정에서 특정 기능들(scope)의 접근 권한을 얻는 것'에 동의하면, 그 이후로는 별도의 동의를 받지 않고 액세스 토큰을 사용할 수 있다. 

순서

1. 리소스 유저가 클라이언트 사이트에서 '소셜로그인' 등의 버튼을 클릭한다(클라이언트에게 요청을 보낸다).

2. 클라이언트는 리소스 유저에게 리다이렉트 응답을 보낸다(리소스 서버 로그인으로 리다이렉트).

3. 정보를 맞게 입력한 경우 기존에는 authorization_code를 보내서 추가 인증을 진행하였지만, 이제는 그러지 않는다. 

셀프 Q&A

더보기

Q. 리소스 유저가 리소스 서버의 로그인 정보를 맞게 입력한 경우 리소스 서버는 리소스 유저에게 어떤 응답을 리턴할까?

A(추측). 클라이언트로 리다이렉트 or (액세스 토큰 유효기간이 지난경우) 새 액세스 토큰 발급할 것 같다. 

 

 

7. API 호출

 

oauth를 이용하는 가장 중요한 목적

액세스 토큰 발급 이후 리소스 서버의 일부 기능을 사용하려면 리소스 서버의 API를 사용해야 한다. 

리소스 서버의 API리소스 서버에서 제공하는 여러 기능을 어떻게 사용할 것인지에 대한 표준 규격이라고 할 수 있다. 

각 리소스 서버는 API들을 어떤 형식으로, 어떤 파라미터 등을 넣어서 사용해야 하는지에 대한 정보를 공식문서로 제공하고 있다. 

ex. "구글 캘린더 API" 를 검색해서 나온 공식문서를 참고해서, 해당 형식으로 요청 보내면 데이터를 받을 수 있다. 

액세스 토큰 보내는 방법(2가지)

1. GET 요청으로 url 쿼리스트링에 access_token={access_token} 값 넣어서 보내기

상대적으로 간편하지만 보안 측면 때문에 덜 사용된다고 한다. 

 

2. GET 요청으로 보내되, 액세스 토큰은 헤더에 Authorization 값으로 넣고, Bearer 토큰 방식으로 보내기

보안 측면에서 더 좋다. 다만 일반적인 url 접근이 불가능하고, curl이나 postman같은 프로그램을 사용해야 한다. 


8. 리프레쉬 토큰 Refresh Token

 

액세스 토큰은 유효기간이 있다. 

리프레쉬 토큰은 처음 액세스 토큰이 발급될 때 액세스 토큰과 같이 발급되는데, 액세스 토큰이 만료되었을 때 새 액세스 토큰을 발급하는 데 사용된다. 

액세스 토큰을 발급받을 때는 보통 액세스 토큰, 리프레쉬 토큰, 만료기간의 값을 같이 리턴하는 것이 일반적이다. 

리프레쉬 토큰이 어떻게 발급되는지도 마찬가지로 사용하려는 각 리소스 서버의 공식문서를 참조하면 알 수 있다. 

ex. 구글의 경우 Google Identity Platform에서 관련 정보를 제공한다. 보통은 특정 url로 필요한 정보(client_id, grant_type, refresh_token 등)를 POST 방식으로 보내면 새 액세스 토큰을 발급하는 방식을 많이 쓴다. 

또한 리프레쉬 토큰의 경우, 새 액세스 토큰의 발급에 사용되면:

(1) 리프레쉬 토큰 값도 새로 발급해 주는 경우도 있고, 

(2) 리프레쉬 토큰 값은 그대로인 경우도 있다. 


9. 수업을 마치며

 

앞으로 공부해 볼 만한 주제

federated identity : 다른 서비스와의 연합을 통해 사용자를 식별하는 인증체계

RESTful API : 많은 형식의 API는 이 방식을 따르고 있다. 

 

'server-side > server' 카테고리의 다른 글

Mac 환경설정  (0) 2024.07.15
Software Release Life Cycle  (0) 2023.07.15
인증(Authentication)  (0) 2022.07.14
linux: cron 사용해서 자동으로 스케줄 실행하기  (0) 2022.07.09
Git: clone, single-branch, checkout  (0) 2022.06.28

1. 이메일로 인증하기

 

보통 회원가입을 위해 이메일을 입력하면 이메일 인증 절차를 거친다. 해당 이메일이 실제 사용자가 사용하는 이메일인지를 인증하기 위해서, 이메일로 링크를 보내고 사용자가 그 링크를 열면 인증이 완료되는 방식이다. 

 

이메일로 보내지는 링크에는 여러 정보가 포함되어 있다. 회원가입으로 생성된 유저의 pk(primary key)를 인코딩한 값이나 고유한 토큰을 쿼리 스트링(query string)으로 받기도 한다. 

 

이번에 구현하는 기능에서는 사용자(유저)의 필드에 계정의 활성화 여부를 나타내는 필드를 사용했다. 

 

즉 이메일 인증이 없다면 입력한 정보 그대로 사용자 계정이 만들어지고 로그인이 된다. 반면 인증이 있다면 사용자 계정은 만들어지지만, 사용자의 활성화 필드가 False로 되어 있어서 계정은 있지만 로그인은 할 수 없다. 로그인하려면 이메일로 전송된 링크를 클릭해야 한다. 

 

따라서 이런 식의 이메일 인증을 구현해 둘 생각이라면, 처음부터 사용자(User) 모델에 계정 활성화 필드(is_active)를 만들고 처음부터 이를 적극 활용하자. 

 

 

2. 이메일로 폼 만들기

 

그러나 이런 링크 하나만 달랑 보내지는 않는다. 요즘 웹사이트들은 html 템플릿에 적절한 텍스트와 링크를 함께 실어서 보내준다. 

 

그러면 그냥 html 파일을 작성하고 이 파일에 적당한 매개변수 넣어서 이메일로 보내면 되는 거 아니야? 라고 할 수 있지만 그렇지 않다. 왜냐하면 html은 단순 텍스트(plain text)가 아니라 일종의 multiform, multipart data처럼 복잡한 데이터로 보기 때문이다. 그러니까 html을 일반 텍스트처럼 매개변수를 넣어서 보낸다면 html 템플릿이 코드로 깨져서 전송된다.

 

요즘은 이메일을 보내는 전용 라이브러리도 많고, html 퍼블리싱이 번거로울 때 사용할 수 있는 템플릿 사이트도 많다고 한다. 

 

내가 했던 삽질(...)은 다음과 같다. 

 

1. html 템플릿에서 장고의 autoescape 템플릿 문법 사용

2. EmailMessage 클래스

3. EmailMultipart 클래스

4. MIMEApplication, MIMEText 등 MIME 클래스 -> 성공

 

사실 EmailMessage나 EmailMultipart 클래스를 통해서도 이메일을 보낼 수 있었을지도 모른다(내가 방법을 몰랐을 뿐). 다행히도 MIME 클래스 타입의 인스턴스를 만들고, 여러 타입의 컨텐츠를 attach() 메소드를 통해서 붙일 수 있었다. 그래서 html 폼 뿐만 아니라 이미지, pdf 등의 파일을 한번에 붙여서 이메일로 보내는 것도 가능했다. 

 

 

3. 이메일 컨텐츠 구성하기

 

2번이 끝나면 다 되었겠거니 생각했다. 하지만 3번에서도 문제는 발생했다. 

 

1) 이메일 템플릿에서는 css 파일이나 style 태그를 통해서 스타일을 적용할 수 없다. html 안 개별 코드의 style 속성을 이용해야 스타일이 폼에 적용된다. 왜냐하면 이메일을 보내기 위해 폼을 인식시킬 때 style 태그를 인식하지 않기 때문이다. 

2) 메일을 확인하는 도메인에 따라서 보여지는 결과가 다를 수 있다. 같은 폼을 보냈는데 Gmail에서는 잘 나왔지만, 네이버 메일에서는 어떤 버튼의 밑부분이 약간 짤려서 나왔다. 

3) body 태그에서 스타일 속성을 적용하면 잘 적용되지 않는다. body 태그 내부에 table 태그를 한번 더 선언해 주자. 

 

 

4. 아쉬웠던 점

1) Gmail에서는 잘 나오던 html 버튼이 네이버 메일에서는 밑부분이 약간 짤려서 나왔었다. 그러나 배포까지 해당 오류를 해결할 시간이 빠듯해서 그냥 버튼을 링크로만 바꿨었다. 

2) EmailMessage, EmailMultipart 클래스 등을 제대로 알아보지 않고 주먹구구식으로 오류를 해결했었다. 오류 원인을 파악하고 잘 접근하기 위해선 관련 클래스의 역할, 메소드 등을 간략하게나마 파악하는 것이 참 중요한 것 같다..!

 

'개발 일기장 > 개발 일지' 카테고리의 다른 글

0706 WED 업무 일지  (0) 2022.07.07

+ Recent posts