티스토리에서는 블로그 활동을 조금 더 편하게 하도록 도움을 줄 수 있는 API들을 제공하고 있다.
나는 평소에 글 작성 시 주로 마크다운으로 작성하는데, 미리보기가 불편한 점과 저장 문제 등 불편한 점이 좀 있어서 개선하고싶었다.
이걸 해결해보기 위해 파이썬으로 간단한 마크다운 업로드 프로그램을 만들어보고자 한다.
티스토리 OPEN API 등록
우선, 티스토리 오픈 API를 사용하기 위해서는 앱 등록을 해주어야한다.
등록 링크에 접속한 뒤, 앱 등록을 해준다.
Tistory
좀 아는 블로거들의 유용한 이야기
www.tistory.com

서비스 명과 설명은 아무거나 써도 상관 없고, 서비스 URL은 자신의 블로그 주소, callback은 localhost:8000을 써준다. 그 이유는 아래 후술하겠지만, callback을 통해 티스토리에서 인증 코드를 발급받을 수 있는데, 이 때 우리는 로컬 서버를 통해 코드를 발급받기 위함이다.

등록하면 다음과 같은 값들을 얻을 수 있다. 이 값들은 API를 사용하기 위한 필수 값들이다.
tkinter
파이썬 프로그램을 간단하게 만들어보기 위해 tkinter 라이브러리를 사용해보았다. tkinter는 GUI를 구현하기 위한 기본 패키지로, 거의 대부분 파이썬이 설치되어있다면 tkinter도 함께 설치되어있을것이다.
tkinter의 사용법은 이 페이지를 보면 될 것이다.
Python tkinter 강좌 : 제 1강 - GUI 생성
tkinter
076923.github.io
프로그램 클래스 생성
import tkinter as tk
from tkinter import filedialog
import tkinter.font
import http.server
import socketserver
import threading
from urllib.parse import urlparse, parse_qs
import tkinter.messagebox as messagebox
import requests
import webbrowser
class TistoryUploaderApp:
def __init__(self, root):
# 동작 추가
if __name__ == "__main__":
root = tk.Tk()
TistoryUploaderApp(root)
root.mainloop()
생성자 추가
if __name__ == "__main__":
root = tk.Tk()
TistoryUploaderApp(root)
root.mainloop()
먼저 이 부분이다. python3 main.py
처럼 프로그램을 실행하게된다면, 이 부분의 로직을 수행하겠다는 의미이다.
tk는 tkinter로, 가장 상위 레벨의 윈도우 창을 생성하고, TistoryUploaderApp이라는 클래스를 생성한 뒤, 윈도우 창을 종료될 때 까지 실행시킨다는 의미이다.
def __init__(self, root):
self.root = root
self.root.title("Tistory Markdown Uploader")
self.root.geometry("800x200")
self.background_color = "#FEE500"
self.root.configure(bg=self.background_color)
self.root.columnconfigure(0, weight=1)
self.root.columnconfigure(3, weight=1)
self.font = tkinter.font.Font(family="NanumGothic", size=12)
self.app_id_label = tk.Label(
root, text="App ID", bg=self.background_color, font=self.font
)
self.app_id_entry = tk.Entry(root)
self.client_secret_label = tk.Label(
root, text="Secret Key", bg=self.background_color, font=self.font
)
self.client_secret_entry = tk.Entry(root)
self.authorize_button = tk.Button(
root, text="Authorize", command=self.get_authorize_code
)
self.unauthorized_button = tk.Button(
root, text="Unauthorize", command=self.unauthorize
)
self.authorize_success_label = tk.Label(
root, text="인증 성공", bg=self.background_color, font=self.font
)
self.layout_widgets()
self.server_thread = threading.Thread(target=self.start_server, daemon=True)
self.server_thread.start()
def layout_widgets(self):
self.app_id_label.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
self.app_id_entry.grid(row=0, column=2, padx=10, pady=10, sticky="ew")
self.client_secret_label.grid(row=1, column=1, padx=10, pady=10, sticky="ew")
self.client_secret_entry.grid(row=1, column=2, padx=10, pady=10, sticky="ew")
self.authorize_button.grid(row=1, column=3, padx=10, pady=10, sticky="ew")
def start_server(self):
handler = lambda *args: self.OAuthHandler(self, *args)
PORT = 8000
with socketserver.TCPServer(("", PORT), handler) as httpd:
print(f"Server started at http://localhost:{PORT}")
httpd.serve_forever()
TistoryUploaderApp 클래스를 생성하면 실행되는 생성자이다. 여기서는 윈도우의 기본 설정, 로컬 서버 설정 등 초기 설정들이 포함된다.
프로그램을 실행시키면 윈도우 세팅이 완료되고, 로컬 서버까지 열리도록 세팅해주었다.


