Skip to content

Commit 1190bc3

Browse files
perf: Optimizing API Key Authentication
1 parent cfa19b5 commit 1190bc3

File tree

9 files changed

+223
-45
lines changed

9 files changed

+223
-45
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""056_api_key_ddl
2+
3+
Revision ID: d9a5589fc00b
4+
Revises: 3d4bd2d673dc
5+
Create Date: 2025-12-23 13:41:26.705947
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
import sqlmodel.sql.sqltypes
11+
from sqlalchemy.dialects import postgresql
12+
13+
# revision identifiers, used by Alembic.
14+
revision = 'd9a5589fc00b'
15+
down_revision = '3d4bd2d673dc'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.create_table('sys_apikey',
23+
sa.Column('access_key', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False),
24+
sa.Column('secret_key', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False),
25+
sa.Column('create_time', sa.BigInteger(), nullable=False),
26+
sa.Column('uid', sa.BigInteger(), nullable=False),
27+
sa.Column('status', sa.Boolean(), nullable=False),
28+
sa.Column('id', sa.BigInteger(), nullable=False),
29+
sa.PrimaryKeyConstraint('id')
30+
)
31+
op.create_index(op.f('ix_sys_apikey_id'), 'sys_apikey', ['id'], unique=False)
32+
# ### end Alembic commands ###
33+
34+
35+
def downgrade():
36+
# ### commands auto generated by Alembic - please adjust! ###
37+
op.drop_index(op.f('ix_sys_apikey_id'), table_name='sys_apikey')
38+
op.drop_table('sys_apikey')
39+
# ### end Alembic commands ###

backend/apps/api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from apps.data_training.api import data_training
66
from apps.datasource.api import datasource, table_relation, recommended_problem
77
from apps.mcp import mcp
8-
from apps.system.api import login, user, aimodel, workspace, assistant, parameter
8+
from apps.system.api import login, user, aimodel, workspace, assistant, parameter, apikey
99
from apps.terminology.api import terminology
1010
from apps.settings.api import base
1111

@@ -24,5 +24,6 @@
2424
api_router.include_router(mcp.router)
2525
api_router.include_router(table_relation.router)
2626
api_router.include_router(parameter.router)
27+
api_router.include_router(apikey.router)
2728

2829
api_router.include_router(recommended_problem.router)

