You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django兼容PHP password_hash()遗留密码:有无更简便实现方案?

Simplifying Django Compatibility with PHP's password_hash() Legacy Passwords

Great question—this is an extremely common scenario when migrating from PHP to Django, and there are cleaner ways to handle this without overriding the check_password method on your custom User model. Let’s break down the best approaches and confirm you’re not alone in this problem!

A Cleaner Implementation: Let the Password Hasher Handle Both Formats

Your current solution works, but you can eliminate the User model’s custom check_password method entirely by adjusting your BCryptPasswordHasher to natively recognize both the legacy PHP hash format (no prefix) and Django’s prefixed format. Here’s how to tweak it:

from django.contrib.auth.hashers import BasePasswordHasher, constant_time_compare, mask_hash
from collections import OrderedDict

class BCryptPHPPasswordHasher(BasePasswordHasher):
    algorithm = "bcrypt_php"
    library = ("bcrypt", "bcrypt")
    rounds = 10

    def salt(self):
        bcrypt = self._load_library()
        return bcrypt.gensalt(self.rounds)

    def encode(self, password, salt):
        bcrypt = self._load_library()
        password = password.encode()
        data = bcrypt.hashpw(password, salt)
        # Keep the encoded format as just the bcrypt string (no prefix) for PHP compatibility
        return data.decode('ascii')

    def verify(self, incoming_password, encoded_db_password):
        bcrypt = self._load_library()
        incoming_password = incoming_password.encode()

        # Check if the stored hash has our algorithm prefix
        if encoded_db_password.startswith(f"{self.algorithm}$"):
            # Split off the prefix to get the raw bcrypt hash
            _, raw_hash = encoded_db_password.split('$', 1)
        else:
            # No prefix—this is a legacy PHP bcrypt hash
            raw_hash = encoded_db_password

        # Bcrypt natively handles $2y$ (PHP) vs $2b$ (Django) format differences
        computed_hash = bcrypt.hashpw(incoming_password, raw_hash.encode('ascii'))
        return constant_time_compare(raw_hash.encode('ascii'), computed_hash)

    def safe_summary(self, encoded):
        # Handle both prefixed and unprefixed hashes for admin display
        if encoded.startswith(f"{self.algorithm}$"):
            _, raw_hash = encoded.split('$', 1)
        else:
            raw_hash = encoded
        
        _, algostr, work_factor, data = raw_hash.split('$', 3)
        salt, checksum = data[:22], data[22:]
        return OrderedDict([
            ('algorithm', self.algorithm),
            ('work factor', work_factor),
            ('salt', mask_hash(salt)),
            ('checksum', mask_hash(checksum)),
        ])

    def must_update(self, encoded):
        # Return False to maintain compatibility with the legacy PHP app
        return False

    def harden_runtime(self, password, encoded):
        if encoded.startswith(f"{self.algorithm}$"):
            _, raw_hash = encoded.split('$', 1)
        else:
            raw_hash = encoded
        
        data = raw_hash.split('$')
        rounds = int(data[2])
        diff = 2 ** (self.rounds - rounds) - 1
        # Re-use the existing salt for runtime hardening
        salt = '$'.join(data[:3]) + '$' + data[3][:22]
        while diff > 0:
            self.encode(password, salt.encode('ascii'))
            diff -= 1

Why This Is Better:

  • No User Model Overrides: You can revert your custom User class to remove the check_password method, keeping your model logic focused on user data rather than password handling.
  • Transparent Format Support: The hasher automatically handles both legacy PHP hashes (no prefix) and any new hashes you generate, without extra code.
  • Django-Aligned Flow: Follows Django’s intended password hasher pattern, making your code easier to maintain for other Django developers.

Have Others Faced This? Absolutely!

This is a top pain point for PHP-to-Django migrations. Many developers run into the exact same issue with PHP’s password_hash() output—especially the $2y$ variant, which is functionally identical to Django’s default $2b$ bcrypt format (most bcrypt libraries, including the one Django uses, automatically handle these version tags).

Common community solutions mirror the approach above: adjusting the custom hasher to recognize unprefixed legacy hashes, rather than modifying the User model. Some also choose to gradually migrate hashes (rehashing with Django’s format when a user logs in), but since you need to keep PHP compatibility, your current must_update=False is the right call.

Final Settings Tweak

Keep your settings.py configuration the same, just make sure the hasher path matches your updated class name:

AUTH_USER_MODEL = 'custom.User'
PASSWORD_HASHERS = [
    'custom.auth.hashers.BCryptPHPPasswordHasher',  # Updated class name
]

This setup will let your Django app seamlessly verify both existing PHP-generated passwords and any new ones you create, without touching the legacy PHP app.

内容的提问来源于stack exchange,提问作者Artur Siepietowski

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:26:18