Se o seu host permitir ou você precisará lidar com dados confidenciais, use HTTPS, ponto final. (Muitas vezes é exigido pela lei afaik).
Caso contrário, se você quiser fazer algo por HTTP. Eu faria algo assim.
- O servidor incorpora sua chave pública na página de login.
- O cliente preenche o formulário de login e clica em enviar.
- Uma solicitação AJAX obtém o carimbo de data / hora atual do servidor.
- O script do lado do cliente concatena as credenciais, o timestamp e um salt (hash de dados analógicos, por exemplo, movimentos do mouse, eventos de pressionamento de tecla), criptografa-o usando a chave pública.
- Envia o hash resultante.
- O servidor descriptografa o hash
- Verifica se o carimbo de data / hora é recente o suficiente (permite apenas uma janela curta de 5 a 10 segundos). Rejeita o login se o carimbo de data / hora for muito antigo.
- Armazena o hash por 20 segundos. Rejeita o mesmo hash para login durante este intervalo.
- Autentica o usuário.
Dessa forma, a senha é protegida e o mesmo hash de autenticação não pode ser reproduzido.
Sobre a segurança do token de sessão. Isso é um pouco mais difícil. Mas é possível tornar a reutilização de um token de sessão roubado um pouco mais difícil.
- O servidor define um cookie de sessão extra que contém uma string aleatória.
- O navegador envia de volta esse cookie na próxima solicitação.
- O servidor verifica o valor no cookie, se for diferente, ele destrói a sessão, caso contrário, está tudo bem.
- O servidor configura o cookie novamente com um texto diferente.
Portanto, se o token de sessão for roubado e uma solicitação for enviada por outra pessoa, na próxima solicitação do usuário original, a sessão será destruída. Portanto, se o usuário estiver navegando ativamente no site, clicando em links com frequência, o ladrão não irá longe com o token roubado. Este esquema pode ser fortalecido exigindo outra autenticação para as operações confidenciais (como exclusão de conta).
EDIT: Observe que isso não evita ataques MITM se o invasor configurar sua própria página com uma chave pública diferente e solicitações de proxy para o servidor. Para se proteger contra isso, a chave pública deve ser fixada no armazenamento local do navegador ou dentro do aplicativo para detectar esse tipo de truque.
Sobre a implementação: RSA é provavelmente o algoritmo mais conhecido, mas é bastante lento para chaves longas. Não sei quão rápida seria uma implementação de PHP ou Javascript. Mas provavelmente existem algoritmos mais rápidos.