backend/apps/system/api/apikey.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
2+
from fastapi import APIRouter
3+
from sqlmodel import func, select
4+
from apps.system.crud.apikey_manage import clear_api_key_cache
5+
from apps.system.models.system_model import ApiKeyModel
6+
from apps.system.schemas.system_schema import ApikeyGridItem, ApikeyStatus
7+
from common.core.deps import CurrentUser, SessionDep
8+
from common.utils.time import get_timestamp
9+
import secrets
10+
11+
router = APIRouter(tags=["system_apikey"], prefix="/system/apikey")
12+
13+
@router.get("")
14+
async def grid(session: SessionDep, current_user: CurrentUser) -> list[ApikeyGridItem]:
15+
query = select(ApiKeyModel).where(ApiKeyModel.uid == current_user.id).order_by(ApiKeyModel.create_time.desc())
16+
return session.exec(query).all()
17+
18+
@router.post("")
19+
async def create(session: SessionDep, current_user: CurrentUser):
20+
count = session.exec(select(func.count()).select_from(ApiKeyModel)).one()
21+
if count >= 5:
22+
raise ValueError("Maximum of 5 API keys allowed")
23+
access_key = secrets.token_urlsafe(16)
24+
secret_key = secrets.token_urlsafe(32)
25+
api_key = ApiKeyModel(
26+
access_key=access_key,
27+
secret_key=secret_key,
28+
create_time=get_timestamp(),
29+
uid=current_user.id,
30+
status=True
31+
)
32+
session.add(api_key)
33+
session.commit()
34+
return api_key.id
35+
36+
@router.put("/status")
37+
async def status(session: SessionDep, current_user: CurrentUser, dto: ApikeyStatus):
38+
api_key = session.get(ApiKeyModel, dto.id)
39+
if not api_key:
40+
raise ValueError("API Key not found")
41+
if api_key.uid != current_user.id:
42+
raise PermissionError("No permission to modify this API Key")
43+
if dto.status == api_key.status:
44+
return
45+
api_key.status = dto.status
46+
await clear_api_key_cache(api_key.access_key)
47+
session.add(api_key)
48+
session.commit()
49+
50+
@router.delete("/{id}")
51+
async def delete(session: SessionDep, current_user: CurrentUser, id: int):
52+
api_key = session.get(ApiKeyModel, id)
53+
if not api_key:
54+
raise ValueError("API Key not found")
55+
if api_key.uid != current_user.id:
56+
raise PermissionError("No permission to delete this API Key")
57+
await clear_api_key_cache(api_key.access_key)
58+
session.delete(api_key)
59+
session.commit()
60+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
from sqlmodel import select
3+
4+
from apps.system.models.system_model import ApiKeyModel
5+
from apps.system.schemas.auth import CacheName, CacheNamespace
6+
from common.core.deps import SessionDep
7+
from common.core.sqlbot_cache import cache, clear_cache
8+
from common.utils.utils import SQLBotLogUtil
9+
10+
@cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.ASK_INFO, keyExpression="access_key")
11+
async def get_api_key(session: SessionDep, access_key: str) -> ApiKeyModel | None:
12+
query = select(ApiKeyModel).where(ApiKeyModel.access_key == access_key)
13+
return session.exec(query).first()
14+
15+
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.ASK_INFO, keyExpression="access_key")
16+
async def clear_api_key_cache(access_key: str):
17+
SQLBotLogUtil.info(f"Api key cache for [{access_key}] has been cleaned")

backend/apps/system/middleware/auth.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import jwt
88
from sqlmodel import Session
99
from starlette.middleware.base import BaseHTTPMiddleware
10-
from apps.system.models.system_model import AssistantModel
10+
from apps.system.crud.apikey_manage import get_api_key
11+
from apps.system.models.system_model import ApiKeyModel, AssistantModel
1112
from common.core.db import engine
1213
from apps.system.crud.assistant import get_assistant_info, get_assistant_user
1314
from apps.system.crud.user import get_user_by_account, get_user_info
@@ -33,7 +34,15 @@ async def dispatch(self, request, call_next):
3334
return await call_next(request)
3435
assistantTokenKey = settings.ASSISTANT_TOKEN_KEY
3536
assistantToken = request.headers.get(assistantTokenKey)
37+
askToken = request.headers.get("X-SQLBOT-ASK-TOKEN")
3638
trans = await get_i18n(request)
39+
if askToken:
40+
validate_pass, data = await self.validateAskToken(askToken, trans)
41+
if validate_pass:
42+
request.state.current_user = data
43+
return await call_next(request)
44+
message = trans('i18n_permission.authenticate_invalid', msg = data)
45+
return JSONResponse(message, status_code=401, headers={"Access-Control-Allow-Origin": "*"})
3746
#if assistantToken and assistantToken.lower().startswith("assistant "):
3847
if assistantToken:
3948
validator: tuple[any] = await self.validateAssistant(assistantToken, trans)
@@ -62,6 +71,50 @@ async def dispatch(self, request, call_next):
6271
def is_options(self, request: Request):
6372
return request.method == "OPTIONS"
6473