디자인이 영 별로긴 한데.. 어쨌든 의도대로 동작하는걸 확인해볼 수 있을것이다.
서버 세팅 중, handler라는 것이 포함된다. 이것을 통해 티스토리 서버에서 callback 해주는 응답을 수신하도록 설정할 수 있다.
OAuth handler 구현
class OAuthHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, app, *args, **kwargs):
self.app = app
super().__init__(*args, **kwargs)
def do_GET(self):
url = urlparse(self.path)
query_params = parse_qs(url.query)
# 에러 메시지 처리
error = query_params.get("error")
error_description = query_params.get("error_description", [""])[0]
if error:
self.send_response(401)
self.end_headers()
self.wfile.write(f"Error: {error[0]} - {error_description}".encode())
return
# 인증 코드 처리
auth_code = query_params.get("code", [None])[0]
if auth_code:
print("Received OAuth code:", auth_code)
# 인증 코드 처리 로직
self.send_response(200)
self.end_headers()
self.wfile.write(
"Authentication successful. You can close this window.".encode()
)
self.app.authorize_success()
return
# 기본 응답
self.send_response(404)
self.end_headers()
self.wfile.write("Page not found.".encode())
이 클래스는 TistoryUploadApp 클래스 내부에 위치하며, http.server.SimpleHTTPRequestHandler를 상속받아 구현했다. OAuth 인증 과정에서 발생하는 HTTP 요청을 처리한다.
__init__ 메서드: 이 메서드는 OAuthHandler 인스턴스를 초기화한다. 여기서 app 매개변수는 TistoryUploaderApp 인스턴스를 참조하며, 이를 통해 Tkinter GUI와 상호작용이 가능해진다.
do_GET 메서드: 웹 브라우저나 다른 HTTP 클라이언트로부터 GET 요청을 받을 때 호출된다. 이 메서드는 다음과 같은 두 가지 주요 기능을 수행한다:
- 에러 처리: 인증 과정에서 오류가 발생한 경우, 적절한 HTTP 응답(401)과 함께 오류 메시지를 클라이언트에 전송한다.
- 인증 코드 처리: OAuth 인증 과정에서 인증 코드(code)를 성공적으로 받은 경우, authorize_success 메서드를 호출하여 인증 성공을 처리한다.
- 기본 응답: 해당 URL 경로에서 처리할 수 없는 요청에 대해 404 오류 응답을 반환한다.
이 클래스를 통해 OAuth 인증 과정에서 발생하는 HTTP 요청을 적절하게 처리해줄 수 있고, 인증 코드를 발급받거나 오류를 식별할 수 있다.
티스토리 인증 코드 발급
인증 코드를 발급받기 위해서는 아래 URL을 통해 자신의 정보를 전송해주어야 한다.
https://www.tistory.com/oauth/authorize?
client_id={client-id}
&redirect_uri={redirect-uri}
&response_type=code
&state={state-param}
매개변수는 다음과 같다.
- client_id: 클라이언트 정보의 Client ID. 이전에 발급받은 App ID가 이에 해당한다.
- redirect_uri: 사용자가 인증 후에 리디렉션할 URI. 클라이언트 정보의 Callback 경로로 등록하여야 하며 등록되지 않은 URI를 사용하는 경우 인증이 거부된다. 이전에 작성한 callback을 작성해주면 된다. 우리의 경우 `http://localhost:8000`이 될 것이다.
- response_type: 항상 code를 사용한다.
- state: 사이트간 요청 위조 공격을 보호하기 위한 임의의 고유한 문자열이며 리디렉션시 해당 값이 전달된다. (필수아님)
인증코드를 요청하는 코드를 작성했다.
def get_authorize_code(self):
app_id = self.app_id_entry.get().strip()
client_secret = self.client_secret_entry.get().strip()
if not app_id or not client_secret:
messagebox.showwarning("경고", "App ID와 Secret Key를 모두 입력해주세요.")
return
oauth_url = "https://www.tistory.com/oauth/authorize"
auth_param = {
"client_id": self.app_id_entry.get().strip(),
"redirect_uri": "http://localhost:8000",
"response_type": "code",
}
with requests.session() as s:
res = s.get(oauth_url, params=auth_param, allow_redirects=False)
webbrowser.open(res.url)
URL에 맞게 코드를 작성해주었다. 이 메서드에서는 입력창이 비어있는지 확인하는 로직과 요청하는 부분으로 나뉘어있다.
마음같아서는 티스토리, 카카오 로그인까지 한번에 다 붙이고 싶었는데, 보안상의 문제인지 무조건 "https://www.tistory.com/oauth/authorize" 이 URL로 인증 요청을 보낸 뒤, 티스토리 서버에서 직접 로그인 과정을 거쳐야했다.
이런 과정때문에, 요청을 보내면 티스토리 서버에서는 리다이렉션 url을 응답하게 되는데, 이 url을 브라우저에서 열어주면 다음과 같은 화면이 뜬다.

아직 client_secret은 사용하지 않기 때문에 아무거나 넣어줘도 되고, 이전에 발급받은 App ID를 입력해준뒤 버튼을 눌러보자.

로그인 창이 뜬다면, 로그인부터 해주면 된다.
허가하기를 눌러주면 터미널에서 인증 코드가 발급된 것을 확인할 수 있다.

Access Token 발급
인증 코드는 access token을 발급받기 위해 필요한 준비물이라고 생각하면 된다.
GET https://www.tistory.com/oauth/access_token?
client_id={client-id}
&client_secret={client-secret}
&redirect_uri={redirect-uri}
&code={code}
&grant_type=authorization_code
이전과 별로 다를건 없지만, 이전에 발급받은 인증 코드와 client-secret이 추가적으로 필요하다.
# TistoryUploaderApp 내부 메서드로 추가
def get_access_token(self, code):
access_url = "https://www.tistory.com/oauth/access_token"
access_param = {
"client_id": self.app_id_entry.get().strip(),
"client_secret": self.client_secret_entry.get().strip(),
"redirect_uri": "http://localhost:8000",
"code": code,
"grant_type": "authorization_code",
}
with requests.session() as s:
res = s.get(access_url, params=access_param)
if res.status_code == 200:
self.access_token = res.text.split("=")[1]
print("Access Token:", self.access_token)
self.authorize_success()
return True
return False
def authorize_success(self):
self.app_id_label.grid_remove()
self.app_id_entry.grid_remove()
self.client_secret_label.grid_remove()
self.client_secret_entry.grid_remove()
self.authorize_button.grid_remove()
self.authorize_success_label.grid(
row=0, column=1, padx=10, pady=10, sticky="ew"
)
self.unauthorized_button.grid(row=0, column=2, padx=10, pady=10, sticky="ew")
# OAuthHandler 클래스 변경사항
# 인증 코드 처리
auth_code = query_params.get("code", [None])[0]
if auth_code:
print("Received OAuth code:", auth_code)
# 인증 코드 처리 로직
if self.app.get_access_token(auth_code):
self.send_response(200)
self.end_headers()
self.wfile.write(
"Authentication successful. You can close this window.".encode()
)
else:
self.send_response(401)
self.end_headers()
self.wfile.write("Authentication failed.".encode())
return
OAuthHandler를 통해 auth_code를 성공적으로 발급받았다면, get_access_token 메서드를 호출해 토큰 발급을 받아주는 코드를 작성했다.
성공적으로 토큰을 발급받았다면 인증 성공을 브라우저에 띄워주고 프로그램의 버튼 레이어를 변경해준다.

이번엔 client_secret도 제대로 입력해준다.

토큰 발급 성공
전체 코드
import tkinter as tk
from tkinter import filedialog
import tkinter.font
import http.server
import socketserver
import threading
from urllib.parse import urlparse, parse_qs
import tkinter.messagebox as messagebox
import requests
import webbrowser
class TistoryUploaderApp:
def __init__(self, root):
self.root = root
self.root.title("Tistory Markdown Uploader")
self.root.geometry("800x200")
self.background_color = "#FEE500"
self.root.configure(bg=self.background_color)
self.root.columnconfigure(0, weight=1)
self.root.columnconfigure(3, weight=1)
self.font = tkinter.font.Font(family="NanumGothic", size=12)
self.access_token = None
self.app_id_label = tk.Label(
root, text="App ID", bg=self.background_color, font=self.font
)
self.app_id_entry = tk.Entry(root)
self.client_secret_label = tk.Label(
root, text="Secret Key", bg=self.background_color, font=self.font
)
self.client_secret_entry = tk.Entry(root)
self.authorize_button = tk.Button(
root, text="Authorize", command=self.get_authorize_code
)
self.unauthorized_button = tk.Button(
root, text="Unauthorize", command=self.unauthorize
)
self.authorize_success_label = tk.Label(
root, text="인증 성공", bg=self.background_color, font=self.font
)
self.layout_widgets()
self.server_thread = threading.Thread(target=self.start_server, daemon=True)
self.server_thread.start()
def layout_widgets(self):
self.app_id_label.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
self.app_id_entry.grid(row=0, column=2, padx=10, pady=10, sticky="ew")
self.client_secret_label.grid(row=1, column=1, padx=10, pady=10, sticky="ew")
self.client_secret_entry.grid(row=1, column=2, padx=10, pady=10, sticky="ew")
self.authorize_button.grid(row=1, column=3, padx=10, pady=10, sticky="ew")
def start_server(self):
handler = lambda *args: self.OAuthHandler(self, *args)
PORT = 8000
with socketserver.TCPServer(("", PORT), handler) as httpd:
print(f"Server started at http://localhost:{PORT}")
httpd.serve_forever()
class OAuthHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, app, *args, **kwargs):
self.app = app
super().__init__(*args, **kwargs)
def do_GET(self):
url = urlparse(self.path)
query_params = parse_qs(url.query)
# 에러 메시지 처리
error = query_params.get("error")
error_description = query_params.get("error_description", [""])[0]
if error:
self.send_response(401)
self.end_headers()
self.wfile.write(f"Error: {error[0]} - {error_description}".encode())
return
# 인증 코드 처리
auth_code = query_params.get("code", [None])[0]
if auth_code:
# 인증 코드 처리 로직
if self.app.get_access_token(auth_code):
self.send_response(200)
self.end_headers()
self.wfile.write(
"Authentication successful. You can close this window.".encode()
)
else:
self.send_response(401)
self.end_headers()
self.wfile.write("Authentication failed.".encode())
return
# 기본 응답
self.send_response(404)
self.end_headers()
self.wfile.write("Page not found.".encode())
def authorize_success(self):
self.app_id_label.grid_remove()
self.app_id_entry.grid_remove()
self.client_secret_label.grid_remove()
self.client_secret_entry.grid_remove()
self.authorize_button.grid_remove()
self.authorize_success_label.grid(
row=0, column=1, padx=10, pady=10, sticky="ew"
)
self.unauthorized_button.grid(row=0, column=2, padx=10, pady=10, sticky="ew")
def unauthorize(self):
self.app_id_label.grid()
self.app_id_entry.grid()
self.client_secret_label.grid()
self.client_secret_entry.grid()
self.authorize_button.grid()
self.authorize_success_label.grid_remove()
self.unauthorized_button.grid_remove()
def get_authorize_code(self):
app_id = self.app_id_entry.get().strip()
client_secret = self.client_secret_entry.get().strip()
if not app_id or not client_secret:
messagebox.showwarning("경고", "App ID와 Secret Key를 모두 입력해주세요.")
return
oauth_url = "https://www.tistory.com/oauth/authorize"
auth_param = {
"client_id": self.app_id_entry.get().strip(),
"redirect_uri": "http://localhost:8000",
"response_type": "code",
}
with requests.session() as s:
res = s.get(oauth_url, params=auth_param, allow_redirects=False)
webbrowser.open(res.url)
def get_access_token(self, code):
access_url = "https://www.tistory.com/oauth/access_token"
access_param = {
"client_id": self.app_id_entry.get().strip(),
"client_secret": self.client_secret_entry.get().strip(),
"redirect_uri": "http://localhost:8000",
"code": code,
"grant_type": "authorization_code",
}
with requests.session() as s:
res = s.get(access_url, params=access_param)
if res.status_code == 200:
self.access_token = res.text.split("=")[1]
self.authorize_success()
return True
return False
if __name__ == "__main__":
root = tk.Tk()
TistoryUploaderApp(root)
root.mainloop()
발급받은 토큰을 통해 여러 티스토리 API를 사용하는것은 다음에 올려보도록 하겠다.
'Blog' 카테고리의 다른 글
[Python] 티스토리 API 100% 활용해보기 - 完 (1) | 2023.12.29 |
---|---|
티스토리로 옮기며... (0) | 2023.02.19 |
깃허브에 개인 블로그 배포하기 (0) | 2023.02.19 |
next.js로 개인 블로그 만들기 (0) | 2023.02.19 |
티스토리에서는 블로그 활동을 조금 더 편하게 하도록 도움을 줄 수 있는 API들을 제공하고 있다.
나는 평소에 글 작성 시 주로 마크다운으로 작성하는데, 미리보기가 불편한 점과 저장 문제 등 불편한 점이 좀 있어서 개선하고싶었다.
이걸 해결해보기 위해 파이썬으로 간단한 마크다운 업로드 프로그램을 만들어보고자 한다.
티스토리 OPEN API 등록
우선, 티스토리 오픈 API를 사용하기 위해서는 앱 등록을 해주어야한다.
등록 링크에 접속한 뒤, 앱 등록을 해준다.
Tistory
좀 아는 블로거들의 유용한 이야기
www.tistory.com

