Aula 12 - Golang para Web - Modelo Usuário - User Model
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:
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:
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
Acesse:
Por agora é só, nos vemos próxima. ;)
Código da aula: Github
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
Novamente deixo meus link de afiliados:
Obrigado, até a próxima e bons estudos. ;)