74+
async def validateAskToken(self, askToken: Optional[str], trans: I18n):
75+
if not askToken:
76+
return False, f"Miss Token[X-SQLBOT-ASK-TOKEN]!"
77+
schema, param = get_authorization_scheme_param(askToken)
78+
if schema.lower() != "sk":
79+
return False, f"Token schema error!"
80+
try:
81+
payload = jwt.decode(
82+
param, options={"verify_signature": False, "verify_exp": False}, algorithms=[security.ALGORITHM]
83+
)
84+
access_key = payload.get('access_key', None)
85+
86+
if not access_key:
87+
return False, f"Miss access_key payload error!"
88+
with Session(engine) as session:
89+
api_key_model = await get_api_key(session, access_key)
90+
api_key_model = ApiKeyModel.model_validate(api_key_model) if api_key_model else None
91+
if not api_key_model:
92+
return False, f"Invalid access_key!"
93+
if not api_key_model.status:
94+
return False, f"Disabled access_key!"
95+
payload = jwt.decode(
96+
param, api_key_model.secret_key, algorithms=[security.ALGORITHM]
97+
)
98+
uid = api_key_model.uid
99+
session_user = await get_user_info(session = session, user_id = uid)
100+
if not session_user:
101+
message = trans('i18n_not_exist', msg = trans('i18n_user.account'))
102+
raise Exception(message)
103+
session_user = UserInfoDTO.model_validate(session_user)
104+
if session_user.status != 1:
105+
message = trans('i18n_login.user_disable', msg = trans('i18n_concat_admin'))
106+
raise Exception(message)
107+
if not session_user.oid or session_user.oid == 0:
108+
message = trans('i18n_login.no_associated_ws', msg = trans('i18n_concat_admin'))
109+
raise Exception(message)
110+
return True, session_user
111+
except Exception as e:
112+
msg = str(e)
113+
SQLBotLogUtil.exception(f"Token validation error: {msg}")
114+
if 'expired' in msg:
115+
return False, jwt.ExpiredSignatureError(trans('i18n_permission.token_expired'))
116+
return False, e
117+
65118
async def validateToken(self, token: Optional[str], trans: I18n):
66119
if not token:
67120
return False, f"Miss Token[{settings.TOKEN_KEY}]!"

backend/apps/system/models/system_model.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,15 @@ class AuthenticationModel(SnowflakeBase, AuthenticationBaseModel, table=True):
6767
__tablename__ = "sys_authentication"
6868
create_time: Optional[int] = Field(default=0, sa_type=BigInteger())
6969
enable: bool = Field(default=False, nullable=False)
70-
valid: bool = Field(default=False, nullable=False)
70+
valid: bool = Field(default=False, nullable=False)
71+
72+
73+
class ApiKeyBaseModel(SQLModel):
74+
access_key: str = Field(max_length=255, nullable=False)
75+
secret_key: str = Field(max_length=255, nullable=False)
76+
create_time: int = Field(default=0, sa_type=BigInteger())
77+
uid: int = Field(default=0,nullable=False, sa_type=BigInteger())
78+
status: bool = Field(default=True, nullable=False)
79+
80+
class ApiKeyModel(SnowflakeBase, ApiKeyBaseModel, table=True):
81+
__tablename__ = "sys_apikey"

backend/apps/system/schemas/auth.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class CacheName(Enum):
1616
USER_INFO = "user:info"
1717
ASSISTANT_INFO = "assistant:info"
1818
ASSISTANT_DS = "assistant:ds"
19+
ASK_INFO = "ask:info"
1920
def __str__(self):
2021
return self.value
2122

backend/apps/system/schemas/system_schema.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,13 @@ class AssistantUiSchema(BaseCreatorDTO):
207207
name: Optional[str] = None
208208
welcome: Optional[str] = None
209209
welcome_desc: Optional[str] = None
210+
211+
class ApikeyStatus(BaseModel):
212+
id: int = Field(description=f"{PLACEHOLDER_PREFIX}id")
213+
status: bool = Field(description=f"{PLACEHOLDER_PREFIX}status")
214+
215+
class ApikeyGridItem(BaseCreatorDTO):
216+
access_key: str = Field(description=f"Access Key")
217+
secret_key: str = Field(description=f"Secret Key")
218+
status: bool = Field(description=f"{PLACEHOLDER_PREFIX}status")
219+
create_time: int = Field(description=f"{PLACEHOLDER_PREFIX}create_time")