서비스 명과 설명은 아무거나 써도 상관 없고, 서비스 URL은 자신의 블로그 주소, callback은 localhost:8000을 써준다. 그 이유는 아래 후술하겠지만, callback을 통해 티스토리에서 인증 코드를 발급받을 수 있는데, 이 때 우리는 로컬 서버를 통해 코드를 발급받기 위함이다.

등록하면 다음과 같은 값들을 얻을 수 있다. 이 값들은 API를 사용하기 위한 필수 값들이다.
tkinter
파이썬 프로그램을 간단하게 만들어보기 위해 tkinter 라이브러리를 사용해보았다. tkinter는 GUI를 구현하기 위한 기본 패키지로, 거의 대부분 파이썬이 설치되어있다면 tkinter도 함께 설치되어있을것이다.
tkinter의 사용법은 이 페이지를 보면 될 것이다.
Python tkinter 강좌 : 제 1강 - GUI 생성
tkinter
076923.github.io
프로그램 클래스 생성
import tkinter as tk
from tkinter import filedialog
import tkinter.font
import http.server
import socketserver
import threading
from urllib.parse import urlparse, parse_qs
import tkinter.messagebox as messagebox
import requests
import webbrowser
class TistoryUploaderApp:
def __init__(self, root):
# 동작 추가
if __name__ == "__main__":
root = tk.Tk()
TistoryUploaderApp(root)
root.mainloop()
생성자 추가
if __name__ == "__main__":
root = tk.Tk()
TistoryUploaderApp(root)
root.mainloop()
먼저 이 부분이다. python3 main.py
처럼 프로그램을 실행하게된다면, 이 부분의 로직을 수행하겠다는 의미이다.
tk는 tkinter로, 가장 상위 레벨의 윈도우 창을 생성하고, TistoryUploaderApp이라는 클래스를 생성한 뒤, 윈도우 창을 종료될 때 까지 실행시킨다는 의미이다.
def __init__(self, root):
self.root = root
self.root.title("Tistory Markdown Uploader")
self.root.geometry("800x200")
self.background_color = "#FEE500"
self.root.configure(bg=self.background_color)
self.root.columnconfigure(0, weight=1)
self.root.columnconfigure(3, weight=1)
self.font = tkinter.font.Font(family="NanumGothic", size=12)
self.app_id_label = tk.Label(
root, text="App ID", bg=self.background_color, font=self.font
)
self.app_id_entry = tk.Entry(root)
self.client_secret_label = tk.Label(
root, text="Secret Key", bg=self.background_color, font=self.font
)
self.client_secret_entry = tk.Entry(root)
self.authorize_button = tk.Button(
root, text="Authorize", command=self.get_authorize_code
)
self.unauthorized_button = tk.Button(
root, text="Unauthorize", command=self.unauthorize
)
self.authorize_success_label = tk.Label(
root, text="인증 성공", bg=self.background_color, font=self.font
)
self.layout_widgets()
self.server_thread = threading.Thread(target=self.start_server, daemon=True)
self.server_thread.start()
def layout_widgets(self):
self.app_id_label.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
self.app_id_entry.grid(row=0, column=2, padx=10, pady=10, sticky="ew")
self.client_secret_label.grid(row=1, column=1, padx=10, pady=10, sticky="ew")
self.client_secret_entry.grid(row=1, column=2, padx=10, pady=10, sticky="ew")
self.authorize_button.grid(row=1, column=3, padx=10, pady=10, sticky="ew")
def start_server(self):
handler = lambda *args: self.OAuthHandler(self, *args)
PORT = 8000
with socketserver.TCPServer(("", PORT), handler) as httpd:
print(f"Server started at http://localhost:{PORT}")
httpd.serve_forever()
TistoryUploaderApp 클래스를 생성하면 실행되는 생성자이다. 여기서는 윈도우의 기본 설정, 로컬 서버 설정 등 초기 설정들이 포함된다.
프로그램을 실행시키면 윈도우 세팅이 완료되고, 로컬 서버까지 열리도록 세팅해주었다.


