Skip to content

Add login graphic captcha #124

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/app/api/v1/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.api.v1.auth.auth import router as auth_router
from backend.app.api.v1.auth.captcha import router as captcha_router

router = APIRouter(prefix='/auth', tags=['认证'])

router.include_router(auth_router)
router.include_router(captcha_router)
4 changes: 2 additions & 2 deletions backend/app/api/v1/auth/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.response.response_schema import response_base
from backend.app.schemas.token import GetLoginToken, GetSwaggerToken, GetNewToken
from backend.app.schemas.user import Auth
from backend.app.schemas.user import AuthLogin
from backend.app.services.auth_service import AuthService

router = APIRouter()
Expand All @@ -28,7 +28,7 @@ async def swagger_user_login(form_data: OAuth2PasswordRequestForm = Depends()) -
description='json 格式登录, 仅支持在第三方api工具调试接口, 例如: postman',
dependencies=[Depends(RateLimiter(times=5, minutes=15))],
)
async def user_login(request: Request, obj: Auth, background_tasks: BackgroundTasks):
async def user_login(request: Request, obj: AuthLogin, background_tasks: BackgroundTasks):
access_token, refresh_token, access_expire, refresh_expire, user = await AuthService().login(
request=request, obj=obj, background_tasks=background_tasks
)
Expand Down
30 changes: 30 additions & 0 deletions backend/app/api/v1/auth/captcha.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fast_captcha import img_captcha
from fastapi import APIRouter, Depends, Request
from fastapi_limiter.depends import RateLimiter
from starlette.concurrency import run_in_threadpool

from backend.app.common.redis import redis_client
from backend.app.common.response.response_schema import response_base
from backend.app.core.conf import settings

router = APIRouter()


@router.get(
'/captcha',
summary='获取登录验证码',
dependencies=[Depends(RateLimiter(times=5, seconds=10))],
)
async def get_captcha(request: Request):
"""
此接口可能存在性能损耗,尽管是异步接口,但是验证码生成是同步IO事件,使用线程池处理尽量减少性能损耗
"""
img_type: str = 'base64'
img, code = await run_in_threadpool(img_captcha, img_byte=img_type)
ip = request.state.ip
await redis_client.set(
f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{ip}', code, ex=settings.CAPTCHA_LOGIN_EXPIRE_SECONDS
)
return await response_base.success(data={'image_type': img_type, 'image': img})
8 changes: 3 additions & 5 deletions backend/app/api/v1/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

router = APIRouter()


@router.get('/{pk}', summary='获取目录详情', dependencies=[DependsJwtAuth])
async def get_menu(pk: int):
menu = await MenuService.get(pk=pk)
Expand All @@ -22,8 +23,8 @@ async def get_menu(pk: int):

@router.get('', summary='获取所有目录展示树', dependencies=[DependsJwtAuth])
async def get_all_menus(
name: Annotated[str | None, Query()] = None,
status: Annotated[bool | None, Query()] = None,
name: Annotated[str | None, Query()] = None,
status: Annotated[bool | None, Query()] = None,
):
menu = await MenuService.get_select(name=name, status=status)
return await response_base.success(data=menu)
Expand All @@ -49,6 +50,3 @@ async def delete_menu(pk: int):
if count > 0:
return await response_base.success()
return await response_base.fail()



4 changes: 4 additions & 0 deletions backend/app/core/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ def validator_api_url(cls, values):
TOKEN_REDIS_PREFIX: str = 'fba_token'
TOKEN_REFRESH_REDIS_PREFIX: str = 'fba_refresh_token'

# captcha
CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba_login_captcha'
CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 过期时间,单位:秒

# Log
LOG_STDOUT_FILENAME: str = 'fba_access.log'
LOG_STDERR_FILENAME: str = 'fba_error.log'
Expand Down
2 changes: 1 addition & 1 deletion backend/app/crud/crud_dept.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
from typing import Any

from sqlalchemy import select, desc, and_, asc
from sqlalchemy import select, and_, asc
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

Expand Down
2 changes: 1 addition & 1 deletion backend/app/schemas/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

class RoleBase(BaseModel):
name: str
data_scope: int | None = Field(default=RoleDataScope.custom, description='数据范围(1:全部数据权限 2:自定数据权限)')
data_scope: int | None = Field(default=RoleDataScope.custom, description='数据范围(1:全部数据权限 2:自定数据权限)') # noqa: E501
status: bool
remark: str | None = None

Expand Down
4 changes: 4 additions & 0 deletions backend/app/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class Auth(BaseModel):
password: str


class AuthLogin(Auth):
captcha: str


class CreateUser(Auth):
dept_id: int
roles: list[int]
Expand Down
12 changes: 9 additions & 3 deletions backend/app/services/auth_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
from backend.app.common.exception import errors
from backend.app.common.jwt import get_token
from backend.app.common.redis import redis_client
from backend.app.common.response.response_code import CustomCode
from backend.app.core.conf import settings
from backend.app.crud.crud_user import UserDao
from backend.app.database.db_mysql import async_db_session
from backend.app.schemas.user import Auth
from backend.app.schemas.user import AuthLogin
from backend.app.services.login_log_service import LoginLogService


Expand All @@ -40,7 +41,7 @@ async def swagger_login(self, *, form_data: OAuth2PasswordRequestForm):
access_token, _ = await jwt.create_access_token(str(user.id), multi_login=user.is_multi_login)
return access_token, user

async def login(self, *, request: Request, obj: Auth, background_tasks: BackgroundTasks):
async def login(self, *, request: Request, obj: AuthLogin, background_tasks: BackgroundTasks):
async with async_db_session() as db:
try:
current_user = await UserDao.get_by_username(db, obj.username)
Expand All @@ -50,6 +51,11 @@ async def login(self, *, request: Request, obj: Auth, background_tasks: Backgrou
raise errors.AuthorizationError(msg='密码错误')
elif not current_user.is_active:
raise errors.AuthorizationError(msg='用户已锁定, 登陆失败')
captcha_code = await redis_client.get(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
if not captcha_code:
raise errors.AuthorizationError(msg='验证码失效,请重新获取')
if captcha_code.lower() != obj.captcha.lower():
raise errors.CustomError(error=CustomCode.CAPTCHA_ERROR)
await UserDao.update_login_time(db, obj.username, self.login_time)
user = await UserDao.get(db, current_user.id)
access_token, access_token_expire_time = await jwt.create_access_token(
Expand All @@ -60,7 +66,7 @@ async def login(self, *, request: Request, obj: Auth, background_tasks: Backgrou
)
except errors.NotFoundError as e:
raise errors.NotFoundError(msg=e.msg)
except errors.AuthorizationError as e:
except (errors.AuthorizationError, errors.CustomError) as e:
err_log_info = dict(
db=db,
request=request,
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ casbin_async_sqlalchemy_adapter==1.1.0
cryptography==41.0.0
email-validator==1.1.3
Faker==9.7.1
fast-captcha==0.1.3
fast-captcha==0.2.1
fastapi==0.95.2
fastapi-limiter==0.1.5
fastapi-pagination==0.12.1
Expand Down