# -*- 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)