디자인이 영 별로긴 한데.. 어쨌든 의도대로 동작하는걸 확인해볼 수 있을것이다.
서버 세팅 중, handler라는 것이 포함된다. 이것을 통해 티스토리 서버에서 callback 해주는 응답을 수신하도록 설정할 수 있다.
OAuth handler 구현
class OAuthHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, app, *args, **kwargs):
self.app = app
super().__init__(*args, **kwargs)
def do_GET(self):
url = urlparse(self.path)
query_params = parse_qs(url.query)
# 에러 메시지 처리
error = query_params.get("error")
error_description = query_params.get("error_description", [""])[0]
if error:
self.send_response(401)
self.end_headers()
self.wfile.write(f"Error: {error[0]} - {error_description}".encode())
return
# 인증 코드 처리
auth_code = query_params.get("code", [None])[0]
if auth_code:
print("Received OAuth code:", auth_code)
# 인증 코드 처리 로직
self.send_response(200)
self.end_headers()
self.wfile.write(
"Authentication successful. You can close this window.".encode()
)
self.app.authorize_success()
return
# 기본 응답
self.send_response(404)
self.end_headers()
self.wfile.write("Page not found.".encode())
이 클래스는 TistoryUploadApp 클래스 내부에 위치하며, http.server.SimpleHTTPRequestHandler를 상속받아 구현했다. OAuth 인증 과정에서 발생하는 HTTP 요청을 처리한다.
__init__ 메서드: 이 메서드는 OAuthHandler 인스턴스를 초기화한다. 여기서 app 매개변수는 TistoryUploaderApp 인스턴스를 참조하며, 이를 통해 Tkinter GUI와 상호작용이 가능해진다.
do_GET 메서드: 웹 브라우저나 다른 HTTP 클라이언트로부터 GET 요청을 받을 때 호출된다. 이 메서드는 다음과 같은 두 가지 주요 기능을 수행한다:
- 에러 처리: 인증 과정에서 오류가 발생한 경우, 적절한 HTTP 응답(401)과 함께 오류 메시지를 클라이언트에 전송한다.
- 인증 코드 처리: OAuth 인증 과정에서 인증 코드(code)를 성공적으로 받은 경우, authorize_success 메서드를 호출하여 인증 성공을 처리한다.
- 기본 응답: 해당 URL 경로에서 처리할 수 없는 요청에 대해 404 오류 응답을 반환한다.
이 클래스를 통해 OAuth 인증 과정에서 발생하는 HTTP 요청을 적절하게 처리해줄 수 있고, 인증 코드를 발급받거나 오류를 식별할 수 있다.
티스토리 인증 코드 발급
인증 코드를 발급받기 위해서는 아래 URL을 통해 자신의 정보를 전송해주어야 한다.
https://www.tistory.com/oauth/authorize?
client_id={client-id}
&redirect_uri={redirect-uri}
&response_type=code
&state={state-param}
매개변수는 다음과 같다.
- client_id: 클라이언트 정보의 Client ID. 이전에 발급받은 App ID가 이에 해당한다.
- redirect_uri: 사용자가 인증 후에 리디렉션할 URI. 클라이언트 정보의 Callback 경로로 등록하여야 하며 등록되지 않은 URI를 사용하는 경우 인증이 거부된다. 이전에 작성한 callback을 작성해주면 된다. 우리의 경우 `http://localhost:8000`이 될 것이다.
- response_type: 항상 code를 사용한다.
- state: 사이트간 요청 위조 공격을 보호하기 위한 임의의 고유한 문자열이며 리디렉션시 해당 값이 전달된다. (필수아님)
인증코드를 요청하는 코드를 작성했다.
def get_authorize_code(self):
app_id = self.app_id_entry.get().strip()
client_secret = self.client_secret_entry.get().strip()
if not app_id or not client_secret:
messagebox.showwarning("경고", "App ID와 Secret Key를 모두 입력해주세요.")
return
oauth_url = "https://www.tistory.com/oauth/authorize"
auth_param = {
"client_id": self.app_id_entry.get().strip(),
"redirect_uri": "http://localhost:8000",
"response_type": "code",
}
with requests.session() as s:
res = s.get(oauth_url, params=auth_param, allow_redirects=False)
webbrowser.open(res.url)
URL에 맞게 코드를 작성해주었다. 이 메서드에서는 입력창이 비어있는지 확인하는 로직과 요청하는 부분으로 나뉘어있다.
마음같아서는 티스토리, 카카오 로그인까지 한번에 다 붙이고 싶었는데, 보안상의 문제인지 무조건 "https://www.tistory.com/oauth/authorize" 이 URL로 인증 요청을 보낸 뒤, 티스토리 서버에서 직접 로그인 과정을 거쳐야했다.
이런 과정때문에, 요청을 보내면 티스토리 서버에서는 리다이렉션 url을 응답하게 되는데, 이 url을 브라우저에서 열어주면 다음과 같은 화면이 뜬다.

