diff --git a/backend/app/admin/crud/crud_user.py b/backend/app/admin/crud/crud_user.py index c6bf35e7..ee1c31d5 100644 --- a/backend/app/admin/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from fast_captcha import text_captcha +import bcrypt + from sqlalchemy import and_, desc, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -70,8 +71,8 @@ async def create(self, db: AsyncSession, obj: RegisterUserParam, *, social: bool :return: """ if not social: - salt = text_captcha(5) - obj.password = get_hash_password(f'{obj.password}{salt}') + salt = bcrypt.gensalt() + obj.password = get_hash_password(f'{obj.password}', salt) dict_obj = obj.model_dump() dict_obj.update({'is_staff': True, 'salt': salt}) else: @@ -88,8 +89,8 @@ async def add(self, db: AsyncSession, obj: AddUserParam) -> None: :param obj: :return: """ - salt = text_captcha(5) - obj.password = get_hash_password(f'{obj.password}{salt}') + salt = bcrypt.gensalt() + obj.password = get_hash_password(f'{obj.password}', salt) dict_obj = obj.model_dump(exclude={'roles'}) dict_obj.update({'salt': salt}) new_user = self.model(**dict_obj) diff --git a/backend/app/admin/model/sys_user.py b/backend/app/admin/model/sys_user.py index dab80193..298cb128 100644 --- a/backend/app/admin/model/sys_user.py +++ b/backend/app/admin/model/sys_user.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import Union -from sqlalchemy import ForeignKey, String +from sqlalchemy import VARBINARY, ForeignKey, String from sqlalchemy.orm import Mapped, mapped_column, relationship from backend.app.admin.model.sys_user_role import sys_user_role @@ -22,7 +22,7 @@ class User(Base): username: Mapped[str] = mapped_column(String(20), unique=True, index=True, comment='用户名') nickname: Mapped[str] = mapped_column(String(20), unique=True, comment='昵称') password: Mapped[str | None] = mapped_column(String(255), comment='密码') - salt: Mapped[str | None] = mapped_column(String(5), comment='加密盐') + salt: Mapped[bytes | None] = mapped_column(VARBINARY(255), comment='加密盐') email: Mapped[str] = mapped_column(String(50), unique=True, index=True, comment='邮箱') is_superuser: Mapped[bool] = mapped_column(default=False, comment='超级权限(0否 1是)') is_staff: Mapped[bool] = mapped_column(default=False, comment='后台管理登陆(0否 1是)') diff --git a/backend/app/admin/service/auth_service.py b/backend/app/admin/service/auth_service.py index 0925a860..2d1de966 100644 --- a/backend/app/admin/service/auth_service.py +++ b/backend/app/admin/service/auth_service.py @@ -34,7 +34,7 @@ async def swagger_login(*, obj: HTTPBasicCredentials) -> tuple[str, User]: current_user = await user_dao.get_by_username(db, obj.username) if not current_user: raise errors.NotFoundError(msg='用户名或密码有误') - elif not password_verify(f'{obj.password}{current_user.salt}', current_user.password): + elif not password_verify(f'{obj.password}', current_user.password): raise errors.AuthorizationError(msg='用户名或密码有误') elif not current_user.status: raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员') @@ -53,7 +53,7 @@ async def login( raise errors.NotFoundError(msg='用户名或密码有误') user_uuid = current_user.uuid username = current_user.username - if not password_verify(obj.password + current_user.salt, current_user.password): + if not password_verify(obj.password, current_user.password): raise errors.AuthorizationError(msg='用户名或密码有误') elif not current_user.status: raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员') diff --git a/backend/app/admin/service/user_service.py b/backend/app/admin/service/user_service.py index b8c988ca..5284c108 100644 --- a/backend/app/admin/service/user_service.py +++ b/backend/app/admin/service/user_service.py @@ -71,13 +71,13 @@ async def add(*, request: Request, obj: AddUserParam) -> None: async def pwd_reset(*, request: Request, obj: ResetPasswordParam) -> int: async with async_db_session.begin() as db: user = await user_dao.get(db, request.user.id) - if not password_verify(f'{obj.old_password}{user.salt}', user.password): + if not password_verify(f'{obj.old_password}', user.password): raise errors.ForbiddenError(msg='原密码错误') np1 = obj.new_password np2 = obj.confirm_password if np1 != np2: raise errors.ForbiddenError(msg='密码输入不一致') - new_pwd = get_hash_password(f'{obj.new_password}{user.salt}') + new_pwd = get_hash_password(f'{obj.new_password}', user.salt) count = await user_dao.reset_password(db, request.user.id, new_pwd) key_prefix = [ f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}', diff --git a/backend/common/security/jwt.py b/backend/common/security/jwt.py index abdf7cc9..b9fb80f5 100644 --- a/backend/common/security/jwt.py +++ b/backend/common/security/jwt.py @@ -6,7 +6,8 @@ from fastapi.security import HTTPBearer from fastapi.security.utils import get_authorization_scheme_param from jose import ExpiredSignatureError, JWTError, jwt -from passlib.context import CryptContext +from pwdlib import PasswordHash +from pwdlib.hashers.bcrypt import BcryptHasher from pydantic_core import from_json from sqlalchemy.ext.asyncio import AsyncSession @@ -20,21 +21,21 @@ from backend.utils.serializers import select_as_dict from backend.utils.timezone import timezone -pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') - - # JWT authorizes dependency injection DependsJwtAuth = Depends(HTTPBearer()) +password_hash = PasswordHash((BcryptHasher(),)) + -def get_hash_password(password: str) -> str: +def get_hash_password(password: str, salt: bytes | None) -> str: """ Encrypt passwords using the hash algorithm :param password: + :param salt: :return: """ - return pwd_context.hash(password) + return password_hash.hash(password, salt=salt) def password_verify(plain_password: str, hashed_password: str) -> bool: @@ -45,7 +46,7 @@ def password_verify(plain_password: str, hashed_password: str) -> bool: :param hashed_password: The hash ciphers to compare :return: """ - return pwd_context.verify(plain_password, hashed_password) + return password_hash.verify(plain_password, hashed_password) async def create_access_token(sub: str, multi_login: bool) -> AccessToken: diff --git a/backend/pyproject.toml b/backend/pyproject.toml index c21bad1a..e6e89590 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "itsdangerous>=2.2.0", "loguru>=0.7.2", "msgspec>=0.18.6", - "passlib>=1.7.4", + "pwdlib>=0.2.1", "path==17.0.0", "phonenumbers>=8.13.0", "psutil>=6.0.0", @@ -46,7 +46,7 @@ dependencies = [ # https://github.com/celery/celery/issues/7874 "celery-aio-pool==0.1.0rc7", "asgi-correlation-id>=4.3.3", - "python-socketio[asyncio]>=5.11.4", + "python-socketio>=5.11.4", ] [dependency-groups] diff --git a/backend/requirements.txt b/backend/requirements.txt index 913abcac..0e3b7c25 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -60,7 +60,6 @@ msgspec==0.18.6 nodeenv==1.9.1 orjson==3.10.7 packaging==24.1 -passlib==1.7.4 path==17.0.0 phonenumbers==8.13.27 pillow==10.4.0 @@ -70,6 +69,7 @@ pre-commit==4.0.1 prometheus-client==0.21.0 prompt-toolkit==3.0.48 psutil==6.1.0 +pwdlib==0.2.1 pyasn1==0.6.1 pycparser==2.22 ; platform_python_implementation != 'PyPy' pydantic==2.9.1 diff --git a/backend/uv.lock b/backend/uv.lock index 9f8fca81..c0086a20 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -534,10 +534,10 @@ dependencies = [ { name = "jinja2" }, { name = "loguru" }, { name = "msgspec" }, - { name = "passlib" }, { name = "path" }, { name = "phonenumbers" }, { name = "psutil" }, + { name = "pwdlib" }, { name = "pydantic" }, { name = "python-jose" }, { name = "python-socketio" }, @@ -587,13 +587,13 @@ requires-dist = [ { name = "jinja2", specifier = ">=3.1.4" }, { name = "loguru", specifier = ">=0.7.2" }, { name = "msgspec", specifier = ">=0.18.6" }, - { name = "passlib", specifier = ">=1.7.4" }, { name = "path", specifier = "==17.0.0" }, { name = "phonenumbers", specifier = ">=8.13.0" }, { name = "psutil", specifier = ">=6.0.0" }, + { name = "pwdlib", specifier = ">=0.2.1" }, { name = "pydantic", specifier = ">=2.9.1" }, { name = "python-jose", specifier = ">=3.3.0" }, - { name = "python-socketio", extras = ["asyncio"], specifier = ">=5.11.4" }, + { name = "python-socketio", specifier = ">=5.11.4" }, { name = "redis", extras = ["hiredis"], specifier = ">=5.2.0" }, { name = "sqlalchemy", specifier = ">=2.0.30" }, { name = "sqlalchemy-crud-plus", specifier = "==1.6.0" }, @@ -1174,15 +1174,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/8d/c1e93296e109a320e508e38118cf7d1fc2a4d1c2ec64de78565b3c445eb5/pamqp-3.3.0-py2.py3-none-any.whl", hash = "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0", size = 33848 }, ] -[[package]] -name = "passlib" -version = "1.7.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554 }, -] - [[package]] name = "path" version = "17.0.0" @@ -1376,6 +1367,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/91/87fa6f060e649b1e1a7b19a4f5869709fbf750b7c8c262ee776ec32f3028/psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be", size = 254228 }, ] +[[package]] +name = "pwdlib" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/a0/9daed437a6226f632a25d98d65d60ba02bdafa920c90dcb6454c611ead6c/pwdlib-0.2.1.tar.gz", hash = "sha256:9a1d8a8fa09a2f7ebf208265e55d7d008103cbdc82b9e4902ffdd1ade91add5e", size = 11699 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/f3/0dae5078a486f0fdf4d4a1121e103bc42694a9da9bea7b0f2c63f29cfbd3/pwdlib-0.2.1-py3-none-any.whl", hash = "sha256:1823dc6f22eae472b540e889ecf57fd424051d6a4023ec0bcf7f0de2d9d7ef8c", size = 8082 }, +] + [[package]] name = "pyasn1" version = "0.6.1"