В прошлой части мы остановились на том, что запустили наше приложение. Но у нас нет пользователей и мы никак не регулируем кто может читать наш секретный контент, а кто нет. Настало время это исправить!
Первым дело добавим в базу данных таблицу для пользователей и сразу одного из пользователей:
create table if not exists users
(
id bigint primary key generated always as identity,
login varchar(200) not null unique,
hashed_password varchar(200) not null,
name varchar(200) not null,
surname varchar(200) not null
);
insert into users (login, hashed_password, name, surname)
values ('alextonkonogov', '827ccb0eea8a706c4c34a16891f84e7b', 'Alex', 'Tonkonogov');
Я добавил себя, вы можете добавить кого угодно, но значение hashed_password рекомендую не менять — мы к нему вернемся позже. Также сразу обращаю внимание на то, что пароль мы будем хранить не в открытом виде, а в виде хеша. Таким образом о паролях пользователей будут знать только сами пользователи.
Теперь доработаем наше приложение так, чтобы оно умело работать с новым объектом (пользователем).
В папке repository создаем файл user.go со следующим кодом:
package repository
import (
"context"
"fmt"
)
type User struct {
Id int `json:"id" db:"id"`
Login string `json:"login" db:"login"`
HashedPassword string `json:"hashed_password" db:"hashed_password"`
Name string `json:"name" db:"name"`
Surname string `json:"surname" db:"surname"`
}
func (r *Repository) Login(ctx context.Context, login, hashedPassword string) (u User, err error) {
row := r.pool.QueryRow(ctx, `select id, login, name, surname from users where login = $1 AND hashed_password = $2`, login, hashedPassword)
if err != nil {
err = fmt.Errorf("failed to query data: %w", err)
return
}
err = row.Scan(&u.Id, &u.Login, &u.Name, &u.Surname)
if err != nil {
err = fmt.Errorf("failed to query data: %w", err)
return
}
return
}
Здесь мы по логину и хешу пароля ищем пользователя в базе. Есть есть совпадение, то вернется пользователь и это будет значить, что авторизация прошла.
Теперь создадим страницу для авторизации. В папке public/html создаем login.html:
{{define "login"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login page</title>
</head>
<body>
<form id="loginForm" name="loginForm" action="/login" method="post" class="mt-4">
<label for="login">Логин</label>
<input type="login" id="login" name="login" autocomplete="on">
<label for="password">Пароль</label>
<input type="password" id="password" name="password" autocomplete="on">
<button type="submit" name="submitBtn">Войти</button>
</form>
{{if . }}
<div>
{{.Message}}
</div>
{{end}}
</body>
</html>
{{end}}
Теперь добавим новый эндпоинт, который будет направлять нас напрямую на страницу авторизации. Для этого идем в application.go и в методе Routes добавим следующее:
r.GET("/login", func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
a.LoginPage(rw, "")
})
Должно получиться вот так:
func (a app) Routes(r *httprouter.Router) {
r.ServeFiles("/public/*filepath", http.Dir("public"))
r.GET("/", a.StartPage)
r.GET("/login", func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
a.LoginPage(rw, "")
})
}
И там же добавим соответствующий метод:
func (a app) LoginPage(rw http.ResponseWriter, message string) {
lp := filepath.Join("public", "html", "login.html")
tmpl, err := template.ParseFiles(lp)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
type answer struct {
Message string
}
data := answer{message}
err = tmpl.ExecuteTemplate(rw, "login", data)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
}
Если запустить приложение через go run main.go и заглянуть на http://localhost:8080/login , то мы увидим нашу страничку для ввода логина и пароля.

