作為程式設計師,遇到身分驗證系統是很常見的,因為如今幾乎每個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...
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中文網其他相關文章!