作为程序员,遇到身份验证系统是很常见的,因为如今几乎每个 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中文网其他相关文章!