Skip to content

Commit f8b48e4

Browse files
authored
Add login graphic captcha (#124)
* add the get verification code interface * add the login interface captcha * clean up the debugging code
1 parent 0bb6275 commit f8b48e4

File tree

10 files changed

+57
-13
lines changed

10 files changed

+57
-13
lines changed

backend/app/api/v1/auth/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
# -*- coding: utf-8 -*-
33
from fastapi import APIRouter
44
from backend.app.api.v1.auth.auth import router as auth_router
5+
from backend.app.api.v1.auth.captcha import router as captcha_router
56

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

89
router.include_router(auth_router)
10+
router.include_router(captcha_router)

backend/app/api/v1/auth/auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from backend.app.common.jwt import DependsJwtAuth
1111
from backend.app.common.response.response_schema import response_base
1212
from backend.app.schemas.token import GetLoginToken, GetSwaggerToken, GetNewToken
13-
from backend.app.schemas.user import Auth
13+
from backend.app.schemas.user import AuthLogin
1414
from backend.app.services.auth_service import AuthService
1515

1616
router = APIRouter()
@@ -28,7 +28,7 @@ async def swagger_user_login(form_data: OAuth2PasswordRequestForm = Depends()) -
2828
description='json 格式登录, 仅支持在第三方api工具调试接口, 例如: postman',
2929
dependencies=[Depends(RateLimiter(times=5, minutes=15))],
3030
)
31-
async def user_login(request: Request, obj: Auth, background_tasks: BackgroundTasks):
31+
async def user_login(request: Request, obj: AuthLogin, background_tasks: BackgroundTasks):
3232
access_token, refresh_token, access_expire, refresh_expire, user = await AuthService().login(
3333
request=request, obj=obj, background_tasks=background_tasks
3434
)

backend/app/api/v1/auth/captcha.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
from fast_captcha import img_captcha
4+
from fastapi import APIRouter, Depends, Request
5+
from fastapi_limiter.depends import RateLimiter
6+
from starlette.concurrency import run_in_threadpool
7+
8+
from backend.app.common.redis import redis_client
9+
from backend.app.common.response.response_schema import response_base
10+
from backend.app.core.conf import settings
11+
12+
router = APIRouter()
13+
14+
15+
@router.get(
16+
'/captcha',
17+
summary='获取登录验证码',
18+
dependencies=[Depends(RateLimiter(times=5, seconds=10))],
19+
)
20+
async def get_captcha(request: Request):
21+
"""
22+
此接口可能存在性能损耗,尽管是异步接口,但是验证码生成是同步IO事件,使用线程池处理尽量减少性能损耗
23+
"""
24+
img_type: str = 'base64'
25+
img, code = await run_in_threadpool(img_captcha, img_byte=img_type)
26+
ip = request.state.ip
27+
await redis_client.set(
28+
f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{ip}', code, ex=settings.CAPTCHA_LOGIN_EXPIRE_SECONDS
29+
)
30+
return await response_base.success(data={'image_type': img_type, 'image': img})

backend/app/api/v1/menu.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
router = APIRouter()
1515

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

2324
@router.get('', summary='获取所有目录展示树', dependencies=[DependsJwtAuth])
2425
async def get_all_menus(
25-
name: Annotated[str | None, Query()] = None,
26-
status: Annotated[bool | None, Query()] = None,
26+
name: Annotated[str | None, Query()] = None,
27+
status: Annotated[bool | None, Query()] = None,
2728
):
2829
menu = await MenuService.get_select(name=name, status=status)
2930
return await response_base.success(data=menu)
@@ -49,6 +50,3 @@ async def delete_menu(pk: int):
4950
if count > 0:
5051
return await response_base.success()
5152
return await response_base.fail()
52-
53-
54-

backend/app/core/conf.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ def validator_api_url(cls, values):
8787
TOKEN_REDIS_PREFIX: str = 'fba_token'
8888
TOKEN_REFRESH_REDIS_PREFIX: str = 'fba_refresh_token'
8989

90+
# captcha
91+
CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba_login_captcha'
92+
CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 过期时间,单位:秒
93+
9094
# Log
9195
LOG_STDOUT_FILENAME: str = 'fba_access.log'
9296
LOG_STDERR_FILENAME: str = 'fba_error.log'

backend/app/crud/crud_dept.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# -*- coding: utf-8 -*-
33
from typing import Any
44

5-
from sqlalchemy import select, desc, and_, asc
5+
from sqlalchemy import select, and_, asc
66
from sqlalchemy.ext.asyncio import AsyncSession
77
from sqlalchemy.orm import selectinload
88

backend/app/schemas/role.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

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

backend/app/schemas/user.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ class Auth(BaseModel):
1414
password: str
1515

1616

17+
class AuthLogin(Auth):
18+
captcha: str
19+
20+
1721
class CreateUser(Auth):
1822
dept_id: int
1923
roles: list[int]

backend/app/services/auth_service.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313
from backend.app.common.exception import errors
1414
from backend.app.common.jwt import get_token
1515
from backend.app.common.redis import redis_client
16+
from backend.app.common.response.response_code import CustomCode
1617
from backend.app.core.conf import settings
1718
from backend.app.crud.crud_user import UserDao
1819
from backend.app.database.db_mysql import async_db_session
19-
from backend.app.schemas.user import Auth
20+
from backend.app.schemas.user import AuthLogin
2021
from backend.app.services.login_log_service import LoginLogService
2122

2223

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

43-
async def login(self, *, request: Request, obj: Auth, background_tasks: BackgroundTasks):
44+
async def login(self, *, request: Request, obj: AuthLogin, background_tasks: BackgroundTasks):
4445
async with async_db_session() as db:
4546
try:
4647
current_user = await UserDao.get_by_username(db, obj.username)
@@ -50,6 +51,11 @@ async def login(self, *, request: Request, obj: Auth, background_tasks: Backgrou
5051
raise errors.AuthorizationError(msg='密码错误')
5152
elif not current_user.is_active:
5253
raise errors.AuthorizationError(msg='用户已锁定, 登陆失败')
54+
captcha_code = await redis_client.get(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
55+
if not captcha_code:
56+
raise errors.AuthorizationError(msg='验证码失效,请重新获取')
57+
if captcha_code.lower() != obj.captcha.lower():
58+
raise errors.CustomError(error=CustomCode.CAPTCHA_ERROR)
5359
await UserDao.update_login_time(db, obj.username, self.login_time)
5460
user = await UserDao.get(db, current_user.id)
5561
access_token, access_token_expire_time = await jwt.create_access_token(
@@ -60,7 +66,7 @@ async def login(self, *, request: Request, obj: Auth, background_tasks: Backgrou
6066
)
6167
except errors.NotFoundError as e:
6268
raise errors.NotFoundError(msg=e.msg)
63-
except errors.AuthorizationError as e:
69+
except (errors.AuthorizationError, errors.CustomError) as e:
6470
err_log_info = dict(
6571
db=db,
6672
request=request,

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ casbin_async_sqlalchemy_adapter==1.1.0
1010
cryptography==41.0.0
1111
email-validator==1.1.3
1212
Faker==9.7.1
13-
fast-captcha==0.1.3
13+
fast-captcha==0.2.1
1414
fastapi==0.95.2
1515
fastapi-limiter==0.1.5
1616
fastapi-pagination==0.12.1

0 commit comments

Comments
 (0)