项目初次提交
This commit is contained in:
7
apps/vadmin/auth/utils/__init__.py
Normal file
7
apps/vadmin/auth/utils/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Create Time : 2022/8/8 11:02
|
||||
# @File : __init__.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 简要说明
|
114
apps/vadmin/auth/utils/current.py
Normal file
114
apps/vadmin/auth/utils/current.py
Normal file
@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Create Time : 2021/10/24 16:44
|
||||
# @File : current.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 获取认证后的信息工具
|
||||
|
||||
from typing import Annotated
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import joinedload
|
||||
from apps.vadmin.auth.crud import UserDal
|
||||
from apps.vadmin.auth.models import VadminUser, VadminRole
|
||||
from core.exception import CustomException
|
||||
from utils import status
|
||||
from .validation import AuthValidation
|
||||
from fastapi import Request, Depends
|
||||
from application import settings
|
||||
from core.database import db_getter
|
||||
from .validation.auth import Auth
|
||||
|
||||
|
||||
class OpenAuth(AuthValidation):
|
||||
|
||||
"""
|
||||
开放认证,无认证也可以访问
|
||||
认证了以后可以获取到用户信息,无认证则获取不到
|
||||
"""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
request: Request,
|
||||
token: Annotated[str, Depends(settings.oauth2_scheme)],
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
):
|
||||
"""
|
||||
每次调用依赖此类的接口会执行该方法
|
||||
"""
|
||||
if not settings.OAUTH_ENABLE:
|
||||
return Auth(db=db)
|
||||
try:
|
||||
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)
|
||||
|
||||
|
||||
class AllUserAuth(AuthValidation):
|
||||
|
||||
"""
|
||||
支持所有用户认证
|
||||
获取用户基本信息
|
||||
"""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
request: Request,
|
||||
token: str = Depends(settings.oauth2_scheme),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
):
|
||||
"""
|
||||
每次调用依赖此类的接口会执行该方法
|
||||
"""
|
||||
if not settings.OAUTH_ENABLE:
|
||||
return Auth(db=db)
|
||||
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)
|
||||
|
||||
|
||||
class FullAdminAuth(AuthValidation):
|
||||
|
||||
"""
|
||||
只支持员工用户认证
|
||||
获取员工用户完整信息
|
||||
如果有权限,那么会验证该用户是否包括权限列表中的其中一个权限
|
||||
"""
|
||||
|
||||
def __init__(self, permissions: list[str] | None = None):
|
||||
if permissions:
|
||||
self.permissions = set(permissions)
|
||||
else:
|
||||
self.permissions = None
|
||||
|
||||
async def __call__(
|
||||
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, password = self.validate_token(request, token)
|
||||
options = [
|
||||
joinedload(VadminUser.roles).subqueryload(VadminRole.menus),
|
||||
joinedload(VadminUser.roles).subqueryload(VadminRole.depts),
|
||||
joinedload(VadminUser.depts)
|
||||
]
|
||||
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
|
180
apps/vadmin/auth/utils/login.py
Normal file
180
apps/vadmin/auth/utils/login.py
Normal file
@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Create Time : 2021/10/24 16:44
|
||||
# @File : views.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 安全认证视图
|
||||
|
||||
"""
|
||||
JWT 表示 「JSON Web Tokens」。https://jwt.io/
|
||||
|
||||
它是一个将 JSON 对象编码为密集且没有空格的长字符串的标准。
|
||||
|
||||
通过这种方式,你可以创建一个有效期为 1 周的令牌。然后当用户第二天使用令牌重新访问时,你知道该用户仍然处于登入状态。
|
||||
一周后令牌将会过期,用户将不会通过认证,必须再次登录才能获得一个新令牌。
|
||||
|
||||
我们需要安装 python-jose 以在 Python 中生成和校验 JWT 令牌:pip install python-jose[cryptography]
|
||||
|
||||
PassLib 是一个用于处理哈希密码的很棒的 Python 包。它支持许多安全哈希算法以及配合算法使用的实用程序。
|
||||
推荐的算法是 「Bcrypt」:pip install passlib[bcrypt]
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
from redis.asyncio import Redis
|
||||
from fastapi import APIRouter, Depends, Request, Body
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from core.database import db_getter, redis_getter
|
||||
from core.exception import CustomException
|
||||
from utils import status
|
||||
from utils.response import SuccessResponse, ErrorResponse
|
||||
from application import settings
|
||||
from .login_manage import LoginManage
|
||||
from .validation import LoginForm, WXLoginForm
|
||||
from apps.vadmin.record.models import VadminLoginRecord
|
||||
from apps.vadmin.auth.crud import MenuDal, UserDal
|
||||
from apps.vadmin.auth.models import VadminUser
|
||||
from .current import FullAdminAuth
|
||||
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)
|
||||
):
|
||||
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=error_code, code=error_code, msg="该手机号不存在")
|
||||
result = VadminUser.verify_password(data.password, user.password)
|
||||
if not result:
|
||||
raise CustomException(status_code=error_code, code=error_code, msg="手机号或密码错误")
|
||||
if not user.is_active:
|
||||
raise CustomException(status_code=error_code, code=error_code, msg="此手机号已被冻结")
|
||||
elif not user.is_staff:
|
||||
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)
|
||||
return resp
|
||||
|
||||
|
||||
@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)
|
||||
):
|
||||
try:
|
||||
result = await manage.password_login(data, db, request)
|
||||
|
||||
if not result.status:
|
||||
raise ValueError(result.msg)
|
||||
|
||||
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(
|
||||
payload={"sub": result.user.telephone, "is_refresh": True, "password": result.user.password},
|
||||
expires=expires
|
||||
)
|
||||
resp = {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "bearer",
|
||||
"is_reset_password": result.user.is_reset_password,
|
||||
"is_wx_server_openid": result.user.is_wx_server_openid
|
||||
}
|
||||
await VadminLoginRecord.create_login_record(db, data, True, request, resp)
|
||||
return SuccessResponse(resp)
|
||||
except ValueError as e:
|
||||
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": str(e)})
|
||||
return ErrorResponse(msg=str(e))
|
||||
|
||||
|
||||
@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)
|
||||
):
|
||||
try:
|
||||
if data.platform != "1" or data.method != "2":
|
||||
raise ValueError("无效参数")
|
||||
wx = WXOAuth(rd, 0)
|
||||
telephone = await wx.parsing_phone_number(data.code)
|
||||
if not telephone:
|
||||
raise ValueError("无效Code")
|
||||
data.telephone = telephone
|
||||
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
|
||||
if not user:
|
||||
raise ValueError("手机号不存在")
|
||||
elif not user.is_active:
|
||||
raise ValueError("手机号已被冻结")
|
||||
except ValueError as e:
|
||||
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": str(e)})
|
||||
return ErrorResponse(msg=str(e))
|
||||
|
||||
# 更新登录时间
|
||||
await UserDal(db).update_login_info(user, request.client.host)
|
||||
|
||||
# 登录成功创建 token
|
||||
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(
|
||||
payload={"sub": user.telephone, "is_refresh": True, "password": user.password},
|
||||
expires=expires
|
||||
)
|
||||
resp = {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "bearer",
|
||||
"is_reset_password": user.is_reset_password,
|
||||
"is_wx_server_openid": user.is_wx_server_openid
|
||||
}
|
||||
await VadminLoginRecord.create_login_record(db, data, True, request, resp)
|
||||
return SuccessResponse(resp)
|
||||
|
||||
|
||||
@app.get("/getMenuList", summary="获取当前用户菜单树")
|
||||
async def get_menu_list(auth: Auth = Depends(FullAdminAuth())):
|
||||
return SuccessResponse(await MenuDal(auth.db).get_routers(auth.user))
|
||||
|
||||
|
||||
@app.post("/token/refresh", summary="刷新Token")
|
||||
async def token_refresh(refresh: str = Body(..., title="刷新Token")):
|
||||
error_code = status.HTTP_401_UNAUTHORIZED
|
||||
try:
|
||||
payload = jwt.decode(refresh, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
telephone: str = payload.get("sub")
|
||||
is_refresh: bool = payload.get("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, "password": password})
|
||||
expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
||||
refresh_token = LoginManage.create_token(
|
||||
payload={"sub": telephone, "is_refresh": True, "password": password},
|
||||
expires=expires
|
||||
)
|
||||
resp = {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "bearer"
|
||||
}
|
||||
return SuccessResponse(resp)
|
62
apps/vadmin/auth/utils/login_manage.py
Normal file
62
apps/vadmin/auth/utils/login_manage.py
Normal file
@ -0,0 +1,62 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Create Time : 2022/8/8 11:02
|
||||
# @File : auth_util.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 简要说明
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import Request
|
||||
from application import settings
|
||||
import jwt
|
||||
from apps.vadmin.auth import models
|
||||
from core.database import redis_getter
|
||||
from utils.sms.code import CodeSMS
|
||||
from .validation import LoginValidation, LoginForm, LoginResult
|
||||
|
||||
|
||||
class LoginManage:
|
||||
"""
|
||||
登录认证工具
|
||||
"""
|
||||
|
||||
@LoginValidation
|
||||
async def password_login(self, data: LoginForm, user: models.VadminUser, **kwargs) -> LoginResult:
|
||||
"""
|
||||
验证用户密码
|
||||
"""
|
||||
result = models.VadminUser.verify_password(data.password, user.password)
|
||||
if result:
|
||||
return LoginResult(status=True, msg="验证成功")
|
||||
return LoginResult(status=False, msg="手机号或密码错误")
|
||||
|
||||
@LoginValidation
|
||||
async def sms_login(self, data: LoginForm, request: Request, **kwargs) -> LoginResult:
|
||||
"""
|
||||
验证用户短信验证码
|
||||
"""
|
||||
rd = redis_getter(request)
|
||||
sms = CodeSMS(data.telephone, rd)
|
||||
result = await sms.check_sms_code(data.password)
|
||||
if result:
|
||||
return LoginResult(status=True, msg="验证成功")
|
||||
return LoginResult(status=False, msg="验证码错误")
|
||||
|
||||
@staticmethod
|
||||
def create_token(payload: dict, expires: timedelta = None):
|
||||
"""
|
||||
创建一个生成新的访问令牌的工具函数。
|
||||
|
||||
pyjwt:https://github.com/jpadilla/pyjwt/blob/master/docs/usage.rst
|
||||
jwt 博客:https://geek-docs.com/python/python-tutorial/j_python-jwt.html
|
||||
|
||||
#TODO 传入的时间为UTC时间datetime.datetime类型,但是在解码时获取到的是本机时间的时间戳
|
||||
"""
|
||||
if expires:
|
||||
expire = datetime.utcnow() + expires
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
payload.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
10
apps/vadmin/auth/utils/validation/__init__.py
Normal file
10
apps/vadmin/auth/utils/validation/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Create Time : 2022/11/9 10:14
|
||||
# @File : __init__.py.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 简要说明
|
||||
|
||||
from .auth import Auth, AuthValidation
|
||||
from .login import LoginValidation, LoginForm, LoginResult, WXLoginForm
|
159
apps/vadmin/auth/utils/validation/auth.py
Normal file
159
apps/vadmin/auth/utils/validation/auth.py
Normal file
@ -0,0 +1,159 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Create Time : 2021/10/24 16:44
|
||||
# @File : auth.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 用户凭证验证装饰器
|
||||
|
||||
from fastapi import Request
|
||||
import jwt
|
||||
from pydantic import BaseModel
|
||||
from application import settings
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from apps.vadmin.auth.models import VadminUser
|
||||
from core.exception import CustomException
|
||||
from utils import status
|
||||
from datetime import timedelta, datetime
|
||||
from apps.vadmin.auth.crud import UserDal
|
||||
|
||||
|
||||
class Auth(BaseModel):
|
||||
user: VadminUser = None
|
||||
db: AsyncSession
|
||||
data_range: int | None = None
|
||||
dept_ids: list | None = []
|
||||
|
||||
class Config:
|
||||
# 接收任意类型
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
class AuthValidation:
|
||||
"""
|
||||
用于用户每次调用接口时,验证用户提交的token是否正确,并从token中获取用户信息
|
||||
"""
|
||||
|
||||
# status_code = 401 时,表示强制要求重新登录,因账号已冻结,账号已过期,手机号码错误,刷新token无效等问题导致
|
||||
# 只有 code = 401 时,表示 token 过期,要求刷新 token
|
||||
# 只有 code = 错误值时,只是报错,不重新登陆
|
||||
error_code = status.HTTP_401_UNAUTHORIZED
|
||||
warning_code = status.HTTP_ERROR
|
||||
|
||||
# status_code = 403 时,表示强制要求重新登录,因无系统权限,而进入到系统访问等问题导致
|
||||
|
||||
@classmethod
|
||||
def validate_token(cls, request: Request, token: str | None) -> tuple[str, bool]:
|
||||
"""
|
||||
验证用户 token
|
||||
"""
|
||||
if not token:
|
||||
raise CustomException(
|
||||
msg="请您先登录!",
|
||||
code=status.HTTP_403_FORBIDDEN,
|
||||
status_code=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
telephone: str = payload.get("sub")
|
||||
exp: int = payload.get("exp")
|
||||
is_refresh: bool = payload.get("is_refresh")
|
||||
password: bool = payload.get("password")
|
||||
if not telephone or is_refresh or not password:
|
||||
raise CustomException(
|
||||
msg="未认证,请您重新登录",
|
||||
code=status.HTTP_403_FORBIDDEN,
|
||||
status_code=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
# 计算当前时间 + 缓冲时间是否大于等于 JWT 过期时间
|
||||
buffer_time = (datetime.now() + timedelta(minutes=settings.ACCESS_TOKEN_CACHE_MINUTES)).timestamp()
|
||||
# print("过期时间", exp, datetime.fromtimestamp(exp))
|
||||
# print("当前时间", buffer_time, datetime.fromtimestamp(buffer_time))
|
||||
# print("剩余时间", exp - buffer_time)
|
||||
if buffer_time >= exp:
|
||||
request.scope["if-refresh"] = 1
|
||||
else:
|
||||
request.scope["if-refresh"] = 0
|
||||
except (jwt.exceptions.InvalidSignatureError, jwt.exceptions.DecodeError):
|
||||
raise CustomException(
|
||||
msg="无效认证,请您重新登录",
|
||||
code=status.HTTP_403_FORBIDDEN,
|
||||
status_code=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
except jwt.exceptions.ExpiredSignatureError:
|
||||
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:
|
||||
"""
|
||||
验证用户信息
|
||||
:param request:
|
||||
:param user:
|
||||
:param db:
|
||||
:param is_all: 是否所有人访问,不加权限
|
||||
:return:
|
||||
"""
|
||||
if user is None:
|
||||
raise CustomException(msg="未认证,请您重新登陆", code=cls.error_code, status_code=cls.error_code)
|
||||
elif not user.is_active:
|
||||
raise CustomException(msg="用户已被冻结!", code=cls.error_code, status_code=cls.error_code)
|
||||
request.scope["telephone"] = user.telephone
|
||||
request.scope["user_id"] = user.id
|
||||
request.scope["user_name"] = user.name
|
||||
try:
|
||||
request.scope["body"] = await request.body()
|
||||
except RuntimeError:
|
||||
request.scope["body"] = "获取失败"
|
||||
if is_all:
|
||||
return Auth(user=user, db=db)
|
||||
data_range, dept_ids = await cls.get_user_data_range(user, db)
|
||||
return Auth(user=user, db=db, data_range=data_range, dept_ids=dept_ids)
|
||||
|
||||
@classmethod
|
||||
def get_user_permissions(cls, user: VadminUser) -> set:
|
||||
"""
|
||||
获取员工用户所有权限列表
|
||||
:param user: 用户实例
|
||||
:return:
|
||||
"""
|
||||
if user.is_admin():
|
||||
return {'*.*.*'}
|
||||
permissions = set()
|
||||
for role_obj in user.roles:
|
||||
for menu in role_obj.menus:
|
||||
if menu.perms and not menu.disabled:
|
||||
permissions.add(menu.perms)
|
||||
return permissions
|
||||
|
||||
@classmethod
|
||||
async def get_user_data_range(cls, user: VadminUser, db: AsyncSession) -> tuple:
|
||||
"""
|
||||
获取用户数据范围
|
||||
0 仅本人数据权限 create_user_id 查询
|
||||
1 本部门数据权限 部门 id 左连接查询
|
||||
2 本部门及以下数据权限 部门 id 左连接查询
|
||||
3 自定义数据权限 部门 id 左连接查询
|
||||
4 全部数据权限 无
|
||||
:param user:
|
||||
:param db:
|
||||
:return:
|
||||
"""
|
||||
if user.is_admin():
|
||||
return 4, ["*"]
|
||||
data_range = max([i.data_range for i in user.roles])
|
||||
dept_ids = set()
|
||||
if data_range == 0:
|
||||
pass
|
||||
elif data_range == 1:
|
||||
for dept in user.depts:
|
||||
dept_ids.add(dept.id)
|
||||
elif data_range == 2:
|
||||
# 递归获取部门列表
|
||||
dept_ids = await UserDal(db).recursion_get_dept_ids(user)
|
||||
elif data_range == 3:
|
||||
for role_obj in user.roles:
|
||||
for dept in role_obj.depts:
|
||||
dept_ids.add(dept.id)
|
||||
elif data_range == 4:
|
||||
dept_ids.add("*")
|
||||
return data_range, list(dept_ids)
|
92
apps/vadmin/auth/utils/validation/login.py
Normal file
92
apps/vadmin/auth/utils/validation/login.py
Normal file
@ -0,0 +1,92 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Create Time : 2022/11/9 10:15
|
||||
# @File : login.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 登录验证装饰器
|
||||
|
||||
from fastapi import Request
|
||||
from pydantic import BaseModel, field_validator
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from application.settings import DEFAULT_AUTH_ERROR_MAX_NUMBER, DEMO, REDIS_DB_ENABLE
|
||||
from apps.vadmin.auth import crud, schemas
|
||||
from core.database import redis_getter
|
||||
from core.validator import vali_telephone
|
||||
from utils.count import Count
|
||||
|
||||
|
||||
class LoginForm(BaseModel):
|
||||
telephone: str
|
||||
password: str
|
||||
method: str = '0' # 认证方式,0:密码登录,1:短信登录,2:微信一键登录
|
||||
platform: str = '0' # 登录平台,0:PC端管理系统,1:移动端管理系统
|
||||
|
||||
# 重用验证器:https://docs.pydantic.dev/dev-v2/usage/validators/#reuse-validators
|
||||
normalize_telephone = field_validator('telephone')(vali_telephone)
|
||||
|
||||
|
||||
class WXLoginForm(BaseModel):
|
||||
telephone: str | None = None
|
||||
code: str
|
||||
method: str = '2' # 认证方式,0:密码登录,1:短信登录,2:微信一键登录
|
||||
platform: str = '1' # 登录平台,0:PC端管理系统,1:移动端管理系统
|
||||
|
||||
|
||||
class LoginResult(BaseModel):
|
||||
status: bool | None = False
|
||||
user: schemas.UserPasswordOut | None = None
|
||||
msg: str | None = None
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
class LoginValidation:
|
||||
|
||||
"""
|
||||
验证用户登录时提交的数据是否有效
|
||||
"""
|
||||
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
async def __call__(self, data: LoginForm, db: AsyncSession, request: Request) -> LoginResult:
|
||||
self.result = LoginResult()
|
||||
if data.platform not in ["0", "1"] or data.method not in ["0", "1"]:
|
||||
self.result.msg = "无效参数"
|
||||
return self.result
|
||||
user = await crud.UserDal(db).get_data(telephone=data.telephone, v_return_none=True)
|
||||
if not user:
|
||||
self.result.msg = "该手机号不存在!"
|
||||
return self.result
|
||||
|
||||
result = await self.func(self, data=data, user=user, request=request)
|
||||
|
||||
if REDIS_DB_ENABLE:
|
||||
count_key = f"{data.telephone}_password_auth" if data.method == '0' else f"{data.telephone}_sms_auth"
|
||||
count = Count(redis_getter(request), count_key)
|
||||
else:
|
||||
count = None
|
||||
|
||||
if not result.status:
|
||||
self.result.msg = result.msg
|
||||
if not DEMO and count:
|
||||
number = await count.add(ex=86400)
|
||||
if number >= DEFAULT_AUTH_ERROR_MAX_NUMBER:
|
||||
await count.reset()
|
||||
# 如果等于最大次数,那么就将用户 is_active=False
|
||||
user.is_active = False
|
||||
await db.flush()
|
||||
elif not user.is_active:
|
||||
self.result.msg = "此手机号已被冻结!"
|
||||
elif data.platform in ["0", "1"] and not user.is_staff:
|
||||
self.result.msg = "此手机号无权限!"
|
||||
else:
|
||||
if not DEMO and count:
|
||||
await count.delete()
|
||||
self.result.msg = "OK"
|
||||
self.result.status = True
|
||||
self.result.user = schemas.UserPasswordOut.model_validate(user)
|
||||
await crud.UserDal(db).update_login_info(user, request.client.host)
|
||||
return self.result
|
Reference in New Issue
Block a user