Files
hr-portal/backend/app/utils/password_generator.py
Porsche Chen 360533393f feat: HR Portal - Complete Multi-Tenant System with Redis Session Storage
Major Features:
-  Multi-tenant architecture (tenant isolation)
-  Employee CRUD with lifecycle management (onboarding/offboarding)
-  Department tree structure with email domain management
-  Company info management (single-record editing)
-  System functions CRUD (permission management)
-  Email account management (multi-account per employee)
-  Keycloak SSO integration (auth.lab.taipei)
-  Redis session storage (10.1.0.254:6379)
  - Solves Cookie 4KB limitation
  - Cross-system session sharing
  - Sliding expiration (8 hours)
  - Automatic token refresh

Technical Stack:
Backend:
- FastAPI + SQLAlchemy
- PostgreSQL 16 (10.1.0.20:5433)
- Keycloak Admin API integration
- Docker Mailserver integration (SSH)
- Alembic migrations

Frontend:
- Next.js 14 (App Router)
- NextAuth 4 with Keycloak Provider
- Redis session storage (ioredis)
- Tailwind CSS

Infrastructure:
- Redis 7 (10.1.0.254:6379) - Session + Cache
- Keycloak 26.1.0 (auth.lab.taipei)
- Docker Mailserver (10.1.0.254)

Architecture Highlights:
- Session管理由 Keycloak + Redis 統一控制
- 支援多系統 (HR/WebMail/Calendar/Drive/Office) 共享 session
- Token 自動刷新,異質服務整合
- 未來可無縫遷移到雲端

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-23 20:12:43 +08:00

212 lines
5.5 KiB
Python

"""
密碼產生與驗證工具
"""
import secrets
import string
import re
import bcrypt
def generate_secure_password(length: int = 16) -> str:
"""
產生安全的隨機密碼
Args:
length: 密碼長度(預設 16 字元)
Returns:
安全的隨機密碼
範例:
>>> pwd = generate_secure_password()
>>> len(pwd)
16
>>> validate_password_strength(pwd)
True
"""
if length < 8:
raise ValueError("密碼長度至少需要 8 個字元")
# 字元集合
lowercase = string.ascii_lowercase
uppercase = string.ascii_uppercase
digits = string.digits
special = "!@#$%^&*()-_=+[]{}|;:,.<>?"
# 確保至少包含每種類型各一個
password = [
secrets.choice(lowercase),
secrets.choice(uppercase),
secrets.choice(digits),
secrets.choice(special)
]
# 剩餘字元隨機選擇
all_chars = lowercase + uppercase + digits + special
password += [secrets.choice(all_chars) for _ in range(length - 4)]
# 打亂順序
secrets.SystemRandom().shuffle(password)
return ''.join(password)
def hash_password(password: str) -> str:
"""
使用 bcrypt 加密密碼
Args:
password: 明文密碼
Returns:
加密後的密碼 hash
"""
password_bytes = password.encode('utf-8')
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password_bytes, salt)
return hashed.decode('utf-8')
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
驗證密碼
Args:
plain_password: 明文密碼
hashed_password: 加密密碼
Returns:
是否匹配
"""
return bcrypt.checkpw(
plain_password.encode('utf-8'),
hashed_password.encode('utf-8')
)
def validate_password_strength(password: str) -> tuple[bool, list[str]]:
"""
驗證密碼強度
Args:
password: 待驗證的密碼
Returns:
(是否通過, 錯誤訊息列表)
範例:
>>> validate_password_strength("weak")
(False, ['密碼長度至少需要 8 個字元', ...])
>>> validate_password_strength("Strong@Pass123")
(True, [])
"""
errors = []
# 長度檢查
if len(password) < 8:
errors.append("密碼長度至少需要 8 個字元")
# 大寫字母
if not re.search(r'[A-Z]', password):
errors.append("密碼必須包含至少一個大寫字母")
# 小寫字母
if not re.search(r'[a-z]', password):
errors.append("密碼必須包含至少一個小寫字母")
# 數字
if not re.search(r'\d', password):
errors.append("密碼必須包含至少一個數字")
# 特殊符號
if not re.search(r'[!@#$%^&*()\-_=+\[\]{}|;:,.<>?]', password):
errors.append("密碼必須包含至少一個特殊符號")
# 常見弱密碼檢查
common_weak_passwords = [
'password', 'password123', '12345678', 'qwerty',
'admin123', 'letmein', 'welcome', 'monkey'
]
if password.lower() in common_weak_passwords:
errors.append("此密碼過於常見,請使用更安全的密碼")
return (len(errors) == 0, errors)
def validate_password_for_user(
password: str,
username: str = None,
name: str = None,
email: str = None
) -> tuple[bool, list[str]]:
"""
驗證密碼(包含使用者資訊檢查)
Args:
password: 待驗證的密碼
username: 使用者帳號
name: 使用者姓名
email: Email
Returns:
(是否通過, 錯誤訊息列表)
"""
# 先檢查基本強度
is_valid, errors = validate_password_strength(password)
# 檢查是否包含使用者資訊
password_lower = password.lower()
if username and username.lower() in password_lower:
errors.append("密碼不可包含帳號名稱")
if name:
name_parts = name.split()
for part in name_parts:
if len(part) >= 3 and part.lower() in password_lower:
errors.append("密碼不可包含姓名")
break
if email:
email_user = email.split('@')[0]
if len(email_user) >= 3 and email_user.lower() in password_lower:
errors.append("密碼不可包含 Email 使用者名稱")
return (len(errors) == 0, errors)
if __name__ == "__main__":
# 測試密碼產生
print("=== 密碼產生測試 ===")
for i in range(5):
pwd = generate_secure_password()
is_valid, errors = validate_password_strength(pwd)
print(f"密碼 {i+1}: {pwd} - 有效: {is_valid}")
# 測試密碼驗證
print("\n=== 密碼驗證測試 ===")
test_cases = [
("weak", False),
("WeakPass", False),
("WeakPass123", False),
("Strong@Pass123", True),
("admin@Pass123", True)
]
for password, expected in test_cases:
is_valid, errors = validate_password_strength(password)
status = "" if is_valid == expected else ""
print(f"{status} {password}: {is_valid}")
if errors:
for error in errors:
print(f" - {error}")
# 測試加密與驗證
print("\n=== 密碼加密測試 ===")
plain_pwd = "TestPassword@123"
hashed = hash_password(plain_pwd)
print(f"明文密碼: {plain_pwd}")
print(f"加密密碼: {hashed}")
print(f"驗證正確密碼: {verify_password(plain_pwd, hashed)}")
print(f"驗證錯誤密碼: {verify_password('WrongPassword', hashed)}")