아직 client_secret은 사용하지 않기 때문에 아무거나 넣어줘도 되고, 이전에 발급받은 App ID를 입력해준뒤 버튼을 눌러보자.

로그인 창이 뜬다면, 로그인부터 해주면 된다.
허가하기를 눌러주면 터미널에서 인증 코드가 발급된 것을 확인할 수 있다.

Access Token 발급
인증 코드는 access token을 발급받기 위해 필요한 준비물이라고 생각하면 된다.
GET https://www.tistory.com/oauth/access_token?
client_id={client-id}
&client_secret={client-secret}
&redirect_uri={redirect-uri}
&code={code}
&grant_type=authorization_code
이전과 별로 다를건 없지만, 이전에 발급받은 인증 코드와 client-secret이 추가적으로 필요하다.
# TistoryUploaderApp 내부 메서드로 추가
def get_access_token(self, code):
access_url = "https://www.tistory.com/oauth/access_token"
access_param = {
"client_id": self.app_id_entry.get().strip(),
"client_secret": self.client_secret_entry.get().strip(),
"redirect_uri": "http://localhost:8000",
"code": code,
"grant_type": "authorization_code",
}
with requests.session() as s:
res = s.get(access_url, params=access_param)
if res.status_code == 200:
self.access_token = res.text.split("=")[1]
print("Access Token:", self.access_token)
self.authorize_success()
return True
return False
def authorize_success(self):
self.app_id_label.grid_remove()
self.app_id_entry.grid_remove()
self.client_secret_label.grid_remove()
self.client_secret_entry.grid_remove()
self.authorize_button.grid_remove()
self.authorize_success_label.grid(
row=0, column=1, padx=10, pady=10, sticky="ew"
)
self.unauthorized_button.grid(row=0, column=2, padx=10, pady=10, sticky="ew")
# OAuthHandler 클래스 변경사항
# 인증 코드 처리
auth_code = query_params.get("code", [None])[0]
if auth_code:
print("Received OAuth code:", auth_code)
# 인증 코드 처리 로직
if self.app.get_access_token(auth_code):
self.send_response(200)
self.end_headers()
self.wfile.write(
"Authentication successful. You can close this window.".encode()
)
else:
self.send_response(401)
self.end_headers()
self.wfile.write("Authentication failed.".encode())
return
OAuthHandler를 통해 auth_code를 성공적으로 발급받았다면, get_access_token 메서드를 호출해 토큰 발급을 받아주는 코드를 작성했다.
성공적으로 토큰을 발급받았다면 인증 성공을 브라우저에 띄워주고 프로그램의 버튼 레이어를 변경해준다.

이번엔 client_secret도 제대로 입력해준다.

