!13 更新至 v3.8.0

Merge pull request !13 from ktianc/master
This commit is contained in:
ktianc 2024-03-07 09:59:17 +00:00 committed by Gitee
commit b08cd1ca42
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
14 changed files with 96 additions and 59 deletions

View File

@ -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:
// 强制要求重新登录,因无系统权限,而进入到系统访问等问题导致 // 强制要求重新登录,因无系统权限,而进入到系统访问等问题导致

View File

@ -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

View File

@ -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"
]
""" """
引入数据库配置 引入数据库配置

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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:

View File

@ -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

View File

@ -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"]))
): ):

View File

@ -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"

View File

@ -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.

View File

@ -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,