Skip to content

Commit bac41a4

Browse files
authored
Optimize token detection and caching logic (#677)
1 parent 8638c26 commit bac41a4

File tree

7 files changed

+105
-85
lines changed

7 files changed

+105
-85
lines changed

backend/app/admin/api/v1/monitor/online.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@ def append_token_detail() -> None:
4444
for key in token_keys:
4545
token = await redis_client.get(key)
4646
token_payload = jwt_decode(token)
47+
user_id = token_payload.id
4748
session_uuid = token_payload.session_uuid
4849
token_detail = GetTokenDetail(
49-
id=token_payload.id,
50+
id=user_id,
5051
session_uuid=session_uuid,
5152
username='未知',
5253
nickname='未知',
@@ -58,7 +59,7 @@ def append_token_detail() -> None:
5859
last_login_time='未知',
5960
expire_time=token_payload.expire_time,
6061
)
61-
extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{session_uuid}')
62+
extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{user_id}:{session_uuid}')
6263
if extra_info:
6364
extra_info = json.loads(extra_info)
6465
# 排除 swagger 登录生成的 token
@@ -87,5 +88,5 @@ async def delete_session(
8788
session_uuid: Annotated[str, Query(description='会话 UUID')],
8889
) -> ResponseModel:
8990
superuser_verify(request)
90-
await revoke_token(str(pk), session_uuid)
91+
await revoke_token(pk, session_uuid)
9192
return response_base.success()

backend/app/admin/service/auth_service.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ async def swagger_login(self, *, obj: HTTPBasicCredentials) -> tuple[str, User]:
6767
async with async_db_session.begin() as db:
6868
user = await self.user_verify(db, obj.username, obj.password)
6969
await user_dao.update_login_time(db, obj.username)
70-
a_token = await create_access_token(
71-
str(user.id),
70+
access_token = await create_access_token(
71+
user.id,
7272
user.is_multi_login,
7373
# extra info
7474
swagger=True,
7575
)
76-
return a_token.access_token, user
76+
return access_token.access_token, user
7777

7878
async def login(
7979
self, *, request: Request, response: Response, obj: AuthLoginParam, background_tasks: BackgroundTasks
@@ -99,24 +99,24 @@ async def login(
9999
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
100100
await user_dao.update_login_time(db, obj.username)
101101
await db.refresh(user)
102-
a_token = await create_access_token(
103-
str(user.id),
102+
access_token = await create_access_token(
103+
user.id,
104104
user.is_multi_login,
105105
# extra info
106106
username=user.username,
107107
nickname=user.nickname,
108-
last_login_time=timezone.t_str(user.last_login_time),
108+
last_login_time=timezone.to_str(user.last_login_time),
109109
ip=request.state.ip,
110110
os=request.state.os,
111111
browser=request.state.browser,
112112
device=request.state.device,
113113
)
114-
r_token = await create_refresh_token(str(user.id), user.is_multi_login)
114+
refresh_token = await create_refresh_token(access_token.session_uuid, user.id, user.is_multi_login)
115115
response.set_cookie(
116116
key=settings.COOKIE_REFRESH_TOKEN_KEY,
117-
value=r_token.refresh_token,
117+
value=refresh_token.refresh_token,
118118
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
119-
expires=timezone.f_utc(r_token.refresh_token_expire_time),
119+
expires=timezone.to_utc(refresh_token.refresh_token_expire_time),
120120
httponly=True,
121121
)
122122
except errors.NotFoundError as e:
@@ -155,9 +155,9 @@ async def login(
155155
),
156156
)
157157
data = GetLoginToken(
158-
access_token=a_token.access_token,
159-
access_token_expire_time=a_token.access_token_expire_time,
160-
session_uuid=a_token.session_uuid,
158+
access_token=access_token.access_token,
159+
access_token_expire_time=access_token.access_token_expire_time,
160+
session_uuid=access_token.session_uuid,
161161
user=user, # type: ignore
162162
)
163163
return data
@@ -198,24 +198,22 @@ async def refresh_token(*, request: Request) -> GetNewToken:
198198
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
199199
if not refresh_token:
200200
raise errors.TokenError(msg='Refresh Token 已过期,请重新登录')
201-
try:
202-
user_id = jwt_decode(refresh_token).id
203-
except Exception:
204-
raise errors.TokenError(msg='Refresh Token 无效')
201+
token_payload = jwt_decode(refresh_token)
205202
async with async_db_session() as db:
206-
user = await user_dao.get(db, user_id)
203+
user = await user_dao.get(db, token_payload.id)
207204
if not user:
208-
raise errors.NotFoundError(msg='用户名或密码有误')
205+
raise errors.NotFoundError(msg='用户不存在')
209206
elif not user.status:
210207
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
211208
new_token = await create_new_token(
212-
user_id=str(user.id),
213-
refresh_token=refresh_token,
214-
multi_login=user.is_multi_login,
209+
refresh_token,
210+
token_payload.session_uuid,
211+
user.id,
212+
user.is_multi_login,
215213
# extra info
216214
username=user.username,
217215
nickname=user.nickname,
218-
last_login_time=timezone.t_str(user.last_login_time),
216+
last_login_time=timezone.to_str(user.last_login_time),
219217
ip=request.state.ip,
220218
os=request.state.os,
221219
browser=request.state.browser,
@@ -241,6 +239,7 @@ async def logout(*, request: Request, response: Response) -> None:
241239
token = get_token(request)
242240
token_payload = jwt_decode(token)
243241
user_id = token_payload.id
242+
session_uuid = token_payload.session_uuid
244243
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
245244
except errors.TokenError:
246245
return
@@ -249,13 +248,15 @@ async def logout(*, request: Request, response: Response) -> None:
249248

250249
# 清理缓存
251250
if request.user.is_multi_login:
252-
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{token_payload.session_uuid}')
251+
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{session_uuid}')
252+
await redis_client.delete(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{user_id}:{session_uuid}')
253253
if refresh_token:
254254
await redis_client.delete(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{refresh_token}')
255255
else:
256256
key_prefix = [
257257
f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:',
258258
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:',
259+
f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{user_id}:',
259260
]
260261
for prefix in key_prefix:
261262
await redis_client.delete_prefix(prefix)

backend/common/dataclasses.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,6 @@ class RequestCallNext:
3434
response: Response
3535

3636

37-
@dataclasses.dataclass
38-
class NewToken:
39-
new_access_token: str
40-
new_access_token_expire_time: datetime
41-
session_uuid: str
42-
43-
4437
@dataclasses.dataclass
4538
class AccessToken:
4639
access_token: str
@@ -54,6 +47,15 @@ class RefreshToken:
5447
refresh_token_expire_time: datetime
5548

5649

50+
@dataclasses.dataclass
51+
class NewToken:
52+
new_access_token: str
53+
new_access_token_expire_time: datetime
54+
new_refresh_token: str
55+
new_refresh_token_expire_time: datetime
56+
session_uuid: str
57+
58+
5759
@dataclasses.dataclass
5860
class TokenPayload:
5961
id: int

backend/common/security/jwt.py

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,7 @@ def jwt_encode(payload: dict[str, Any]) -> str:
6060
:param payload: 载荷
6161
:return:
6262
"""
63-
return jwt.encode(
64-
payload,
65-
settings.TOKEN_SECRET_KEY,
66-
settings.TOKEN_ALGORITHM,
67-
)
63+
return jwt.encode(payload, settings.TOKEN_SECRET_KEY, settings.TOKEN_ALGORITHM)
6864

6965

7066
def jwt_decode(token: str) -> TokenPayload:
@@ -75,20 +71,27 @@ def jwt_decode(token: str) -> TokenPayload:
7571
:return:
7672
"""
7773
try:
78-
payload = jwt.decode(token, settings.TOKEN_SECRET_KEY, algorithms=[settings.TOKEN_ALGORITHM])
79-
session_uuid = payload.get('session_uuid') or 'debug'
74+
payload = jwt.decode(
75+
token,
76+
settings.TOKEN_SECRET_KEY,
77+
algorithms=[settings.TOKEN_ALGORITHM],
78+
options={'verify_exp': True},
79+
)
80+
session_uuid = payload.get('session_uuid')
8081
user_id = payload.get('sub')
81-
expire_time = payload.get('exp')
82-
if not user_id:
82+
expire = payload.get('exp')
83+
if not session_uuid or not user_id or not expire:
8384
raise errors.TokenError(msg='Token 无效')
8485
except ExpiredSignatureError:
8586
raise errors.TokenError(msg='Token 已过期')
8687
except (JWTError, Exception):
8788
raise errors.TokenError(msg='Token 无效')
88-
return TokenPayload(id=int(user_id), session_uuid=session_uuid, expire_time=expire_time)
89+
return TokenPayload(
90+
id=int(user_id), session_uuid=session_uuid, expire_time=timezone.from_datetime(timezone.to_utc(expire))
91+
)
8992

9093

91-
async def create_access_token(user_id: str, multi_login: bool, **kwargs) -> AccessToken:
94+
async def create_access_token(user_id: int, multi_login: bool, **kwargs) -> AccessToken:
9295
"""
9396
生成加密 token
9497
@@ -101,8 +104,8 @@ async def create_access_token(user_id: str, multi_login: bool, **kwargs) -> Acce
101104
session_uuid = str(uuid4())
102105
access_token = jwt_encode({
103106
'session_uuid': session_uuid,
104-
'exp': expire,
105-
'sub': user_id,
107+
'exp': timezone.to_utc(expire).timestamp(),
108+
'sub': str(user_id),
106109
})
107110

108111
if not multi_login:
@@ -117,68 +120,82 @@ async def create_access_token(user_id: str, multi_login: bool, **kwargs) -> Acce
117120
# Token 附加信息单独存储
118121
if kwargs:
119122
await redis_client.setex(
120-
f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{session_uuid}',
123+
f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{user_id}:{session_uuid}',
121124
settings.TOKEN_EXPIRE_SECONDS,
122125
json.dumps(kwargs, ensure_ascii=False),
123126
)
124127

125128
return AccessToken(access_token=access_token, access_token_expire_time=expire, session_uuid=session_uuid)
126129

127130

128-
async def create_refresh_token(user_id: str, multi_login: bool) -> RefreshToken:
131+
async def create_refresh_token(session_uuid: str, user_id: int, multi_login: bool) -> RefreshToken:
129132
"""
130133
生成加密刷新 token,仅用于创建新的 token
131134
135+
:param session_uuid: 会话 UUID
132136
:param user_id: 用户 ID
133137
:param multi_login: 是否允许多端登录
134138
:return:
135139
"""
136140
expire = timezone.now() + timedelta(seconds=settings.TOKEN_REFRESH_EXPIRE_SECONDS)
137-
refresh_token = jwt_encode({'exp': expire, 'sub': user_id})
141+
refresh_token = jwt_encode({
142+
'session_uuid': session_uuid,
143+
'exp': timezone.to_utc(expire).timestamp(),
144+
'sub': str(user_id),
145+
})
138146

139147
if not multi_login:
140-
key_prefix = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}'
141-
await redis_client.delete_prefix(key_prefix)
148+
await redis_client.delete_prefix(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}')
142149

143150
await redis_client.setex(
144-
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{refresh_token}',
151+
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{session_uuid}',
145152
settings.TOKEN_REFRESH_EXPIRE_SECONDS,
146153
refresh_token,
147154
)
148155
return RefreshToken(refresh_token=refresh_token, refresh_token_expire_time=expire)
149156

150157

151-
async def create_new_token(user_id: str, refresh_token: str, multi_login: bool, **kwargs) -> NewToken:
158+
async def create_new_token(
159+
refresh_token: str, session_uuid: str, user_id: int, multi_login: bool, **kwargs
160+
) -> NewToken:
152161
"""
153162
生成新的 token
154163
155-
:param user_id: 用户 ID
156164
:param refresh_token: 刷新 token
165+
:param session_uuid: 会话 UUID
166+
:param user_id: 用户 ID
157167
:param multi_login: 是否允许多端登录
158168
:param kwargs: token 附加信息
159169
:return:
160170
"""
161-
redis_refresh_token = await redis_client.get(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{refresh_token}')
171+
redis_refresh_token = await redis_client.get(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{session_uuid}')
162172
if not redis_refresh_token or redis_refresh_token != refresh_token:
163173
raise errors.TokenError(msg='Refresh Token 已过期,请重新登录')
174+
175+
await redis_client.delete(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{session_uuid}')
176+
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{session_uuid}')
177+
164178
new_access_token = await create_access_token(user_id, multi_login, **kwargs)
179+
new_refresh_token = await create_refresh_token(new_access_token.session_uuid, user_id, multi_login)
165180
return NewToken(
166181
new_access_token=new_access_token.access_token,
167182
new_access_token_expire_time=new_access_token.access_token_expire_time,
183+
new_refresh_token=new_refresh_token.refresh_token,
184+
new_refresh_token_expire_time=new_refresh_token.refresh_token_expire_time,
168185
session_uuid=new_access_token.session_uuid,
169186
)
170187

171188

172-
async def revoke_token(user_id: str, session_uuid: str) -> None:
189+
async def revoke_token(user_id: int, session_uuid: str) -> None:
173190
"""
174191
撤销 token
175192
176193
:param user_id: 用户 ID
177194
:param session_uuid: 会话 ID
178195
:return:
179196
"""
180-
token_key = f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{session_uuid}'
181-
await redis_client.delete(token_key)
197+
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{session_uuid}')
198+
await redis_client.delete(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{user_id}:{session_uuid}')
182199

183200

184201
def get_token(request: Request) -> str:

backend/plugin/oauth2/service/oauth2_service.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,20 @@ async def create_with_login(
8989

9090
# 创建 token
9191
access_token = await jwt.create_access_token(
92-
str(sys_user.id),
92+
sys_user.id,
9393
sys_user.is_multi_login,
9494
# extra info
9595
username=sys_user.username,
9696
nickname=sys_user.nickname or f'#{text_captcha(5)}',
97-
last_login_time=timezone.t_str(timezone.now()),
97+
last_login_time=timezone.to_str(timezone.now()),
9898
ip=request.state.ip,
9999
os=request.state.os,
100100
browser=request.state.browser,
101101
device=request.state.device,
102102
)
103-
refresh_token = await jwt.create_refresh_token(str(sys_user.id), multi_login=sys_user.is_multi_login)
103+
refresh_token = await jwt.create_refresh_token(
104+
access_token.session_uuid, sys_user.id, sys_user.is_multi_login
105+
)
104106
await user_dao.update_login_time(db, sys_user.username)
105107
await db.refresh(sys_user)
106108
login_log = dict(
@@ -118,7 +120,7 @@ async def create_with_login(
118120
key=settings.COOKIE_REFRESH_TOKEN_KEY,
119121
value=refresh_token.refresh_token,
120122
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
121-
expires=timezone.f_utc(refresh_token.refresh_token_expire_time),
123+
expires=timezone.to_utc(refresh_token.refresh_token_expire_time),
122124
httponly=True,
123125
)
124126
data = GetLoginToken(

backend/utils/server_info.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def get_service_info() -> dict[str, str | datetime]:
150150

151151
try:
152152
create_time = datetime.fromtimestamp(process.create_time(), tz=tz.utc)
153-
start_time = timezone.f_datetime(create_time)
153+
start_time = timezone.from_datetime(create_time)
154154
except (psutil.NoSuchProcess, OSError):
155155
start_time = timezone.now()
156156

@@ -164,7 +164,7 @@ def get_service_info() -> dict[str, str | datetime]:
164164
'mem_vms': ServerInfo.format_bytes(mem_info.vms),
165165
'mem_rss': ServerInfo.format_bytes(mem_info.rss),
166166
'mem_free': ServerInfo.format_bytes(mem_info.vms - mem_info.rss),
167-
'startup': timezone.t_str(start_time),
167+
'startup': timezone.to_str(start_time),
168168
'elapsed': elapsed,
169169
}
170170

0 commit comments

Comments
 (0)