Но как вы, надеюсь, понимаете нам недостаточно просто создать страницу для авторизации. На текущий момент нам никто не запрещает заходить на корневую страницу и именно исправлением этого недоразумения мы сейчас и займемся.
Если вы попытаетесь ввести что-либо в поля и нажать на кнопку «Войти», то вам выведется сообщение «Method Not Allowed». Это потому, что данные с нашей формы при нажатии на кнопку будут отправлены на /login методом POST ( см. login.html — action=»/login» method=»post»). То есть мы должны принять от пользователя введенные логин и пароль, сходить в базу, чтобы проверить его существование и дальше принять решение — пускать или не пускать дальше. Давайте реализуем эту логику!
В application.go в методе Routes добавим новый эндпоинт r.POST(«/login», a.Login):
func (a app) Routes(r *httprouter.Router) {
r.ServeFiles("/public/*filepath", http.Dir("public"))
r.GET("/", a.StartPage)
r.GET("/login", func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
a.LoginPage(rw, "")
})
r.POST("/login", a.Login)
}
И там же добавим новый метод, который будет отвечать за логику:
func (a app) Login(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
login := r.FormValue("login")
password := r.FormValue("password")
if login == "" || password == "" {
a.LoginPage(rw, "Необходимо указать логин и пароль!")
return
}
hash := md5.Sum([]byte(password))
hashedPass := hex.EncodeToString(hash[:])
_, err := a.repo.Login(a.ctx, login, hashedPass)
if err != nil {
a.LoginPage(rw, "Вы ввели неверный логин или пароль!")
return
}
flag = true
http.Redirect(rw, r, "/", http.StatusSeeOther)
}
Первым делом мы проверяем, не переданы ли нам пустые значения. Затем сразу получаем хеш пароля и идем проверять наличие такого пользователя в базе (разбирали это выше). То есть, еще раз, если в базе не нашли пользователя с таким логином и хешем пароля, то отправляем его на очередной круг, если нашли, то добро пожаловать.
Запускаем через go run main.go и проверяем на http://localhost:8080/login. Какой бы пароль вы не ввели, приложение должно вас отфутболивать:

Но если ввести в качестве пароля «12345» (именно это и скрывается за хешем ‘827ccb0eea8a706c4c34a16891f84e7b’), то вы будете успешно перенаправлены на корневую страницу с цитатками.
Теперь давайте сделаем так, чтобы приложение пускало на корневую страницу только через страницу авторизации.
Для простоты и наглядности сначала добавим внешнюю переменную flag со значением по умолчанию false (var flag bool) и в случае успешной авторизации в методе Login будем менять значение на true (в самом конце прямо перед http.Redirect(rw, r, «/», http.StatusSeeOther) добавьте flag = true). Также в методе StartPage самой первой строкой возвращайте его обратно (flag = false). Это не должно быть сложно и, уверен, что вы справитесь )) Если что смотрите код на github по ссылке в конце статьи.
Теперь добавим новый метод authorized. Обратите внимание на то, что в качестве параметра этот метод принимает хендлеры! Это middleware — в нашем случае метод, который принимает другой метод и возвращает его же, выполняя при этом промежуточную логику проверки авторизованности (flag = true).
func (a app) authorized(next httprouter.Handle) httprouter.Handle {
return func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if !flag {
http.Redirect(rw, r, "/login", http.StatusSeeOther)
return
}
next(rw, r, ps)
}
}
И давайте используем его, обернув им наш хендлер для эндпоинта, ведущего на главную страничку — r.GET(«/», a.authorized(a.StartPage)). В итоге должно получиться вот так:
func (a app) Routes(r *httprouter.Router) {
r.ServeFiles("/public/*filepath", http.Dir("public"))
r.GET("/", a.authorized(a.StartPage))
r.GET("/login", func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
a.LoginPage(rw, "")
})
r.POST("/login", a.Login)
}
В очередной раз запустив приложение, мы попадем на страницу авторизации. После успешного ввода логина и пароля, приложение покажет нам рандомную мотивацию, но при следующей перезагрузки мы снова попадем на страницу авторизации и так каждый раз 🙂 Но как сделать так, чтобы приложение запоминало, что мы уже вводили правильный логин и пароль? Этому есть простое решение и о нем мы поговорим в следующей части цикла по авторизации.
Текущая структура проекта:

Ссылка на pull request: https://github.com/alextonkonogov/atonko-authorization/pull/2