commit
b08cd1ca42
@ -113,7 +113,7 @@ service.interceptors.response.use(
|
||||
case 401:
|
||||
// 强制要求重新登录,因账号已冻结,账号已过期,手机号码错误,刷新token无效等问题导致
|
||||
authStore.logout()
|
||||
message = '认证已过期,请重新登录'
|
||||
message = '认证已失效,请重新登录'
|
||||
break
|
||||
case 403:
|
||||
// 强制要求重新登录,因无系统权限,而进入到系统访问等问题导致
|
||||
|
@ -102,7 +102,8 @@ const save = async () => {
|
||||
const res = await postCurrentUserResetPassword(formData)
|
||||
if (res) {
|
||||
elForm?.resetFields()
|
||||
ElMessage.success('保存成功')
|
||||
authStore.logout()
|
||||
ElMessage.warning('请重新登录')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
@ -11,7 +11,7 @@ from fastapi.security import OAuth2PasswordBearer
|
||||
"""
|
||||
系统版本
|
||||
"""
|
||||
VERSION = "3.7.1"
|
||||
VERSION = "3.8.0"
|
||||
|
||||
"""安全警告: 不要在生产中打开调试运行!"""
|
||||
DEBUG = False
|
||||
@ -28,6 +28,10 @@ DEMO_WHITE_LIST_PATH = [
|
||||
"/vadmin/resource/images",
|
||||
"/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 .menu import Menu, MenuSimpleOut, RouterOut, Meta, MenuTreeListOut
|
||||
from .dept import Dept, DeptSimpleOut, DeptTreeListOut
|
||||
|
@ -74,6 +74,12 @@ class UserSimpleOut(User):
|
||||
last_ip: str | None = None
|
||||
|
||||
|
||||
class UserPasswordOut(UserSimpleOut):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
password: str
|
||||
|
||||
|
||||
class UserOut(UserSimpleOut):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
@ -27,10 +27,10 @@ class OpenAuth(AuthValidation):
|
||||
"""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
request: Request,
|
||||
token: Annotated[str, Depends(settings.oauth2_scheme)],
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
self,
|
||||
request: Request,
|
||||
token: Annotated[str, Depends(settings.oauth2_scheme)],
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
):
|
||||
"""
|
||||
每次调用依赖此类的接口会执行该方法
|
||||
@ -38,8 +38,8 @@ class OpenAuth(AuthValidation):
|
||||
if not settings.OAUTH_ENABLE:
|
||||
return Auth(db=db)
|
||||
try:
|
||||
telephone = self.validate_token(request, token)
|
||||
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
|
||||
telephone, password = self.validate_token(request, token)
|
||||
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)
|
||||
except CustomException:
|
||||
return Auth(db=db)
|
||||
@ -53,18 +53,18 @@ class AllUserAuth(AuthValidation):
|
||||
"""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
request: Request,
|
||||
token: str = Depends(settings.oauth2_scheme),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
self,
|
||||
request: Request,
|
||||
token: str = Depends(settings.oauth2_scheme),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
):
|
||||
"""
|
||||
每次调用依赖此类的接口会执行该方法
|
||||
"""
|
||||
if not settings.OAUTH_ENABLE:
|
||||
return Auth(db=db)
|
||||
telephone = self.validate_token(request, token)
|
||||
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
|
||||
telephone, password = self.validate_token(request, token)
|
||||
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)
|
||||
|
||||
|
||||
@ -83,23 +83,28 @@ class FullAdminAuth(AuthValidation):
|
||||
self.permissions = None
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
request: Request,
|
||||
token: str = Depends(settings.oauth2_scheme),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
self,
|
||||
request: Request,
|
||||
token: str = Depends(settings.oauth2_scheme),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> Auth:
|
||||
"""
|
||||
每次调用依赖此类的接口会执行该方法
|
||||
"""
|
||||
if not settings.OAUTH_ENABLE:
|
||||
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)]
|
||||
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)
|
||||
permissions = self.get_user_permissions(user)
|
||||
if permissions != {'*.*.*'} and self.permissions:
|
||||
if not (self.permissions & permissions):
|
||||
raise CustomException(msg="无权限操作", code=status.HTTP_403_FORBIDDEN)
|
||||
return result
|
||||
|
||||
|
@ -39,27 +39,27 @@ from .validation.auth import Auth
|
||||
from utils.wx.oauth import WXOAuth
|
||||
import jwt
|
||||
|
||||
|
||||
app = APIRouter()
|
||||
|
||||
|
||||
@app.post("/api/login", summary="API 手机号密码登录", description="Swagger API 文档登录认证")
|
||||
async def api_login_for_access_token(
|
||||
request: Request,
|
||||
data: OAuth2PasswordRequestForm = Depends(),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
request: Request,
|
||||
data: OAuth2PasswordRequestForm = Depends(),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
):
|
||||
user = await UserDal(db).get_data(telephone=data.username, v_return_none=True)
|
||||
error_code = status.HTTP_401_UNAUTHORIZED
|
||||
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)
|
||||
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:
|
||||
raise CustomException(status_code=401, code=401, msg="此手机号已被冻结")
|
||||
raise CustomException(status_code=error_code, code=error_code, msg="此手机号已被冻结")
|
||||
elif not user.is_staff:
|
||||
raise CustomException(status_code=401, code=401, msg="此手机号无权限")
|
||||
access_token = LoginManage.create_token({"sub": user.telephone})
|
||||
raise CustomException(status_code=error_code, code=error_code, msg="此手机号无权限")
|
||||
access_token = LoginManage.create_token({"sub": user.telephone, "password": user.password})
|
||||
record = LoginForm(platform='2', method='0', telephone=data.username, password=data.password)
|
||||
resp = {"access_token": access_token, "token_type": "bearer"}
|
||||
await VadminLoginRecord.create_login_record(db, record, True, request, resp)
|
||||
@ -68,10 +68,10 @@ async def api_login_for_access_token(
|
||||
|
||||
@app.post("/login", summary="手机号密码登录", description="员工登录通道,限制最多输错次数,达到最大值后将is_active=False")
|
||||
async def login_for_access_token(
|
||||
request: Request,
|
||||
data: LoginForm,
|
||||
manage: LoginManage = Depends(),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
request: Request,
|
||||
data: LoginForm,
|
||||
manage: LoginManage = Depends(),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
):
|
||||
try:
|
||||
if data.method == "0":
|
||||
@ -84,9 +84,14 @@ async def login_for_access_token(
|
||||
if not result.status:
|
||||
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)
|
||||
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 = {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
@ -103,10 +108,10 @@ async def login_for_access_token(
|
||||
|
||||
@app.post("/wx/login", summary="微信服务端一键登录", description="员工登录通道")
|
||||
async def wx_login_for_access_token(
|
||||
request: Request,
|
||||
data: WXLoginForm,
|
||||
db: AsyncSession = Depends(db_getter),
|
||||
rd: Redis = Depends(redis_getter)
|
||||
request: Request,
|
||||
data: WXLoginForm,
|
||||
db: AsyncSession = Depends(db_getter),
|
||||
rd: Redis = Depends(redis_getter)
|
||||
):
|
||||
try:
|
||||
if data.platform != "1" or data.method != "2":
|
||||
@ -129,9 +134,12 @@ async def wx_login_for_access_token(
|
||||
await UserDal(db).update_login_info(user, request.client.host)
|
||||
|
||||
# 登录成功创建 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)
|
||||
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 = {
|
||||
"access_token": access_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])
|
||||
telephone: str = payload.get("sub")
|
||||
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)
|
||||
except jwt.exceptions.InvalidSignatureError:
|
||||
return ErrorResponse("无效认证,请您重新登录", code=error_code, status=error_code)
|
||||
except jwt.exceptions.ExpiredSignatureError:
|
||||
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)
|
||||
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 = {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
|
@ -57,7 +57,8 @@ class AuthValidation:
|
||||
telephone: str = payload.get("sub")
|
||||
exp: int = payload.get("exp")
|
||||
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(
|
||||
msg="未认证,请您重新登录",
|
||||
code=status.HTTP_403_FORBIDDEN,
|
||||
@ -79,8 +80,8 @@ class AuthValidation:
|
||||
status_code=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
except jwt.exceptions.ExpiredSignatureError:
|
||||
raise CustomException(msg="认证已过期,请您重新登录", code=cls.error_code, status_code=cls.error_code)
|
||||
return telephone
|
||||
raise CustomException(msg="认证已失效,请您重新登录", code=cls.error_code, status_code=cls.error_code)
|
||||
return telephone, password
|
||||
|
||||
@classmethod
|
||||
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):
|
||||
status: bool | None = False
|
||||
user: schemas.UserOut | None = None
|
||||
user: schemas.UserPasswordOut | None = None
|
||||
msg: str | None = None
|
||||
|
||||
class Config:
|
||||
@ -87,6 +87,6 @@ class LoginValidation:
|
||||
await count.delete()
|
||||
self.result.msg = "OK"
|
||||
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)
|
||||
return self.result
|
||||
|
@ -246,7 +246,7 @@ async def put_menus(
|
||||
|
||||
|
||||
@app.get("/menus/{data_id}", summary="获取菜单信息")
|
||||
async def put_menus(
|
||||
async def get_menus(
|
||||
data_id: int,
|
||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.view", "auth.menu.update"]))
|
||||
):
|
||||
|
@ -32,3 +32,4 @@ class LoginParams(QueryParams):
|
||||
self.address = ("like", address)
|
||||
self.status = status
|
||||
self.platform = platform
|
||||
self.v_order = "desc"
|
||||
|
@ -17,11 +17,12 @@ from core.logger import logger
|
||||
from fastapi import FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from user_agents import parse
|
||||
from application.settings import OPERATION_RECORD_METHOD, MONGO_DB_ENABLE, IGNORE_OPERATION_FUNCTION,\
|
||||
DEMO_WHITE_LIST_PATH, DEMO
|
||||
from application.settings import OPERATION_RECORD_METHOD, MONGO_DB_ENABLE, IGNORE_OPERATION_FUNCTION, \
|
||||
DEMO_WHITE_LIST_PATH, DEMO, DEMO_BLACK_LIST_PATH
|
||||
from utils.response import ErrorResponse
|
||||
from apps.vadmin.record.crud import OperationRecordDal
|
||||
from core.database import mongo_getter
|
||||
from utils import status
|
||||
|
||||
|
||||
def write_request_log(request: Request, response: Response):
|
||||
@ -129,8 +130,15 @@ def register_demo_env_middleware(app: FastAPI):
|
||||
path = request.scope.get("path")
|
||||
if request.method != "GET":
|
||||
print("路由:", path, request.method)
|
||||
if DEMO and request.method != "GET" and path not in DEMO_WHITE_LIST_PATH:
|
||||
return ErrorResponse(msg="演示环境,禁止操作")
|
||||
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 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
|
||||
, **kwargs):
|
||||
def __init__(self, data=None, msg="success", code=http.HTTP_SUCCESS, status=http_status.HTTP_200_OK, **kwargs):
|
||||
self.data = {
|
||||
"code": code,
|
||||
"message": msg,
|
||||
|
Loading…
x
Reference in New Issue
Block a user