미소를뿌리는감자의 코딩
[CatchLounge] JWT - 브라우저 Cookie 저장 본문
1. 개요
JWT란, Json Web Token으로, 서버에 토큰 저장의 부담을 줄여줄 수 있다.
client가 로그인을 성공하게 되면 서버에서 JWT를 반환해 준다.
이를 client가 session 혹은 브라우저 쿠키에 저장을 해두고, 요청 시마다
헤더에 토큰을 포함하여 요청을 보내어, 사용자에 대한 인증을 간편히 할 수 있다.
위에 그림은 로그인 시, JWT 토큰을 서버에서 발행하여 client에게 넘겨주는 것이다.
HTTP 응답에서 Headers에 "set-cookie" : <jwt 토큰> 으로 반환해준다. (session에다가 저장하는 것도 가능하다.)
이를 통해 브라우저는 응답 받은 jwt 토큰을 쿠키에 저장해 두고, 요청을 jwt토큰을 포함해서 서버에 보낸다.
client에서 서버로 요청을 보낼 땐, Headers 부분에 Authorization: Bearer <JWT 토큰> 으로 반환을 해준다.
이를 서버에서 이용해서, 인증된 사용자로 판단, 편리하게 이용할 수 있다.
마지막으로 JWT는 header + payload + signature로 구성되어 있다.
각 부분은 . 으로 구분이 된다.
header는 JWT 타입과 이용 된, 알고리즘을 알려주며,
Payload는 사용자에 대한 정보를 포함한다.
signature은 비밀 키 부분으로, 보안을 위해서 존재한다.
이때, JWT의 헤더와 HTTP 요청의 Headers는 다른 것이니, 잘 파악하도록 하자.
2. 코드
다음으로 JWT를 적용시킨 코드를 확인해 보자.
우선, /sign_in 경로에 POST 요청을 보내었다.
$.ajax({
type: "POST",
url: "/sign_in",
data: {
username_give: username,
password_give: password,
},
success: function (response) {
if (response["result"] == "success") {
$.cookie("mytoken", response["token"], { path: "/" });
alert("로그인 성공");
window.location.href = "/";
} else {
alert("로그인 실패");
}
},
});
@app.route("/sign_in", methods=["POST"])
def sign_in():
username_receive = request.form["username_give"]
password_receive = request.form["password_give"]
password_hash = hashlib.sha256(password_receive.encode("utf-8")).hexdigest()
result = collection_user.find_one(
{
"username": username_receive,
"password": password_hash,
}
)
if result is not None:
payload = {
"username": username_receive,
"exp": datetime.utcnow()
+ timedelta(seconds=60 * 60 * 24), # 로그인 24시간 유지
}
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
return jsonify({"result": "success", "token": token})
return jsonify({"result": "failure"})
비밀번호의 경우 암호화를 해주고, db에서 일치하는 사용자가 있는 지, 확인한다.
있다면, payload ( 사용자에 대한 정보를 포함하는 곳 ) 에 username과 expiretime을 설정해 두고,
나머지 jwt를 구성해 준다. 이렇게 토큰을 생성이 완료되었다.
json으로 token을 넘겨주었다.
success: function (response) {
if (response["result"] == "success") {
$.cookie("mytoken", response["token"], { path: "/" });
alert("로그인 성공");
window.location.href = "/";
} else {
alert("로그인 실패");
}
}
jQuery의 cookie 플러그인을 사용하여, 브라우저의 쿠키에 저장을 해주었다.
"token"으로 명시되어 들어온 토큰을, "mytoken"이라는 이름으로 명시를 하였다.
경로를 / 즉, 루트 경로로 설정해 두어, 저장한 쿠키를 모든 경로에서 접근할 수 있도록 하였다.
다음으로, client에서 다른 API로 요청을 보낼 때, 토큰을 포함해서 보내는 client측 코드를 확인해 보자.
function leaveTable() {
const token = getCookie("mytoken");
$.ajax({
type: "POST",
url: "/cancel",
headers: { Authorization: `Bearer ${token}` },
data: {},
success: function (response) {
if (response["result"] === "success") {
alert("자리가 취소되었습니다.");
window.location.reload();
}
},
error: function (xhr) {
if (xhr.status === 400) {
alert(xhr.responseJSON.message);
} else {
alert("예약에 실패했습니다. 다시 시도해주세요.");
window.location.reload();
}
},
});
}
getCookie로 브라우저에 mytoken의 이름으로 저장해 두었던 token을 들고 왔다.
이후 headers에 Authorization이라는 이름으로 토큰 값을 전달해 주었다.
이제, 백엔드에서 cookie가 토큰이 포함되어 요청된 것을 어떻게 처리하는 지 확인해 볼 것이다.
@app.route("/cancel", methods=["POST"])
@token_required
def cancel_table(current_user):
is_reserved = int(current_user.get("is_reserved", "0"))
if is_reserved > 0: # 예약되어있는 경우 DB에서 예약을 False로 바꾼다.
collection_table.update_one(
{"tableNum": is_reserved},
{"$set": {"occupied": False, "user_name": "None", "time": None}},
)
collection_user.update_one(
{"_id": current_user["_id"]}, {"$set": {"is_reserved": 0}}
)
emit_db_update()
else:
return jsonify({"result": "fail", "message": "예약된 내용이 없습니다."})
return jsonify({"result": "success"})
해당 코드에서, @token_required를 주목해야 한다. token_required라는 데코레이터 함수를 이용해 주었다.
token_required 함수를 확인해 보자.
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = None
if "Authorization" in request.headers:
token = request.headers["Authorization"].split(" ")[1]
if not token:
return jsonify({"result": "failure", "message": "Token is missing"}), 401
try:
data = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
current_user = collection_user.find_one({"username": data["username"]})
except:
return jsonify({"result": "failure", "message": "Token is invalid!"}), 401
return f(current_user, *args, **kwargs)
return decorated
token_required는 인자로 함수를 받고, 함수 실행 중간에 요청이 온 f(함수)를 실행하고 이어서 token_required 함수를 실행하는 식으로 구성이 된다.
즉
======token_required 실행 ======
--------cancel_table 함수 실행 -------
======token_required 실행=======
이 흐름으로 코드가 실행된다.
@wraps(f)는 functools 모듈에 있는 데코레이터이다.
이는 데코레이터로 감싸인 함수의 원래 메타데이터들을 유지하도록 도와준다.
즉, 위 예제에선 cancel_table 의 메타데이터를 유지하도록 도와주게 되는 것이다.
token_required 함수를 자세히 봐보자.
request.headers에 Authorization 유무와 유효성을 확인하고, 성공한다면,
jwt.decode를 이용해서 토큰을 decode한 후, payload에 저장해 두었던 정보들을 읽어온다.
data["username"] 으로 collection에서 user를 찾아낸다.
이후,
f(current_user, *args, **kwargs) 코드가 진행됨에 따라,
인자로 current_user를 전달해주며, cancel_table 함수가 실행되게 된다.
그렇다면 *args와 **kwargs는 무엇일까?
이는 함수에 전달되는 인자의 개수를 미리 알 수 없거나 여러 개의 인자를 유연하게 처리하고 싶을 때, 사용된다 (gpt왈)
데코레이터 함수는 감쌀 함수마다 인자가 다르기 때문에 *args와 **kwargs를 사용해 준다.
각각,
args [위치 인자]
kwargs [키워드 인자]
라고 부른다.
같이 사용해서, 위치 혹은 키워드로 매칭을 할 수 있도록 한다.
이렇게 JWT와 브라우저 쿠키에 저장, 사용하는 방법에 대해서 알아보았다.
'프로젝트' 카테고리의 다른 글
[CatchLounge] 특정 시간에 작동하는 코드 - apscheduler (0) | 2024.09.05 |
---|---|
[CatchLounge] countdown 적용 (1) | 2024.09.05 |
[CatchLounge] Flask를 이용한 CI/CD 구성 (0) | 2024.09.05 |
SSL 인증서 적용 - w. 가비아 + 502 err 해결 (1) | 2024.08.14 |
채팅 서버 배포 - ECR, MongoBD, Docker, Swagger 이용 (0) | 2024.08.12 |