diff --git a/app/database/email_verification.py b/app/database/email_verification.py index baf053b..46a55b9 100644 --- a/app/database/email_verification.py +++ b/app/database/email_verification.py @@ -35,7 +35,7 @@ class LoginSession(SQLModel, table=True): user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), nullable=False, index=True)) session_token: str = Field(unique=True, index=True) # 会话令牌 ip_address: str = Field() # 登录IP - user_agent: str | None = Field(default=None) + user_agent: str | None = Field(default=None, max_length=250) country_code: str | None = Field(default=None) is_verified: bool = Field(default=False) # 是否已验证 created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) diff --git a/app/service/email_verification_service.py b/app/service/email_verification_service.py index 5207fa9..f37cffe 100644 --- a/app/service/email_verification_service.py +++ b/app/service/email_verification_service.py @@ -388,13 +388,18 @@ class LoginSessionService: is_new_location: bool = False ) -> LoginSession: """创建登录会话""" + from app.utils import simplify_user_agent + session_token = EmailVerificationService.generate_session_token() + # 简化 User-Agent 字符串 + simplified_user_agent = simplify_user_agent(user_agent, max_length=250) + session = LoginSession( user_id=user_id, session_token=session_token, ip_address=ip_address, - user_agent=user_agent, + user_agent=simplified_user_agent, country_code=country_code, is_new_location=is_new_location, expires_at=datetime.now(UTC) + timedelta(hours=24), # 24小时过期 diff --git a/app/service/login_log_service.py b/app/service/login_log_service.py index 74f6171..8f2298d 100644 --- a/app/service/login_log_service.py +++ b/app/service/login_log_service.py @@ -45,8 +45,10 @@ class LoginLogService: raw_ip = get_client_ip(request) ip_address = normalize_ip(raw_ip) - # 获取User-Agent - user_agent = request.headers.get("User-Agent", "") + # 获取并简化User-Agent + from app.utils import simplify_user_agent + raw_user_agent = request.headers.get("User-Agent", "") + user_agent = simplify_user_agent(raw_user_agent, max_length=500) # 创建基本的登录记录 login_log = UserLoginLog( diff --git a/app/utils.py b/app/utils.py index b4cc162..031b0cc 100644 --- a/app/utils.py +++ b/app/utils.py @@ -146,3 +146,80 @@ def check_image(content: bytes, size: int, width: int, height: int) -> None: ) except Exception as e: raise HTTPException(status_code=400, detail=f"Error processing image: {e}") + + +def simplify_user_agent(user_agent: str | None, max_length: int = 200) -> str | None: + """ + 简化 User-Agent 字符串,只保留 osu! 和关键设备系统信息浏览器 + + Args: + user_agent: 原始 User-Agent 字符串 + max_length: 最大长度限制 + + Returns: + 简化后的 User-Agent 字符串,或 None + """ + import re + + if not user_agent: + return None + + # 如果长度在限制内,直接返回 + if len(user_agent) <= max_length: + return user_agent + + # 提取操作系统信息 + os_info = "" + os_patterns = [ + r'(Windows[^;)]*)', + r'(Mac OS[^;)]*)', + r'(Linux[^;)]*)', + r'(Android[^;)]*)', + r'(iOS[^;)]*)', + r'(iPhone[^;)]*)', + r'(iPad[^;)]*)' + ] + + for pattern in os_patterns: + match = re.search(pattern, user_agent, re.IGNORECASE) + if match: + os_info = match.group(1).strip() + break + + # 提取浏览器信息 + browser_info = "" + browser_patterns = [ + r'(osu![^)]*)', # osu! 客户端 + r'(Chrome/[\d.]+)', + r'(Firefox/[\d.]+)', + r'(Safari/[\d.]+)', + r'(Edge/[\d.]+)', + r'(Opera/[\d.]+)' + ] + + for pattern in browser_patterns: + match = re.search(pattern, user_agent, re.IGNORECASE) + if match: + browser_info = match.group(1).strip() + # 如果找到了 osu! 客户端,优先使用 + if 'osu!' in browser_info.lower(): + break + + # 构建简化的 User-Agent + parts = [] + if os_info: + parts.append(os_info) + if browser_info: + parts.append(browser_info) + + if parts: + simplified = '; '.join(parts) + else: + # 如果没有识别到关键信息,截断原始字符串 + simplified = user_agent[:max_length-3] + "..." + + # 确保不超过最大长度 + if len(simplified) > max_length: + simplified = simplified[:max_length-3] + "..." + + return simplified diff --git a/migrations/versions/5b76689f6e4b_increase_the_length_limit_of_the_user_.py b/migrations/versions/5b76689f6e4b_increase_the_length_limit_of_the_user_.py new file mode 100644 index 0000000..8a26293 --- /dev/null +++ b/migrations/versions/5b76689f6e4b_increase_the_length_limit_of_the_user_.py @@ -0,0 +1,40 @@ +"""Increase the length limit of the user_agent field in the login_sessions table + +Revision ID: 5b76689f6e4b +Revises: 65e7dc8d5905 +Create Date: 2025-08-22 15:14:59.242274 + +""" +from __future__ import annotations + +from collections.abc import Sequence + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision: str = "5b76689f6e4b" +down_revision: str | Sequence[str] | None = "65e7dc8d5905" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("login_sessions", "user_agent", + existing_type=mysql.VARCHAR(length=255), + type_=sa.String(length=250), + existing_nullable=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("login_sessions", "user_agent", + existing_type=sa.String(length=250), + type_=mysql.VARCHAR(length=255), + existing_nullable=True) + # ### end Alembic commands ###