commit
b08cd1ca42
@ -113,7 +113,7 @@ service.interceptors.response.use(
|
|||||||
case 401:
|
case 401:
|
||||||
// 强制要求重新登录,因账号已冻结,账号已过期,手机号码错误,刷新token无效等问题导致
|
// 强制要求重新登录,因账号已冻结,账号已过期,手机号码错误,刷新token无效等问题导致
|
||||||
authStore.logout()
|
authStore.logout()
|
||||||
message = '认证已过期,请重新登录'
|
message = '认证已失效,请重新登录'
|
||||||
break
|
break
|
||||||
case 403:
|
case 403:
|
||||||
// 强制要求重新登录,因无系统权限,而进入到系统访问等问题导致
|
// 强制要求重新登录,因无系统权限,而进入到系统访问等问题导致
|
||||||
|
@ -102,7 +102,8 @@ const save = async () => {
|
|||||||
const res = await postCurrentUserResetPassword(formData)
|
const res = await postCurrentUserResetPassword(formData)
|
||||||
if (res) {
|
if (res) {
|
||||||
elForm?.resetFields()
|
elForm?.resetFields()
|
||||||
ElMessage.success('保存成功')
|
authStore.logout()
|
||||||
|
ElMessage.warning('请重新登录')
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
@ -11,7 +11,7 @@ from fastapi.security import OAuth2PasswordBearer
|
|||||||
"""
|
"""
|
||||||
系统版本
|
系统版本
|
||||||
"""
|
"""
|
||||||
VERSION = "3.7.1"
|
VERSION = "3.8.0"
|
||||||
|
|
||||||
"""安全警告: 不要在生产中打开调试运行!"""
|
"""安全警告: 不要在生产中打开调试运行!"""
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
@ -28,6 +28,10 @@ DEMO_WHITE_LIST_PATH = [
|
|||||||
"/vadmin/resource/images",
|
"/vadmin/resource/images",
|
||||||
"/vadmin/auth/user/export/query/list/to/excel"
|
"/vadmin/auth/user/export/query/list/to/excel"
|
||||||
]
|
]
|
||||||
|
"""演示功能黑名单(触发异常 status_code=403),黑名单优先级更高"""
|
||||||
|
DEMO_BLACK_LIST_PATH = [
|
||||||
|
"/auth/api/login"
|
||||||
|
]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
引入数据库配置
|
引入数据库配置
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from .user import UserOut, UserUpdate, User, UserIn, UserSimpleOut, ResetPwd, UserUpdateBaseInfo
|
from .user import UserOut, UserUpdate, User, UserIn, UserSimpleOut, ResetPwd, UserUpdateBaseInfo, UserPasswordOut
|
||||||
from .role import Role, RoleOut, RoleIn, RoleOptionsOut, RoleSimpleOut
|
from .role import Role, RoleOut, RoleIn, RoleOptionsOut, RoleSimpleOut
|
||||||
from .menu import Menu, MenuSimpleOut, RouterOut, Meta, MenuTreeListOut
|
from .menu import Menu, MenuSimpleOut, RouterOut, Meta, MenuTreeListOut
|
||||||
from .dept import Dept, DeptSimpleOut, DeptTreeListOut
|
from .dept import Dept, DeptSimpleOut, DeptTreeListOut
|
||||||
|
@ -74,6 +74,12 @@ class UserSimpleOut(User):
|
|||||||
last_ip: str | None = None
|
last_ip: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserPasswordOut(UserSimpleOut):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
class UserOut(UserSimpleOut):
|
class UserOut(UserSimpleOut):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
@ -38,8 +38,8 @@ class OpenAuth(AuthValidation):
|
|||||||
if not settings.OAUTH_ENABLE:
|
if not settings.OAUTH_ENABLE:
|
||||||
return Auth(db=db)
|
return Auth(db=db)
|
||||||
try:
|
try:
|
||||||
telephone = self.validate_token(request, token)
|
telephone, password = self.validate_token(request, token)
|
||||||
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
|
user = await UserDal(db).get_data(telephone=telephone, password=password, v_return_none=True)
|
||||||
return await self.validate_user(request, user, db, is_all=True)
|
return await self.validate_user(request, user, db, is_all=True)
|
||||||
except CustomException:
|
except CustomException:
|
||||||
return Auth(db=db)
|
return Auth(db=db)
|
||||||
@ -63,8 +63,8 @@ class AllUserAuth(AuthValidation):
|
|||||||
"""
|
"""
|
||||||
if not settings.OAUTH_ENABLE:
|
if not settings.OAUTH_ENABLE:
|
||||||
return Auth(db=db)
|
return Auth(db=db)
|
||||||
telephone = self.validate_token(request, token)
|
telephone, password = self.validate_token(request, token)
|
||||||
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
|
user = await UserDal(db).get_data(telephone=telephone, password=password, v_return_none=True)
|
||||||
return await self.validate_user(request, user, db, is_all=True)
|
return await self.validate_user(request, user, db, is_all=True)
|
||||||
|
|
||||||
|
|
||||||
@ -93,13 +93,18 @@ class FullAdminAuth(AuthValidation):
|
|||||||
"""
|
"""
|
||||||
if not settings.OAUTH_ENABLE:
|
if not settings.OAUTH_ENABLE:
|
||||||
return Auth(db=db)
|
return Auth(db=db)
|
||||||
telephone = self.validate_token(request, token)
|
telephone, password = self.validate_token(request, token)
|
||||||
options = [joinedload(VadminUser.roles).subqueryload(VadminRole.menus), joinedload(VadminUser.depts)]
|
options = [joinedload(VadminUser.roles).subqueryload(VadminRole.menus), joinedload(VadminUser.depts)]
|
||||||
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True, v_options=options, is_staff=True)
|
user = await UserDal(db).get_data(
|
||||||
|
telephone=telephone,
|
||||||
|
password=password,
|
||||||
|
v_return_none=True,
|
||||||
|
v_options=options,
|
||||||
|
is_staff=True
|
||||||
|
)
|
||||||
result = await self.validate_user(request, user, db, is_all=False)
|
result = await self.validate_user(request, user, db, is_all=False)
|
||||||
permissions = self.get_user_permissions(user)
|
permissions = self.get_user_permissions(user)
|
||||||
if permissions != {'*.*.*'} and self.permissions:
|
if permissions != {'*.*.*'} and self.permissions:
|
||||||
if not (self.permissions & permissions):
|
if not (self.permissions & permissions):
|
||||||
raise CustomException(msg="无权限操作", code=status.HTTP_403_FORBIDDEN)
|
raise CustomException(msg="无权限操作", code=status.HTTP_403_FORBIDDEN)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -39,7 +39,6 @@ from .validation.auth import Auth
|
|||||||
from utils.wx.oauth import WXOAuth
|
from utils.wx.oauth import WXOAuth
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
|
|
||||||
app = APIRouter()
|
app = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@ -50,16 +49,17 @@ async def api_login_for_access_token(
|
|||||||
db: AsyncSession = Depends(db_getter)
|
db: AsyncSession = Depends(db_getter)
|
||||||
):
|
):
|
||||||
user = await UserDal(db).get_data(telephone=data.username, v_return_none=True)
|
user = await UserDal(db).get_data(telephone=data.username, v_return_none=True)
|
||||||
|
error_code = status.HTTP_401_UNAUTHORIZED
|
||||||
if not user:
|
if not user:
|
||||||
raise CustomException(status_code=401, code=401, msg="该手机号不存在")
|
raise CustomException(status_code=error_code, code=error_code, msg="该手机号不存在")
|
||||||
result = VadminUser.verify_password(data.password, user.password)
|
result = VadminUser.verify_password(data.password, user.password)
|
||||||
if not result:
|
if not result:
|
||||||
raise CustomException(status_code=401, code=401, msg="手机号或密码错误")
|
raise CustomException(status_code=error_code, code=error_code, msg="手机号或密码错误")
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
raise CustomException(status_code=401, code=401, msg="此手机号已被冻结")
|
raise CustomException(status_code=error_code, code=error_code, msg="此手机号已被冻结")
|
||||||
elif not user.is_staff:
|
elif not user.is_staff:
|
||||||
raise CustomException(status_code=401, code=401, msg="此手机号无权限")
|
raise CustomException(status_code=error_code, code=error_code, msg="此手机号无权限")
|
||||||
access_token = LoginManage.create_token({"sub": user.telephone})
|
access_token = LoginManage.create_token({"sub": user.telephone, "password": user.password})
|
||||||
record = LoginForm(platform='2', method='0', telephone=data.username, password=data.password)
|
record = LoginForm(platform='2', method='0', telephone=data.username, password=data.password)
|
||||||
resp = {"access_token": access_token, "token_type": "bearer"}
|
resp = {"access_token": access_token, "token_type": "bearer"}
|
||||||
await VadminLoginRecord.create_login_record(db, record, True, request, resp)
|
await VadminLoginRecord.create_login_record(db, record, True, request, resp)
|
||||||
@ -84,9 +84,14 @@ async def login_for_access_token(
|
|||||||
if not result.status:
|
if not result.status:
|
||||||
raise ValueError(result.msg)
|
raise ValueError(result.msg)
|
||||||
|
|
||||||
access_token = LoginManage.create_token({"sub": result.user.telephone, "is_refresh": False})
|
access_token = LoginManage.create_token(
|
||||||
|
{"sub": result.user.telephone, "is_refresh": False, "password": result.user.password}
|
||||||
|
)
|
||||||
expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
||||||
refresh_token = LoginManage.create_token({"sub": result.user.telephone, "is_refresh": True}, expires=expires)
|
refresh_token = LoginManage.create_token(
|
||||||
|
payload={"sub": result.user.telephone, "is_refresh": True, "password": result.user.password},
|
||||||
|
expires=expires
|
||||||
|
)
|
||||||
resp = {
|
resp = {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"refresh_token": refresh_token,
|
"refresh_token": refresh_token,
|
||||||
@ -129,9 +134,12 @@ async def wx_login_for_access_token(
|
|||||||
await UserDal(db).update_login_info(user, request.client.host)
|
await UserDal(db).update_login_info(user, request.client.host)
|
||||||
|
|
||||||
# 登录成功创建 token
|
# 登录成功创建 token
|
||||||
access_token = LoginManage.create_token({"sub": user.telephone, "is_refresh": False})
|
access_token = LoginManage.create_token({"sub": user.telephone, "is_refresh": False, "password": user.password})
|
||||||
expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
||||||
refresh_token = LoginManage.create_token({"sub": user.telephone, "is_refresh": True}, expires=expires)
|
refresh_token = LoginManage.create_token(
|
||||||
|
payload={"sub": user.telephone, "is_refresh": True, "password": user.password},
|
||||||
|
expires=expires
|
||||||
|
)
|
||||||
resp = {
|
resp = {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"refresh_token": refresh_token,
|
"refresh_token": refresh_token,
|
||||||
@ -155,16 +163,20 @@ async def token_refresh(refresh: str = Body(..., title="刷新Token")):
|
|||||||
payload = jwt.decode(refresh, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
payload = jwt.decode(refresh, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||||
telephone: str = payload.get("sub")
|
telephone: str = payload.get("sub")
|
||||||
is_refresh: bool = payload.get("is_refresh")
|
is_refresh: bool = payload.get("is_refresh")
|
||||||
if telephone is None or not is_refresh:
|
password: str = payload.get("password")
|
||||||
|
if not telephone or not is_refresh or not password:
|
||||||
return ErrorResponse("未认证,请您重新登录", code=error_code, status=error_code)
|
return ErrorResponse("未认证,请您重新登录", code=error_code, status=error_code)
|
||||||
except jwt.exceptions.InvalidSignatureError:
|
except jwt.exceptions.InvalidSignatureError:
|
||||||
return ErrorResponse("无效认证,请您重新登录", code=error_code, status=error_code)
|
return ErrorResponse("无效认证,请您重新登录", code=error_code, status=error_code)
|
||||||
except jwt.exceptions.ExpiredSignatureError:
|
except jwt.exceptions.ExpiredSignatureError:
|
||||||
return ErrorResponse("登录已超时,请您重新登录", code=error_code, status=error_code)
|
return ErrorResponse("登录已超时,请您重新登录", code=error_code, status=error_code)
|
||||||
|
|
||||||
access_token = LoginManage.create_token({"sub": telephone, "is_refresh": False})
|
access_token = LoginManage.create_token({"sub": telephone, "is_refresh": False, "password": password})
|
||||||
expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
||||||
refresh_token = LoginManage.create_token({"sub": telephone, "is_refresh": True}, expires=expires)
|
refresh_token = LoginManage.create_token(
|
||||||
|
payload={"sub": telephone, "is_refresh": True, "password": password},
|
||||||
|
expires=expires
|
||||||
|
)
|
||||||
resp = {
|
resp = {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"refresh_token": refresh_token,
|
"refresh_token": refresh_token,
|
||||||
|
@ -57,7 +57,8 @@ class AuthValidation:
|
|||||||
telephone: str = payload.get("sub")
|
telephone: str = payload.get("sub")
|
||||||
exp: int = payload.get("exp")
|
exp: int = payload.get("exp")
|
||||||
is_refresh: bool = payload.get("is_refresh")
|
is_refresh: bool = payload.get("is_refresh")
|
||||||
if telephone is None or is_refresh:
|
password: bool = payload.get("password")
|
||||||
|
if not telephone or is_refresh or not password:
|
||||||
raise CustomException(
|
raise CustomException(
|
||||||
msg="未认证,请您重新登录",
|
msg="未认证,请您重新登录",
|
||||||
code=status.HTTP_403_FORBIDDEN,
|
code=status.HTTP_403_FORBIDDEN,
|
||||||
@ -79,8 +80,8 @@ class AuthValidation:
|
|||||||
status_code=status.HTTP_403_FORBIDDEN
|
status_code=status.HTTP_403_FORBIDDEN
|
||||||
)
|
)
|
||||||
except jwt.exceptions.ExpiredSignatureError:
|
except jwt.exceptions.ExpiredSignatureError:
|
||||||
raise CustomException(msg="认证已过期,请您重新登录", code=cls.error_code, status_code=cls.error_code)
|
raise CustomException(msg="认证已失效,请您重新登录", code=cls.error_code, status_code=cls.error_code)
|
||||||
return telephone
|
return telephone, password
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def validate_user(cls, request: Request, user: VadminUser, db: AsyncSession, is_all: bool = True) -> Auth:
|
async def validate_user(cls, request: Request, user: VadminUser, db: AsyncSession, is_all: bool = True) -> Auth:
|
||||||
|
@ -35,7 +35,7 @@ class WXLoginForm(BaseModel):
|
|||||||
|
|
||||||
class LoginResult(BaseModel):
|
class LoginResult(BaseModel):
|
||||||
status: bool | None = False
|
status: bool | None = False
|
||||||
user: schemas.UserOut | None = None
|
user: schemas.UserPasswordOut | None = None
|
||||||
msg: str | None = None
|
msg: str | None = None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@ -87,6 +87,6 @@ class LoginValidation:
|
|||||||
await count.delete()
|
await count.delete()
|
||||||
self.result.msg = "OK"
|
self.result.msg = "OK"
|
||||||
self.result.status = True
|
self.result.status = True
|
||||||
self.result.user = schemas.UserSimpleOut.model_validate(user)
|
self.result.user = schemas.UserPasswordOut.model_validate(user)
|
||||||
await crud.UserDal(db).update_login_info(user, request.client.host)
|
await crud.UserDal(db).update_login_info(user, request.client.host)
|
||||||
return self.result
|
return self.result
|
||||||
|
@ -246,7 +246,7 @@ async def put_menus(
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/menus/{data_id}", summary="获取菜单信息")
|
@app.get("/menus/{data_id}", summary="获取菜单信息")
|
||||||
async def put_menus(
|
async def get_menus(
|
||||||
data_id: int,
|
data_id: int,
|
||||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.view", "auth.menu.update"]))
|
auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.view", "auth.menu.update"]))
|
||||||
):
|
):
|
||||||
|
@ -32,3 +32,4 @@ class LoginParams(QueryParams):
|
|||||||
self.address = ("like", address)
|
self.address = ("like", address)
|
||||||
self.status = status
|
self.status = status
|
||||||
self.platform = platform
|
self.platform = platform
|
||||||
|
self.v_order = "desc"
|
||||||
|
@ -17,11 +17,12 @@ from core.logger import logger
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
from user_agents import parse
|
from user_agents import parse
|
||||||
from application.settings import OPERATION_RECORD_METHOD, MONGO_DB_ENABLE, IGNORE_OPERATION_FUNCTION,\
|
from application.settings import OPERATION_RECORD_METHOD, MONGO_DB_ENABLE, IGNORE_OPERATION_FUNCTION, \
|
||||||
DEMO_WHITE_LIST_PATH, DEMO
|
DEMO_WHITE_LIST_PATH, DEMO, DEMO_BLACK_LIST_PATH
|
||||||
from utils.response import ErrorResponse
|
from utils.response import ErrorResponse
|
||||||
from apps.vadmin.record.crud import OperationRecordDal
|
from apps.vadmin.record.crud import OperationRecordDal
|
||||||
from core.database import mongo_getter
|
from core.database import mongo_getter
|
||||||
|
from utils import status
|
||||||
|
|
||||||
|
|
||||||
def write_request_log(request: Request, response: Response):
|
def write_request_log(request: Request, response: Response):
|
||||||
@ -129,7 +130,14 @@ def register_demo_env_middleware(app: FastAPI):
|
|||||||
path = request.scope.get("path")
|
path = request.scope.get("path")
|
||||||
if request.method != "GET":
|
if request.method != "GET":
|
||||||
print("路由:", path, request.method)
|
print("路由:", path, request.method)
|
||||||
if DEMO and request.method != "GET" and path not in DEMO_WHITE_LIST_PATH:
|
if DEMO and request.method != "GET":
|
||||||
|
if path in DEMO_BLACK_LIST_PATH:
|
||||||
|
return ErrorResponse(
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
code=status.HTTP_403_FORBIDDEN,
|
||||||
|
msg="演示环境,禁止操作"
|
||||||
|
)
|
||||||
|
elif path not in DEMO_WHITE_LIST_PATH:
|
||||||
return ErrorResponse(msg="演示环境,禁止操作")
|
return ErrorResponse(msg="演示环境,禁止操作")
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
|
|
||||||
|
Binary file not shown.
@ -8,8 +8,7 @@ class SuccessResponse(Response):
|
|||||||
"""
|
"""
|
||||||
成功响应
|
成功响应
|
||||||
"""
|
"""
|
||||||
def __init__(self, data=None, msg="success", code=http.HTTP_SUCCESS, status=http_status.HTTP_200_OK
|
def __init__(self, data=None, msg="success", code=http.HTTP_SUCCESS, status=http_status.HTTP_200_OK, **kwargs):
|
||||||
, **kwargs):
|
|
||||||
self.data = {
|
self.data = {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": msg,
|
"message": msg,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user