Skip to content

Commit d6d9dc1

Browse files
committed
Add token related interfaces
1 parent 7553ccf commit d6d9dc1

File tree

12 files changed

+301
-93
lines changed

12 files changed

+301
-93
lines changed

backend/app/admin/api/v1/sys/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from backend.app.admin.api.v1.sys.menu import router as menu_router
1313
from backend.app.admin.api.v1.sys.notice import router as notice_router
1414
from backend.app.admin.api.v1.sys.role import router as role_router
15+
from backend.app.admin.api.v1.sys.token import router as token_router
1516
from backend.app.admin.api.v1.sys.user import router as user_router
1617

1718
router = APIRouter(prefix='/sys')
@@ -27,3 +28,4 @@
2728
router.include_router(user_router, prefix='/users', tags=['系统用户'])
2829
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据权限规则'])
2930
router.include_router(notice_router, prefix='/notices', tags=['系统通知公告'])
31+
router.include_router(token_router, prefix='/tokens', tags=['系统令牌'])
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
import json
4+
5+
from typing import Annotated
6+
7+
from fastapi import APIRouter, Depends, Query, Request
8+
9+
from backend.app.admin.schema.token import GetTokenDetail, KickOutToken
10+
from backend.common.enums import StatusType
11+
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
12+
from backend.common.security.jwt import DependsJwtAuth, jwt_decode, superuser_verify
13+
from backend.common.security.permission import RequestPermission
14+
from backend.common.security.rbac import DependsRBAC
15+
from backend.core.conf import settings
16+
from backend.database.redis import redis_client
17+
18+
router = APIRouter()
19+
20+
21+
@router.get('', summary='获取令牌列表', dependencies=[DependsJwtAuth])
22+
async def get_tokens(username: Annotated[str | None, Query()] = None) -> ResponseSchemaModel[list[GetTokenDetail]]:
23+
token_keys = await redis_client.keys(f'{settings.TOKEN_REDIS_PREFIX}:*')
24+
token_online = await redis_client.smembers(settings.TOKEN_ONLINE_REDIS_PREFIX)
25+
data = []
26+
for key in token_keys:
27+
token = await redis_client.get(key)
28+
token_payload = jwt_decode(token)
29+
session_uuid = token_payload.session_uuid
30+
token_detail = GetTokenDetail(
31+
session_uuid=session_uuid,
32+
username='未知',
33+
nickname='未知',
34+
ip='未知',
35+
os='未知',
36+
browser='未知',
37+
device='未知',
38+
status=StatusType.disable if session_uuid not in token_online else StatusType.enable,
39+
last_login_time='未知',
40+
expire_time=token_payload.expire_time,
41+
)
42+
extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{session_uuid}')
43+
if extra_info:
44+
45+
def append_token_detail():
46+
data.append(
47+
token_detail.model_copy(
48+
update={
49+
'username': extra_info.get('username'),
50+
'nickname': extra_info.get('nickname'),
51+
'ip': extra_info.get('ip'),
52+
'os': extra_info.get('os'),
53+
'browser': extra_info.get('browser'),
54+
'device': extra_info.get('device'),
55+
'last_login_time': extra_info.get('last_login_time'),
56+
}
57+
)
58+
)
59+
60+
extra_info = json.loads(extra_info)
61+
if extra_info.get('login_type') != 'swagger':
62+
if username:
63+
if username == extra_info.get('username'):
64+
append_token_detail()
65+
else:
66+
append_token_detail()
67+
else:
68+
data.append(token_detail)
69+
return response_base.success(data=data)
70+
71+
72+
@router.post(
73+
'/kick',
74+
summary='踢下线',
75+
dependencies=[
76+
Depends(RequestPermission('sys:token:kick')),
77+
DependsRBAC,
78+
],
79+
)
80+
async def kick_out(request: Request, obj: KickOutToken) -> ResponseModel:
81+
superuser_verify(request)
82+
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{obj.user_id}:{obj.session_uuid}')
83+
return response_base.success()

backend/app/admin/schema/token.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from datetime import datetime
44

55
from backend.app.admin.schema.user import GetUserInfoNoRelationDetail
6+
from backend.common.enums import StatusType
67
from backend.common.schema import SchemaBase
78

89

@@ -14,8 +15,8 @@ class GetSwaggerToken(SchemaBase):
1415

1516
class AccessTokenBase(SchemaBase):
1617
access_token: str
17-
access_token_type: str = 'Bearer'
1818
access_token_expire_time: datetime
19+
session_uuid: str
1920

2021

2122
class GetNewToken(AccessTokenBase):
@@ -24,3 +25,21 @@ class GetNewToken(AccessTokenBase):
2425

2526
class GetLoginToken(AccessTokenBase):
2627
user: GetUserInfoNoRelationDetail
28+
29+
30+
class KickOutToken(SchemaBase):
31+
user_id: int
32+
session_uuid: str
33+
34+
35+
class GetTokenDetail(SchemaBase):
36+
session_uuid: str
37+
username: str
38+
nickname: str
39+
ip: str
40+
os: str
41+
browser: str
42+
device: str
43+
status: StatusType
44+
last_login_time: str
45+
expire_time: datetime