frontend/src/components/layout/Apikey.vue

Lines changed: 28 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts" setup>
2-
import { computed, reactive, ref } from 'vue'
2+
import { computed, onMounted, reactive, ref } from 'vue'
33
import { useI18n } from 'vue-i18n'
44
import icon_warning_filled from '@/assets/svg/icon_info_colorful.svg'
55
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
@@ -11,6 +11,8 @@ import icon_visible_outlined from '@/assets/embedded/icon_visible_outlined.svg'
1111
import { formatTimestamp } from '@/utils/date'
1212
import { useClipboard } from '@vueuse/core'
1313
import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue'
14+
import { request } from '@/utils/request'
15+
1416
const { t } = useI18n()
1517
1618
const limitCount = ref(5)
@@ -23,37 +25,13 @@ const state = reactive({
2325
tableData: [] as any,
2426
})
2527
26-
state.tableData = [
27-
{
28-
access_key: 'fwafwafwafwaf',
29-
secret_key: '1234567',
30-
status: false,
31-
create_time: 1766455902237,
32-
},
33-
{
34-
access_key: 'asdasdasdasd',
35-
secret_key: '987654321',
36-
status: true,
37-
create_time: 1766455902237,
38-
},
39-
{
40-
access_key: 'asdasdasdasd',
41-
secret_key: '987654321',
42-
status: true,
43-
create_time: 1766455902237,
44-
},
45-
{
46-
access_key: 'asdasdasdasd',
47-
secret_key: '987654321',
48-
status: true,
49-
create_time: 1766455902237,
50-
},
51-
]
5228
const handleAdd = () => {
5329
if (triggerLimit.value) {
5430
return
5531
}
56-
console.log('Add API Key')
32+
request.post('/system/apikey', {}).then(() => {
33+
loadGridData()
34+
})
5735
}
5836
const pwd = ref('**********')
5937
const toApiDoc = () => {
@@ -63,14 +41,13 @@ const toApiDoc = () => {
6341
}
6442
6543
const statusHandler = (row: any) => {
66-
/* state.form = { ...row }
67-
editTerm() */
6844
const param = {
6945
id: row.id,
7046
status: row.status,
7147
}
72-
console.log(row, param)
73-
// userApi.status(param)
48+
request.put('/system/apikey/status', param).then(() => {
49+
loadGridData()
50+
})
7451
}
7552
const { copy } = useClipboard({ legacy: true })
7653
@@ -90,18 +67,27 @@ const deleteHandler = (row: any) => {
9067
cancelButtonText: t('common.cancel'),
9168
customClass: 'confirm-no_icon',
9269
autofocus: false,
93-
}).then(() => {
94-
/* userApi.delete(row.id).then(() => {
95-
multipleSelectionAll.value = multipleSelectionAll.value.filter((ele) => ele.id !== row.id)
96-
ElMessage({
97-
type: 'success',
98-
message: t('dashboard.delete_success'),
99-
})
100-
search()
101-
}) */
102-
console.log('execute delete')
70+
callback: (action: any) => {
71+
if (action === 'confirm') {
72+
request.delete(`/system/apikey/${row.id}`).then(() => {
73+
loadGridData()
74+
ElMessage({
75+
type: 'success',
76+
message: t('dashboard.delete_success'),
77+
})
78+
})
79+
}
80+
},
81+
})
82+
}
83+
const loadGridData = () => {
84+
request.get('/system/apikey').then((res: any) => {
85+
state.tableData = res || []
10386
})
10487
}
88+
onMounted(() => {
89+
loadGridData()
90+
})
10591
</script>
10692

10793
<template>

0 commit comments

Comments
 (0)