Skip to content

Commit 1432a8e

Browse files
committed
[fix] gnuboard#616 REST API > 글쓰기 시간 간격 검증 추가
slowapi 라이브러리 추가 - request 요청 IP 주소를 통해 글쓰기 시간 간격 검증 및 제한 - dependency 설치: pip install -r requirements.txt
1 parent 87e4ac8 commit 1432a8e

File tree

4 files changed

+110
-3
lines changed

4 files changed

+110
-3
lines changed

api/v1/routers/board.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
from core.database import db_session
99
from lib.common import get_paging_info
10-
from lib.board_lib import insert_board_new, set_write_delay, get_list_thumbnail
10+
from lib.board_lib import insert_board_new, get_list_thumbnail
11+
from lib.slowapi.limiter import validate_slowapi_create_post
1112
from api.v1.models.response import (
1213
response_401, response_403, response_404, response_422
1314
)
@@ -195,13 +196,13 @@ async def api_create_post(
195196
service.validate_post_content(wr_data.wr_content)
196197
service.validate_write_level()
197198
service.arrange_data(wr_data, wr_data.secret, wr_data.html, wr_data.mail)
199+
validate_slowapi_create_post(service.request)
198200
write = service.save_write(wr_data.parent_id, wr_data)
199201
insert_board_new(service.bo_table, write)
200202
service.add_point(write)
201203
parent_write = service.get_parent_post(wr_data.parent_id)
202204
service.send_write_mail_(write, parent_write)
203205
service.set_notice(write.wr_id, wr_data.notice)
204-
set_write_delay(service.request)
205206
service.delete_cache()
206207
db.commit()
207208
return {"result": "created", "wr_id": write.wr_id}

lib/slowapi/__init__.py

Whitespace-only changes.

lib/slowapi/limiter.py

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from typing import Annotated, Optional
2+
from fastapi import Request, Depends
3+
from fastapi.security.utils import get_authorization_scheme_param
4+
from sqlalchemy import select
5+
from slowapi import Limiter
6+
from slowapi.util import get_remote_address
7+
8+
from core.database import DBConnect
9+
from core.models import Config, Member
10+
from api.settings import api_settings
11+
from api.v1.auth import oauth2_scheme
12+
from api.v1.auth.jwt import JWT
13+
from api.v1.service.member import MemberServiceAPI
14+
from api.v1.models.auth import TokenPayload
15+
16+
17+
def get_request_member(
18+
token: Annotated[str, Depends(oauth2_scheme)],
19+
member_service: Annotated[MemberServiceAPI, Depends()]
20+
) -> Optional[Member]:
21+
"""
22+
REST_API 요청 시 JWT 토큰을 통해 사용자 정보를 가져오는 함수
23+
24+
Args:
25+
token (Annotated[str, Depends(oauth2_scheme)]): JWT 토큰
26+
member_service (Annotated[MemberServiceAPI, Depends()]): 사용자 정보 서비스
27+
28+
Returns:
29+
Member: 사용자 정보 객체 또는 None
30+
"""
31+
payload: TokenPayload = JWT.decode_token(
32+
token,
33+
api_settings.ACCESS_TOKEN_SECRET_KEY
34+
)
35+
36+
mb_id: str = payload.sub
37+
if mb_id is None:
38+
return None
39+
40+
member = member_service.get_member(mb_id)
41+
return member
42+
43+
44+
def limiter_key_func(request: Request) -> Optional[str]:
45+
"""
46+
Limiter 인스턴스 생성시 key_func 인자에 제공될 함수.
47+
None으로 반환되는 IP 주소(관리자 IP)는 요청 제한을 하지 않는다.
48+
49+
Args:
50+
request (Request): FastAPI Request 객체
51+
52+
Returns:
53+
Optional[str]: 요청 제한 IP 주소 또는 None
54+
"""
55+
authorization = request.headers.get("Authorization")
56+
scheme, token = get_authorization_scheme_param(authorization)
57+
58+
if not authorization or scheme.lower() != "bearer":
59+
return get_remote_address(request)
60+
61+
with DBConnect().sessionLocal() as db:
62+
member_service = MemberServiceAPI(request, db)
63+
cf_admin = db.scalar(select(Config)).cf_admin
64+
65+
member = get_request_member(token, member_service)
66+
if member.mb_id == cf_admin:
67+
return None
68+
69+
return get_remote_address(request)
70+
71+
72+
def get_cf_delay_sec_from_db():
73+
"""
74+
데이터베이스에서 cf_delay_sec 값을 가져와서
75+
Limiter 인스턴스 생성시 사용할 제한 표현식을 반환하는 함수
76+
"n/t time" 형식으로 반환
77+
- t시간 (시간 단위는 time) 동안 n번의 요청을 허용
78+
- time: second, minute, hour, day, month, year
79+
- documentation: https://limits.readthedocs.io/en/stable/quickstart.html#rate-limit-string-notation
80+
"""
81+
with DBConnect().sessionLocal() as db:
82+
cf_delay_sec = db.scalar(select(Config)).cf_delay_sec
83+
limiter_expr = f"1/{cf_delay_sec} second"
84+
return limiter_expr
85+
86+
87+
# 요청 제한 limiter 인스턴스 생성
88+
limiter = Limiter(key_func=limiter_key_func)
89+
90+
91+
@limiter.limit(
92+
get_cf_delay_sec_from_db,
93+
error_message="너무 빠른 시간내에 게시글을 연속해서 올릴 수 없습니다.",
94+
)
95+
def validate_slowapi_create_post(request: Request):
96+
"""
97+
slowapi의 Limiter를 통해 게시글 생성 API 요청 제한 시간을 검증하는 함수
98+
99+
Args:
100+
request (Request): FastAPI Request 객체
101+
"""
102+
pass

requirements.txt

+5-1
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,8 @@ APScheduler>=3.10.0
6161
pymysql==1.1.1
6262
psycopg2-binary==2.9.9
6363
lxml==5.1.0
64-
PyJWT==2.8.0
64+
PyJWT==2.8.0
65+
Deprecated==1.2.14
66+
limits==3.12.0
67+
slowapi==0.1.9
68+
wrapt==1.16.0

0 commit comments

Comments
 (0)