backend/app/admin/service/auth_service.py

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,13 @@ async def user_verify(db: AsyncSession, username: str, password: str) -> User:
4444
async def swagger_login(self, *, obj: HTTPBasicCredentials) -> tuple[str, User]:
4545
async with async_db_session.begin() as db:
4646
user = await self.user_verify(db, obj.username, obj.password)
47-
user_id = user.id
48-
a_token = await create_access_token(str(user_id), user.is_multi_login)
4947
await user_dao.update_login_time(db, obj.username)
48+
a_token = await create_access_token(
49+
str(user.id),
50+
user.is_multi_login,
51+
# extra info
52+
login_type='swagger',
53+
)
5054
return a_token.access_token, user
5155

5256
async def login(
@@ -61,9 +65,29 @@ async def login(
6165
raise errors.AuthorizationError(msg='验证码失效,请重新获取')
6266
if captcha_code.lower() != obj.captcha.lower():
6367
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
64-
user_id = user.id
65-
a_token = await create_access_token(str(user_id), user.is_multi_login)
66-
r_token = await create_refresh_token(str(user_id), user.is_multi_login)
68+
await redis_client.delete(f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
69+
await user_dao.update_login_time(db, obj.username)
70+
await db.refresh(user)
71+
a_token = await create_access_token(
72+
str(user.id),
73+
user.is_multi_login,
74+
# extra info
75+
username=user.username,
76+
nickname=user.nickname,
77+
last_login_time=timezone.t_str(user.last_login_time),
78+
ip=request.state.ip,
79+
os=request.state.os,
80+
browser=request.state.browser,
81+
device=request.state.device,
82+
)
83+
r_token = await create_refresh_token(str(user.id), user.is_multi_login)
84+
response.set_cookie(
85+
key=settings.COOKIE_REFRESH_TOKEN_KEY,
86+
value=r_token.refresh_token,
87+
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
88+
expires=timezone.f_utc(r_token.refresh_token_expire_time),
89+
httponly=True,
90+
)
6791
except errors.NotFoundError as e:
6892
log.error('登陆错误: 用户名不存在')
6993
raise errors.NotFoundError(msg=e.msg)
@@ -99,19 +123,10 @@ async def login(
99123
msg='登录成功',
100124
),
101125
)
102-
await redis_client.delete(f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
103-
await user_dao.update_login_time(db, obj.username)
104-
response.set_cookie(
105-
key=settings.COOKIE_REFRESH_TOKEN_KEY,
106-
value=r_token.refresh_token,
107-
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
108-
expires=timezone.f_utc(r_token.refresh_token_expire_time),
109-
httponly=True,
110-
)
111-
await db.refresh(user)
112126
data = GetLoginToken(
113127
access_token=a_token.access_token,
114128
access_token_expire_time=a_token.access_token_expire_time,
129+
session_uuid=a_token.session_uuid,
115130
user=user, # type: ignore
116131
)
117132
return data
@@ -122,23 +137,31 @@ async def new_token(*, request: Request, response: Response) -> GetNewToken:
122137
if not refresh_token:
123138
raise errors.TokenError(msg='Refresh Token 丢失,请重新登录')
124139
try:
125-
user_id = jwt_decode(refresh_token)
140+
user_id = jwt_decode(refresh_token).user_id
126141
except Exception:
127142
raise errors.TokenError(msg='Refresh Token 无效')
128143
if request.user.id != user_id:
129144
raise errors.TokenError(msg='Refresh Token 无效')
130145
async with async_db_session() as db:
146+
token = get_token(request)
131147
user = await user_dao.get(db, user_id)
132148
if not user:
133149
raise errors.NotFoundError(msg='用户名或密码有误')
134150
elif not user.status:
135151
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
136-
current_token = get_token(request)
137152
new_token = await create_new_token(
138-
sub=str(user.id),
139-
token=current_token,
153+
user_id=str(user.id),
154+
token=token,
140155
refresh_token=refresh_token,
141156
multi_login=user.is_multi_login,
157+
# extra info
158+
username=user.username,
159+
nickname=user.nickname,
160+
last_login_time=timezone.t_str(user.last_login_time),
161+
ip=request.state.ip,
162+
os=request.state.os,
163+
browser=request.state.browser,
164+
device_type=request.state.device,
142165
)
143166
response.set_cookie(
144167
key=settings.COOKIE_REFRESH_TOKEN_KEY,
@@ -150,25 +173,27 @@ async def new_token(*, request: Request, response: Response) -> GetNewToken:
150173
data = GetNewToken(
151174
access_token=new_token.new_access_token,
152175
access_token_expire_time=new_token.new_access_token_expire_time,
176+
session_uuid=new_token.session_uuid,
153177
)
154178
return data
155179

156180
@staticmethod
157181
async def logout(*, request: Request, response: Response) -> None:
158182
token = get_token(request)
183+
token_payload = jwt_decode(token)
159184
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
160185
response.delete_cookie(settings.COOKIE_REFRESH_TOKEN_KEY)
161186
if request.user.is_multi_login:
162-
key = f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}:{token}'
163-
await redis_client.delete(key)
187+
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}:{token_payload.session_uuid}')
164188
if refresh_token:
165-
key = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{request.user.id}:{refresh_token}'
166-
await redis_client.delete(key)
189+
await redis_client.delete(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{request.user.id}:{refresh_token}')
167190
else:
168-
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}:'
169-
await redis_client.delete_prefix(key_prefix)
170-
key_prefix = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{request.user.id}:'
171-
await redis_client.delete_prefix(key_prefix)
191+
key_prefix = [
192+
f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}:',
193+
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{request.user.id}:',
194+
]
195+
for prefix in key_prefix:
196+
await redis_client.delete_prefix(prefix)
172197

173198

174199
auth_service: AuthService = AuthService()

backend/app/admin/service/oauth2_service.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,37 +31,50 @@ async def create_with_login(
3131
) -> GetLoginToken | None:
3232
async with async_db_session.begin() as db:
3333
# 获取 OAuth2 平台用户信息
34-
_id = user.get('id')
35-
_username = user.get('username')
34+
social_id = user.get('id')
35+
social_username = user.get('username')
3636
if social == UserSocialType.github:
37-
_username = user.get('login')
38-
_nickname = user.get('name')
39-
_email = user.get('email')
40-
if social == UserSocialType.linuxdo:
41-
_email = f'{_username}@linux.do'
42-
if not _email:
37+
social_username = user.get('login')
38+
social_nickname = user.get('name')
39+
social_email = user.get('email')
40+
if social == UserSocialType.linuxdo: # 不提供明文邮箱的平台
41+
social_email = f'{social_username}@linux.do'
42+
if not social_email:
4343
raise AuthorizationError(msg=f'授权失败,{social.value} 账户未绑定邮箱')
4444
# 创建系统用户
45-
sys_user = await user_dao.check_email(db, _email)
45+
sys_user = await user_dao.check_email(db, social_email)
4646
if not sys_user:
47-
sys_user = await user_dao.get_by_username(db, _username)
47+
sys_user = await user_dao.get_by_username(db, social_username)
4848
if sys_user:
49-
_username = f'{_username}#{text_captcha(5)}'
50-
sys_user = await user_dao.get_by_nickname(db, _nickname)
49+
username = f'{social_username}#{text_captcha(5)}'
50+
sys_user = await user_dao.get_by_nickname(db, social_nickname)
5151
if sys_user:
52-
_nickname = f'{_nickname}#{text_captcha(5)}'
53-
new_sys_user = RegisterUserParam(username=_username, password=None, nickname=_nickname, email=_email)
52+
nickname = f'{social_nickname}#{text_captcha(5)}'
53+
new_sys_user = RegisterUserParam(
54+
username=username, password=None, nickname=nickname, email=social_email
55+
)
5456
await user_dao.create(db, new_sys_user, social=True)
5557
await db.flush()
56-
sys_user = await user_dao.check_email(db, _email)
58+
sys_user = await user_dao.check_email(db, social_email)
5759
# 绑定社交用户
5860
sys_user_id = sys_user.id
5961
user_social = await user_social_dao.get(db, sys_user_id, social.value)
6062
if not user_social:
61-
new_user_social = CreateUserSocialParam(source=social.value, uid=str(_id), user_id=sys_user_id)
63+
new_user_social = CreateUserSocialParam(source=social.value, uid=str(social_id), user_id=sys_user_id)
6264
await user_social_dao.create(db, new_user_social)
6365
# 创建 token
64-
access_token = await jwt.create_access_token(str(sys_user_id), sys_user.is_multi_login)
66+
access_token = await jwt.create_access_token(
67+
str(sys_user_id),
68+
sys_user.is_multi_login,
69+
# extra info
70+
username=sys_user.username,
71+
nickname=sys_user.nickname,
72+
last_login_time=timezone.t_str(user.last_login_time),
73+
ip=request.state.ip,
74+
os=request.state.os,
75+
browser=request.state.browser,
76+
device=request.state.device,
77+
)
6578
refresh_token = await jwt.create_refresh_token(str(sys_user_id), multi_login=sys_user.is_multi_login)
6679
await user_dao.update_login_time(db, sys_user.username)
6780
await db.refresh(sys_user)
@@ -87,6 +100,7 @@ async def create_with_login(
87100
access_token=access_token.access_token,
88101
access_token_expire_time=access_token.access_token_expire_time,
89102
user=sys_user, # type: ignore
103+
session_uuid=access_token.session_uuid,
90104
)
91105
return data
92106

0 commit comments

Comments
 (0)