토큰 발급 성공
전체 코드
import tkinter as tk
from tkinter import filedialog
import tkinter.font
import http.server
import socketserver
import threading
from urllib.parse import urlparse, parse_qs
import tkinter.messagebox as messagebox
import requests
import webbrowser
class TistoryUploaderApp:
def __init__(self, root):
self.root = root
self.root.title("Tistory Markdown Uploader")
self.root.geometry("800x200")
self.background_color = "#FEE500"
self.root.configure(bg=self.background_color)
self.root.columnconfigure(0, weight=1)
self.root.columnconfigure(3, weight=1)
self.font = tkinter.font.Font(family="NanumGothic", size=12)
self.access_token = None
self.app_id_label = tk.Label(
root, text="App ID", bg=self.background_color, font=self.font
)
self.app_id_entry = tk.Entry(root)
self.client_secret_label = tk.Label(
root, text="Secret Key", bg=self.background_color, font=self.font
)
self.client_secret_entry = tk.Entry(root)
self.authorize_button = tk.Button(
root, text="Authorize", command=self.get_authorize_code
)
self.unauthorized_button = tk.Button(
root, text="Unauthorize", command=self.unauthorize
)
self.authorize_success_label = tk.Label(
root, text="인증 성공", bg=self.background_color, font=self.font
)
self.layout_widgets()
self.server_thread = threading.Thread(target=self.start_server, daemon=True)
self.server_thread.start()
def layout_widgets(self):
self.app_id_label.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
self.app_id_entry.grid(row=0, column=2, padx=10, pady=10, sticky="ew")
self.client_secret_label.grid(row=1, column=1, padx=10, pady=10, sticky="ew")
self.client_secret_entry.grid(row=1, column=2, padx=10, pady=10, sticky="ew")
self.authorize_button.grid(row=1, column=3, padx=10, pady=10, sticky="ew")
def start_server(self):
handler = lambda *args: self.OAuthHandler(self, *args)
PORT = 8000
with socketserver.TCPServer(("", PORT), handler) as httpd:
print(f"Server started at http://localhost:{PORT}")
httpd.serve_forever()
class OAuthHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, app, *args, **kwargs):
self.app = app
super().__init__(*args, **kwargs)
def do_GET(self):
url = urlparse(self.path)
query_params = parse_qs(url.query)
# 에러 메시지 처리
error = query_params.get("error")
error_description = query_params.get("error_description", [""])[0]
if error:
self.send_response(401)
self.end_headers()
self.wfile.write(f"Error: {error[0]} - {error_description}".encode())
return
# 인증 코드 처리
auth_code = query_params.get("code", [None])[0]
if auth_code:
# 인증 코드 처리 로직
if self.app.get_access_token(auth_code):
self.send_response(200)
self.end_headers()
self.wfile.write(
"Authentication successful. You can close this window.".encode()
)
else:
self.send_response(401)
self.end_headers()
self.wfile.write("Authentication failed.".encode())
return
# 기본 응답
self.send_response(404)
self.end_headers()
self.wfile.write("Page not found.".encode())
def authorize_success(self):
self.app_id_label.grid_remove()
self.app_id_entry.grid_remove()
self.client_secret_label.grid_remove()
self.client_secret_entry.grid_remove()
self.authorize_button.grid_remove()
self.authorize_success_label.grid(
row=0, column=1, padx=10, pady=10, sticky="ew"
)
self.unauthorized_button.grid(row=0, column=2, padx=10, pady=10, sticky="ew")
def unauthorize(self):
self.app_id_label.grid()
self.app_id_entry.grid()
self.client_secret_label.grid()
self.client_secret_entry.grid()
self.authorize_button.grid()
self.authorize_success_label.grid_remove()
self.unauthorized_button.grid_remove()
def get_authorize_code(self):
app_id = self.app_id_entry.get().strip()
client_secret = self.client_secret_entry.get().strip()
if not app_id or not client_secret:
messagebox.showwarning("경고", "App ID와 Secret Key를 모두 입력해주세요.")
return
oauth_url = "https://www.tistory.com/oauth/authorize"
auth_param = {
"client_id": self.app_id_entry.get().strip(),
"redirect_uri": "http://localhost:8000",
"response_type": "code",
}
with requests.session() as s:
res = s.get(oauth_url, params=auth_param, allow_redirects=False)
webbrowser.open(res.url)
def get_access_token(self, code):
access_url = "https://www.tistory.com/oauth/access_token"
access_param = {
"client_id": self.app_id_entry.get().strip(),
"client_secret": self.client_secret_entry.get().strip(),
"redirect_uri": "http://localhost:8000",
"code": code,
"grant_type": "authorization_code",
}
with requests.session() as s:
res = s.get(access_url, params=access_param)
if res.status_code == 200:
self.access_token = res.text.split("=")[1]
self.authorize_success()
return True
return False
if __name__ == "__main__":
root = tk.Tk()
TistoryUploaderApp(root)
root.mainloop()
발급받은 토큰을 통해 여러 티스토리 API를 사용하는것은 다음에 올려보도록 하겠다.
'Blog' 카테고리의 다른 글
[Python] 티스토리 API 100% 활용해보기 - 完 (1) | 2023.12.29 |
---|---|
티스토리로 옮기며... (0) | 2023.02.19 |
깃허브에 개인 블로그 배포하기 (0) | 2023.02.19 |
next.js로 개인 블로그 만들기 (0) | 2023.02.19 |