From 5ddab9477320c1049cb6c09e4f591275a3803546 Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Sun, 21 Sep 2025 17:49:55 +0300 Subject: [PATCH] jwt refresh token 1.0 --- .env | 2 +- server/backend/JWT.py | 40 +++++++++++++++++++++++++++++------- server/backend/endpoints.py | 12 +++++++++-- server/database/db.py | 8 ++++++++ server/front/login/js.js | 18 +++++++++++----- server/front/main/index.html | 3 +++ server/front/main/js.js | 17 +++++++++++---- server/front/main/style.css | 14 +++++++++++++ 8 files changed, 95 insertions(+), 19 deletions(-) diff --git a/.env b/.env index 819e2e1..7861a9d 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ SECRET_KEY=SUPER_SECRET_KEY ALGORITHM=HS256 ACCESS_TOKEN_EXPIRE_MINUTES=30 - +REFRESH_TOKEN_EXPIRE_MINUTES=6000 # CORS-middleware # ALLOW_ORIGINS=*, # "*" — разрешить всем; можно указать список конкретных доменов # ALLOW_CREDENTIALS=True, diff --git a/server/backend/JWT.py b/server/backend/JWT.py index 9d7f107..0517469 100644 --- a/server/backend/JWT.py +++ b/server/backend/JWT.py @@ -3,21 +3,47 @@ from jose import JWTError, jwt from fastapi import HTTPException, Depends, status from fastapi.security import OAuth2PasswordBearer +from server.database import db + from dotenv import load_dotenv #Работа с env для jwt import os load_dotenv() SECRET_KEY = os.getenv('SECRET_KEY') ALGORITHM = os.getenv('ALGORITHM') ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES')) - +REFRESH_TOKEN_EXPIRE_MINUTES = int(os.getenv('REFRESH_TOKEN_EXPIRE_MINUTES')) oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") #Создание jwt -async def create_access_token(data: dict, expires_delta: timedelta | None = None): - to_encode = data.copy() - expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15)) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt +class Token(): + @staticmethod + async def create_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15)) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt +class AccessToken(Token): + @staticmethod + async def create(data:dict, expires_delta: timedelta | None = None): + return await Token.create_token(data, expires_delta) +class RefreshToken(Token): + @staticmethod + async def create(data:dict, expires_delta: timedelta | None = None): + token_str = await Token.create_token(data, expires_delta) + await db.refresh_token(encoded_jwt = token_str,email=data["sub"]) + return token_str +# async def create_access_token(data: dict, expires_delta: timedelta | None = None): +# to_encode = data.copy() +# expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15)) +# to_encode.update({"exp": expire}) +# encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) +# return encoded_jwt +# async def create_refresh_token(data:dict, expires_delta:timedelta | None = None): +# to_encode = data.copy() +# expire = datetime.utcnow() + (expires_delta or timedelta(minutes=6000)) +# to_encode.update({"exp": expire}) +# encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) +# return encoded_jwt async def current_user(token: str = Depends(oauth2_scheme)): #Проверка jwt try: diff --git a/server/backend/endpoints.py b/server/backend/endpoints.py index 8de5f74..86d1eee 100644 --- a/server/backend/endpoints.py +++ b/server/backend/endpoints.py @@ -87,8 +87,16 @@ async def login_user(row: pydentic.UserLogin): if not user: raise HTTPException(status_code=401, detail="The user isn't found") - token = await JWT.create_access_token( + access_token = await JWT.AccessToken.create( {"sub": user.email}, timedelta(minutes=JWT.ACCESS_TOKEN_EXPIRE_MINUTES) ) - return {"access_token": token, "token_type": "bearer"} \ No newline at end of file + refresh_token = await JWT.RefreshToken.create( + {"sub": user.email}, + timedelta(minutes=JWT.REFRESH_TOKEN_EXPIRE_MINUTES) + ) + return { + "access_token": access_token, + "refresh_token": refresh_token, + "token_type": "bearer" + } \ No newline at end of file diff --git a/server/database/db.py b/server/database/db.py index 4eb4801..95a59fe 100644 --- a/server/database/db.py +++ b/server/database/db.py @@ -33,6 +33,7 @@ class User(Base): description = Column(String, nullable=False) activated = Column(Boolean, default=False) password = Column(String, nullable=False) + refresh_token = Column(String, nullable=True) async def init_db(): async with async_engine.begin() as conn: @@ -77,6 +78,13 @@ async def LoginUser(user_info): if user and verify_password(user_info.password, user.password): return user return None +async def refresh_token(encoded_jwt, email): + async with AsyncSessionLocal() as session: + result = await session.execute(select(User).where(User.email==email)) + user = result.scalar_one_or_none() + if user: + user.refresh_token = encoded_jwt + await session.commit() async def main(): await init_db() #await CreateUser() diff --git a/server/front/login/js.js b/server/front/login/js.js index c5ef518..a21dc19 100644 --- a/server/front/login/js.js +++ b/server/front/login/js.js @@ -1,3 +1,6 @@ +//Разобраться с хранением и использованием refresh token +//Добавить endpoint logout + const token = localStorage.getItem("token"); if (token) { window.location.href = "./../main/index.html"; @@ -21,11 +24,16 @@ document.getElementById('loginForm').addEventListener('submit', async function ( }); const data = await response.json(); // читаем только один раз - - if (response.ok) { - localStorage.setItem("token", data.access_token); // сохраняем только при успехе + const remember_me = document.getElementById("remember").checked + if (response.ok) { // сохраняем только при успехе + if (remember_me) { // если нажат чекбокс, то сохраняем в localstorage со стандартным временем действия + localStorage.setItem("token", data.access_token); + } else { // если не нажат, то в sessionstorage до перезахода в браузер(?) + sessionStorage.setItem("token", data.access_token); + } + window.location.href = './../main/index.html'; - } else { + } else { //парсинг и вывод ошибок, если есть if (Array.isArray(data.detail)) { const messages = data.detail.map(e => { const field = e.loc.filter(locPart => locPart !== 'body').join(' -> '); @@ -42,7 +50,7 @@ document.getElementById('loginForm').addEventListener('submit', async function ( showError(['Connection error: ' + err.message]); } }); -function showError(messages){ +function showError(messages){ //Добавление их на form со стилями let errorElem = document.getElementById('formError'); let container = document.getElementById('glass-container'); if (!errorElem){ diff --git a/server/front/main/index.html b/server/front/main/index.html index 595914b..cea9322 100644 --- a/server/front/main/index.html +++ b/server/front/main/index.html @@ -9,6 +9,9 @@
+
+ +

data

Data

diff --git a/server/front/main/js.js b/server/front/main/js.js index dc147dd..f716cd1 100644 --- a/server/front/main/js.js +++ b/server/front/main/js.js @@ -1,4 +1,13 @@ -const token = localStorage.getItem("token"); -if (!token) { - window.location.href = "./../login/index.html"; -} \ No newline at end of file +function tokenCheck(){ + const token = localStorage.getItem("token") || sessionStorage.getItem("token"); + if (!token) { + window.location.href = "./../login/index.html"; + } +} + +tokenCheck() +document.getElementById('logoutForm').addEventListener('submit', async function (e) { + e.preventDefault(); + localStorage.removeItem("token"); + tokenCheck(); +}); \ No newline at end of file diff --git a/server/front/main/style.css b/server/front/main/style.css index 87f71e1..d3edd92 100644 --- a/server/front/main/style.css +++ b/server/front/main/style.css @@ -58,3 +58,17 @@ p { color: #fff; margin-top: 15px; } +button { + background: #fff; + color: black; + padding: 10px; + border: none; + border-radius: 10px; + cursor: pointer; + margin-top: 15px; +} +button:hover { + background: transparent; + color: white; + outline: 1px solid #fff; +} \ No newline at end of file