首頁  >  文章  >  後端開發  >  使用 Salt 在 Python 中建立身份驗證服務

使用 Salt 在 Python 中建立身份驗證服務

WBOY
WBOY原創
2024-08-27 06:03:021023瀏覽

作為程式設計師,遇到身分驗證系統是很常見的,因為如今幾乎每個Web 系統都需要控制和維護其客戶的數據,並且由於其中大多數都是敏感資源,因此有必要保證它們的安全。我認為安全性,就像 API 的許多非功能性需求一樣,可以透過想像各種場景來衡量或測試。例如,在身分驗證服務中,我們可以思考:如果有人試圖透過暴力破解使用者的密碼怎麼辦,如果另一個使用者嘗試使用另一個客戶端的存取權杖怎麼辦,如果兩個使用者意外創建了他們的憑證怎麼辦使用相同的密碼

透過想像這些情況,我們可以預測並制定預防措施。例如,為密碼建立標準可能會使透過暴力破解變得非常困難,或者對 API 應用速率限制可以防止惡意操作。在本文中,我打算重點討論最後一個場景的問題。兩個使用者使用相同的密碼在同一系統上註冊是對系統的嚴重破壞。

在銀行對使用者密碼進行加密是一種很好的做法,這樣可以防止資料外洩。下面的程式碼展示了一個簡單的憑證註冊系統如何在 Python 中運作。

@dataclass
class CreateCredentialUsecase:
    _credential_repository: CredentialRepositoryInterface
    _password_salt_repository: PasswordSaltRepositoryInterface

    async def handle(self, data: CreateCredentialInputDto) -> CreateCredentialOutputDto:
        try:
            now = datetime.now()

            self.__hash = sha256()
            self.__hash.update(data.password.encode())
            self.__credential = Credential(
                uuid4(), data.email, self.__hash.hexdigest(), now, now
            )

            credential_id = await self._credential_repository.create(self.__credential)

            return CreateCredentialOutputDto(UUID(credential_id))
        except Exception as e:
            raise e

前 4 行是類別定義,使用 @dataclass 裝飾器省略建構函式方法、其屬性和函式簽章。在 try/ except 區塊內,首先定義當前時間戳,我們實例化 Hash 對象,使用提供的密碼更新它,將其保存在銀行中,最後將憑證 ID 回傳給使用者。在這裡你可能會想「好吧......如果密碼是加密的我就不需要擔心,對吧?」。然而,事實並非如此,我會解釋一下。

發生的情況是,當密碼加密時,這是透過雜湊完成的,雜湊是一種將輸入映射到最終值的資料結構,但是,如果兩個輸入相同,則會儲存相同的密碼。這與哈希值是確定性的相同。請注意下面的範例,該範例說明了資料庫中儲存使用者和哈希的簡單表。

user password
alice@example.com 5e884898da28047151d0e56f8dc6292773603d0d
bob@example.com 6dcd4ce23d88e2ee9568ba546c007c63e8f6f8d6
carol@example.com a3c5b2c98b4325c6c8c6f6e6dbda6cf17b5d7f9a
dave@example.com 1a79a4d60de6718e8e5b326e338ae533
eve@example.com 5e884898da28047151d0e56f8dc6292773603d0d
frank@example.com 7c6a180b36896a8a8c6a2c29e7d7b1d3
grace@example.com 3c59dc048e885024e146d1e4d9d0e4b2

Neste exemplo, as linhas 1 e 5 compartilham o mesmo hash e, portanto, a mesma senha. Para contornarmos esse problema podemos utilizar o salt.

Vamos colocar um pouco de sal nessa senha...

CRIANDO UM SERVIÇO DE AUTENTICAÇÃO EM PYTHON UTILIZANDO SALT

A ideia é que no momento do cadastro do usuário uma string seja gerada de forma aleatória e seja concatenada a senha do usuário antes das credenciais serem salvas no banco. Em seguida esse salt é salvo em uma tabela separada e deve ser utilizada novamente durante o login do usuário. O código alterado ficaria como o exemplo abaixo:

@dataclass
class CreateCredentialUsecase:
    _credential_repository: CredentialRepositoryInterface
    _password_salt_repository: PasswordSaltRepositoryInterface

    async def handle(self, data: CreateCredentialInputDto) -> CreateCredentialOutputDto:
        try:
            now = datetime.now()

            self.__salt = urandom(32)
            self.__hash = sha256()
            self.__hash.update(self.__salt + data.password.encode())
            self.__credential = Credential(
                uuid4(), data.email, self.__hash.hexdigest(), now, now
            )
            self.__salt = PasswordSalt(
                uuid4(), self.__salt.hex(), self.__credential.id, now, now
            )

            credential_id = await self._credential_repository.create(self.__credential)
            await self._password_salt_repository.create(self.__salt)

            return CreateCredentialOutputDto(UUID(credential_id))
        except Exception as e:
            raise e

Agora é possível notar o salt gerado na linha 59. Em seguida ele é utilizado para gerar o hash junto com a senha que o usuário cadastrou, na linha 61. Por fim ele é instanciado através da classe PasswordSalt na linha 65 e armazenado no banco na linha 70. Por último, o código abaixo é o caso de uso de autenticação/login utilizando o salt.

@dataclass
class AuthUsecase:
    _credential_repository: CredentialRepositoryInterface
    _jwt_service: JWTService
    _refresh_token_repository: RefreshTokenRepositoryInterface

    async def handle(self, data: AuthInputDto) -> AuthOutputDto:
        try:

            ACCESS_TOKEN_HOURS_TO_EXPIRATION = int(
                getenv("ACCESS_TOKEN_HOURS_TO_EXPIRATION")
            )
            REFRESH_TOKEN_HOURS_TO_EXPIRATION = int(
                getenv("REFRESH_TOKEN_HOURS_TO_EXPIRATION")
            )

            self.__credential = await self._credential_repository.find_by_email(
                data.email
            )

            if self.__credential is None:
                raise InvalidCredentials()

            self.__hash = sha256()
            self.__hash.update(
                bytes.fromhex(self.__credential.salt) + data.password.encode()
            )

            if self.__hash.hexdigest() != self.__credential.hashed_password:
                raise InvalidCredentials()

            access_token_expiration_time = datetime.now() + timedelta(
                hours=(
                    ACCESS_TOKEN_HOURS_TO_EXPIRATION
                    if ACCESS_TOKEN_HOURS_TO_EXPIRATION is not None
                    else 24
                )
            )
            refresh_token_expiration_time = datetime.now() + timedelta(
                hours=(
                    REFRESH_TOKEN_HOURS_TO_EXPIRATION
                    if REFRESH_TOKEN_HOURS_TO_EXPIRATION is not None
                    else 48
                )
            )

            access_token_payload = {
                "credential_id": self.__credential.id,
                "email": self.__credential.email,
                "exp": access_token_expiration_time,
            }

            access_token = self._jwt_service.encode(access_token_payload)

            refresh_token_payload = {
                "exp": refresh_token_expiration_time,
                "context": {
                    "credential": {
                        "id": self.__credential.id,
                        "email": self.__credential.email,
                    },
                },
            }

            refresh_token = self._jwt_service.encode(refresh_token_payload)
            print(self._jwt_service.decode(refresh_token))

            now = datetime.now()

            await self._refresh_token_repository.create(
                RefreshToken(
                    uuid4(),
                    refresh_token,
                    False,
                    self.__credential.id,
                    refresh_token_expiration_time,
                    now,
                    now,
                    now,
                )
            )

            return AuthOutputDto(
                UUID(self.__credential.id),
                self.__credential.email,
                access_token,
                refresh_token,
            )
        except Exception as e:
            raise e

O tempo de expiração dos tokens é recuperado através de variáveis de ambiente e a credencial com o salt são recuperados através do email. Entre as linhas 103 e 106 a senha fornecida pelo usuário é concatenada ao salt e o hash dessa string resultante é gerado, assim é possível comparar com a senha armazenada no banco. Por fim acontecem os processos de criação dos access_token e refresh_token, o armazenamento do refresh_token e o retorno dos mesmos ao client. Utilizar essa técnica é bem simples e permite fechar uma falha de segurança no seu sistema, além de dificultar alguns outros possíveis ataques. O código exposto no texto faz parte de um projeto maior meu e está no meu github: https://github.com/geovanymds/auth.

Espero que esse texto tenha sido útil para deixar os processos de autenticação no seu sistem mais seguros. Nos vemos no próximo artigo!

以上是使用 Salt 在 Python 中建立身份驗證服務的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn