오늘날 거의 모든 웹 시스템은 고객의 데이터를 제어하고 유지해야 하며 대부분의 데이터는 민감한 리소스이므로 보안을 유지해야 하기 때문에 프로그래머로서 인증 시스템을 접하는 것은 매우 흔한 일입니다. 저는 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/exc 블록 내에서 현재 타임스탬프가 먼저 정의되고 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!