Сайт на Golang. Авторизация. Часть 2

В прошлой части мы остановились на том, что запустили наше приложение. Но у нас нет пользователей и мы никак не регулируем кто может читать наш секретный контент, а кто нет. Настало время это исправить!

Первым дело добавим в базу данных таблицу для пользователей и сразу одного из пользователей:

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

Leave a Comment

Ваш адрес email не будет опубликован.