Aula 12 – Golang para Web – Modelo Usuário – User Model
Aula 12 – Golang para Web – Modelo Usuário – User Model
Voltar para página principal do blog
Todas as aulas desse curso
Aula 11 Aula 13
Se gostarem do conteúdo dêem um joinha 👍 na página do Código Fluente no
Facebook
Link do código fluente no Pinterest
Meus links de afiliados:
Hostinger
Digital Ocean
One.com
Código da aula: Github
Melhore seu NETWORKING
Participe de comunidades de desenvolvedores:
Fiquem a vontade para me adicionar ao linkedin.
E também para me seguir no GITHUB.
Ah, se puder, clica na estrela nos meus repositórios pra dá uma força ao meu perfil no GITHUB.
Aula 12 – Golang para Web – Modelo Usuário – User Model
Vamos revisitar o modelo de dados para adicionar alguns recursos que precisaremos mais tarde e também para dar mais uma visão geral de alguns padrões de design comuns do Redis.
O redis oferece algumas vantagens em termos de desempenho e flexibilidade, mas, requer um certo cuidado na perspectiva do programador para usar ele efetivamente.
Na aula passada, a gente moveu o código que lida com o Redis para a pasta model:
│ readme.md
│
└───web_app
│ main.go
│
├───middleware
│ middleware.go
│
├───models
│ comment.go
│ db.go
│ user.go
│
├───routes
│ routes.go
│
├───sessions
│ sessions.go
│
├───static
│ index.css
│
├───templates
│ index.html
│ login.html
│ register.html
│
└───utils
templates.go
Essa reorganização do código, esse tipo de design ajuda e muito em termos de gerenciamento.
Agora iremos nos aprofundar um pouco mais na construção de alguns modelos de dados, para abstrair os detalhes de back-end do Redis.
Vamos começar com nosso modelo usuário (User).
Vamos criar uma estrutura de dados para o usuário, um struct de User que irá representar um usuário.
Mais a frente, a gente vai poder anexar métodos e organizar todas as funções que lidam com os usuários em métodos anexados ao objeto User.
No web_app/models/user.go a gente vai colocar:
type User struct { key string }
Onde key é a chave do usuário dentro do Redis.
Então, além dos atributos do usuário, o hash bcrypt e o nome, agora a gente tem a chave do User que vai funcionar tipo uma chave primária em bancos relacionais.
Todos os atributos estarão em um hash map, ou seja, em um dicionário chave valor no Redis, e esta chave do User vai apontar para aquele hash map com todos os atributos do usuário.
Toda a estrutura do User deve ser armazenada basicamente como uma forma de procurar um determinado User no hash map e em seguida, os métodos anexados a sua estrutura que farão as chamadas as buscas reais no Redis.
Vamos também fazer um construtor para o modelo usuário, ou seja, um construct para o User.
func NewUser(username string, hash []byte) (*User, error) { id, err := client.Incr("user:next-id").Result() if err != nil { return nil, err } key := fmt.Sprintf("user:%d", id) pipe := client.Pipeline() pipe.HSet(key, "id", id) pipe.HSet(key, "username", username) pipe.HSet(key, "hash", hash) pipe.HSet("user:by-username", username, id) _, err = pipe.Exec() if err != nil { return nil, err } return &User{key}, nil }
INCR
O comando Redis INCR é usado para incrementar o valor inteiro da chave do User a medida que os usuários são criados no banco.
Um erro é retornado se a chave contém um valor do tipo errado ou contém uma string que não pode ser representada como um inteiro.
Esta operação é limitada a inteiros signed de 64 bits.
A chave começa no número 0.
A linha:
key := fmt.Sprintf(“user:%d”, id)
Atribui a key daquele usuário um id tipo: user: 0
Pipeline
Em seguida, criamos um pipeline através da variável global client do Redis criada no db.go, na mesma pasta.
pipe := client.Pipeline()
Um servidor de Solicitação / Resposta pode ser implementado de forma que seja capaz de processar novas solicitações, mesmo que o cliente ainda não tenha lido as respostas antigas.
Desta forma, é possível enviar vários comandos para o servidor sem esperar pelas respostas e finalmente ler as respostas em uma única etapa.
É para isso que serve o Pipeline do cliente Redis.
Em seguida temos várias linhas usando HSet do Pipeline do cliente Redis, o H é de Hash.
pipe.HSet(key, “id”, id)
pipe.HSet(key, “username”, username)
pipe.HSet(key, “hash”, hash)
pipe.HSet(“user:by-username”, username, id)
A primeira linha atribui o id, a segunda atribui o username recebido como parâmetro, ao usuário com aquele id, a mesma coisa para a linha seguinte para o hash de autenticação.
Na última temos uma outra chave no Redis criada implicitamente.
Esta chave vai ser usuário por nome de usuário, ela vai conter uma forma de busca que relaciona o nome do usuário ao ID, isso nos permitirá procurar nosso usuário pelo nome.
Isso é legal, porque sem isso, se a gente quisesse ver os atributos de um determinado usuário, teríamos que saber seu ID.
Execução do Pipeline
_, err = pipe.Exec()
if err != nil {
return nil, err
}
return &User{key}, nil
O underscore recebe um array com os comandos executados como resultado do pipe.Exec(), então ele não vai interessar muito.
Caso haja algum erro na execução do Pipeline, a variável erro receberá algum conteúdo e cairá no if e retornará nil para o user e retornará também a informação do erro.
Caso tudo ocorra bem vai retornar um ponteiro para o usuário com aquele ID e nil caso não exista.
Temos também a função GetUsername(), que vai retornar um ponteiro para uma estrutura user que guarda as informações de um usuário específico.
GetUsername()
func (user *User) GetUsername() (string, error) { return client.HGet(user.key, "username").Result() }
O Result retorna uma string.
Vamos ter a mesma coisa para o hash.
GetHash()
func (user *User) GetHash() ([]byte, error) { return client.HGet(user.key, "hash").Bytes() }
Agora a função que vai verificar as credenciais de autenticação do usuário.
Authenticate()
func (user *User) Authenticate(password string) error { hash, err := user.GetHash() if err != nil { return err } err = bcrypt.CompareHashAndPassword(hash, []byte(password)) if err == bcrypt.ErrMismatchedHashAndPassword { return ErrInvalidLogin } return err }
Ela vai retornar nil se tudo der certo, que é o que err estará guardando quando tudo estiver correto com a senha do usuário.
Então vamos analisar a Authenticate().
Ela tenta pegar o hash de autenticação daquele usuário, ou se der errado, um erro.
Verifica: if err != nil
Ou seja, se o erro for diferente de nil, isto significa que teve um erro, tipo: o Redis pode estar desconectado.
Seguindo
CompareHashAndPassword
err = bcrypt.CompareHashAndPassword(hash, []byte(password))
A CompareHashAndPassword() do bcrypt compara uma senha com hash com seu possível equivalente em texto simples. Ela retorna nulo em caso de sucesso ou erro em caso de falha.
Ela compara uma senha com hash bcrypt com seu possível equivalente em texto simples e retorna nulo em caso de sucesso ou erro em caso de falha.
ErrMismatchedHashAndPassword
Depois, outra verificação:
if err == bcrypt.ErrMismatchedHashAndPassword
Agora a função ErrMismatchedHashAndPassword do bcrypt compara a senha com hash, ou seja, a senha que o usuário digitou e que passou pelo algoritmo bcrypt de hash, e compara com a senha armazenada já hasheada daquele usuário, e aí se não corresponder certinho, entra no if e retorna ErrInvalidLogin.
Se não, é porque tá tudo ok com a senha digitada, com hash armazenado, tudo correspondendo certinho, aí retorna o que tiver em err, que no caso será nil quando tudo for ok.
Teremos também a função GetUserByUsername.
GetUserByUsername()
func GetUserByUsername(username string) (*User, error) { id, err := client.HGet("user:by-username", username).Int64() if err == redis.Nil { return nil, ErrUserNotFound } else if err != nil { return nil, err } key := fmt.Sprintf("user:%d", id) return &User{key}, nil }
Ela recebe um nome de usuário em uma string como parâmetro.
Ela retorna um ponteiro para um usuário ou um erro em potencial.
Então, se a gente chama a função passando um nome de usuário que existe, ela retorna um ponteiro para esse usuário, se não existir, ela retorna um erro.
Ela será útil para nosso método de autenticação, mas também em outras situações, aliás em várias situações a gente vai precisar pegar o usuário pelo nome dele.
Em id, err := client.HGet(“user:by-username”, username).Int64() a gente tá pegando o ID daquele usuário com aquele nome e um erro caso ele não exista ou o Redis não esteja funcionando.
Depois uma primeira verificação que retorna um erro se não encontrar o usuário informado e nil para esse usuário.
Outra verificação retorna o erro se ele for diferente de nil, e retorna nil para o user.
Se não entrar em nenhuma verificação, atribui a key ao ID daquele usuário e retorna o ID desse usuário e nil para o erro, já que não houve erro.
AuthenticateUser()
func AuthenticateUser(username, password string) error { user, err := GetUserByUsername(username) if err != nil { return err } return user.Authenticate(password) }
A AuthenticateUser() recebe o nome do usuário e a senha em strings como argumentos.
Ela chama a função GetUserByUsername() passando o username que recebeu como parâmetro e o retorno é atribuído a user e se houver um erro, ele joga o retorno do erro em err.
Depois vem uma verificação pra saber se a variável err tem conteúdo ou não.
Se tiver é porque houve um erro, a execução entra no if e retorna o err.
Se tudo ocorrer bem, ela vai retornar o que vem da chamada da Authenticate() passando a senha que recebeu como parâmetro, pra ela.
O Authenticate() vai retornar um erro se ocorrer problema com a senha ou com o Redis, se não, vai retornar nil, ou seja, não houve problema na autenticação desse usuário.
RegisterUser()
Precisaremos mudar a RegisterUser(), ela vai ficar assim:
func RegisterUser(username, password string) error { cost := bcrypt.DefaultCost hash, err := bcrypt.GenerateFromPassword([]byte(password), cost) if err != nil { return err } _, err = NewUser(username, hash) return err }
Por causa das modificações, precisaremos registrar um novo usuário novamente, porque não conseguiremos acessar com nossas antigas credenciais.
Link para o código dessa aula:
Github
Com todas as mudanças feitas, você pode testar para ver se tudo continua funcionando.
Ligue o redis-server:
redis-server
Ligue o servidor:
go run main.go