По просьбам читателей (а они у меня есть :D), я наконец-то созрел выдать вам про авторизацию пользователей на сайте. Эта тема, на мой взгляд, чуть сложнее предыдущих, поэтому я постараюсь более менее подробно разобрать все основные моменты. Мы начнем с простого и постепенно будем усложнять наше решение. Нас ожидают следующие статьи:
- В первой части мы с вами, используя знания из предыдущих уроков, создадим простейшее веб-приложение, которое будет выдавать рандомную мотивационную цитатку из базы данных.
- Во второй мы добавим первого пользователя и начнем пускать его на сайт только через логин и пароль (авторизация).
- В третьей части мы начнем запоминать ранее авторизованного пользователя в памяти, чтобы не просить его вводить логи и пароль каждый раз (кеш).
- В четвертой части мы добавим возможность регистрироваться новым пользователям (регистрация).
В качестве БД предлагаю остановиться на PostgreSQL (опустим шаг с ее установкой). Просто создайте там одну таблицу с данными:
create table if not exists motivations (
id bigint primary key generated always as identity,
content varchar(999) not null,
author varchar(999) not null
);
insert into motivations (content, author) VALUES
('А если ты не уверен в себе ничего хорошего никогда не получится. Ведь если ты в себя не веришь, кто же поверит?', 'Кто-то умный'),
('Без идеи не может быть ничего великого! Без великого не может быть ничего прекрасного.', 'Гюстав Флобер'),
('Быстрее всего учишься в трех случаях — до 7 лет, на тренингах, и когда жизнь загнала тебя в угол.', 'Стивен Кови'),
('В вашем подсознании скрыта сила, способная перевернуть мир.', 'Уильям Джеймс'),
('В моем словаре нет слова «невозможно».', 'Наполеон Бонапарт'),
('Важно верить, что талант нам даётся не просто так – и что любой ценой его нужно для чего-то использовать.', 'Мари Кюри'),
('Ваше время ограничено, не тратьте его, живя чужой жизнью', 'Стив Джобс'),
('Велики те, кто видит, что миром правят мысли.', 'Ральф Эмерсон'),
('Великие души обладают волей, слабые же имеют только желания.', 'Китайская пословица'),
('Великие умы ставят перед собой цели остальные люди следуют своим желаниям.', 'Вашингтонг Ирвинг'),
('Вместо того, чтобы сетовать, что роза имеет шипы, я радуюсь тому, что среди шипов растет роза.', 'Жозеф Жубер'),
('Во-первых, не делай ничего без причины и цели. Во-вторых, не делай ничего, что бы не клонилось на пользу общества.', 'Марк Аврелий'),
('Возникшие желания должны реализовываться безотлагательно. А то все удовольствие пропадает. Захотел – сделал, чего тянуть, жизнь коротка. Здесь и сейчас!', 'Михаил Веллер'),
('Воин действует, а глупец протестует.', 'Мирный воин'),
('Воин не отказывается от того, что любит. Он находит любовь в том, что делает.', 'Мирный воин'),
('Вопрос не в том, кто мне разрешит, а в том, кто сможет мне запретить.', 'Айн Рэнд'),
('Все дело в мыслях. Мысль — начало всего. И мыслями можно управлять. И поэтому главное дело совершенствования: работать над мыслями.', 'Лев Толстой'),
('Все дети - художники. Проблема в том, чтобы остаться художником, когда ты вырос.', 'Пабло Пикассо'),
('Все победы начинаются с победы над самим собой.', 'Леонид Леонов'),
('Всегда выбирайте самый трудный путь - на нем вы не встретите конкурентов.', 'Шарль де Голль');
И убедитесь, что все работает.

Ну а теперь пишем наше простое приложение!
В корне проекта создаем папку internal и в ней repository. Создаем внутри файл storage.go со следующим кодом:
package repository
import (
"context"
"fmt"
"net"
"time"
"github.com/jackc/pgx/v4/pgxpool"
)
func InitDBConn(ctx context.Context) (dbpool *pgxpool.Pool, err error) {
url := "postgres://postgres:password@localhost:5432/postgres?sslmode=disable"
cfg, err := pgxpool.ParseConfig(url)
if err != nil {
err = fmt.Errorf("failed to parse pg config: %w", err)
return
}
cfg.MaxConns = int32(5)
cfg.MinConns = int32(1)
cfg.HealthCheckPeriod = 1 * time.Minute
cfg.MaxConnLifetime = 24 * time.Hour
cfg.MaxConnIdleTime = 30 * time.Minute
cfg.ConnConfig.ConnectTimeout = 1 * time.Second
cfg.ConnConfig.DialFunc = (&net.Dialer{
KeepAlive: cfg.HealthCheckPeriod,
Timeout: cfg.ConnConfig.ConnectTimeout,
}).DialContext
dbpool, err = pgxpool.ConnectConfig(ctx, cfg)
if err != nil {
err = fmt.Errorf("failed to connect config: %w", err)
return
}
return
}
Этот код отвечает за установку соединения нашего приложения с базой данных.
В этой же папке создадим еще два файла: repository.go и motivation.go
Код для repository.go
package repository
import (
"github.com/jackc/pgx/v4/pgxpool"
)
type Repository struct {
pool *pgxpool.Pool
}
func NewRepository(pool *pgxpool.Pool) *Repository {
return &Repository{pool: pool}
}
Здесь мы создаем объект, который получает в качестве параметра пул соединений с базой и будет выполнять для нас нужные запросы.
Код для motivation.go
package repository
import (
"context"
"fmt"
)
type Motivation struct {
Id int `db:"id"`
Content string `db:"content"`
Author string `db:"author"`
}
func (r *Repository) GetRandomMotivation(ctx context.Context) (m Motivation, err error) {
row := r.pool.QueryRow(ctx, `select * from motivations order by random() limit 1`)
if err != nil {
err = fmt.Errorf("failed to query data: %w", err)
return
}
err = row.Scan(&m.Id, &m.Content, &m.Author)
if err != nil {
err = fmt.Errorf("failed to query data: %w", err)
return
}
return
}
Этот код отвечает за выборку из базы рандомной мотивации.
Уровнем выше создаем папку application и внутри нее application.go, в котором мы опишем логику работы нашего приложения.
package application
import (
"context"
"html/template"
"net/http"
"path/filepath"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/julienschmidt/httprouter"
"github.com/alextonkonogov/atonko-authorization/internal/repository"
)
type app struct {
ctx context.Context
repo *repository.Repository
}
func (a app) Routes(r *httprouter.Router) {
r.ServeFiles("/public/*filepath", http.Dir("public"))
r.GET("/", a.StartPage)
}
func (a app) StartPage(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
motivation, err := a.repo.GetRandomMotivation(a.ctx)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
lp := filepath.Join("public", "html", "motivation.html")
tmpl, err := template.ParseFiles(lp)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
err = tmpl.ExecuteTemplate(rw, "motivation", motivation)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
}
func NewApp(ctx context.Context, dbpool *pgxpool.Pool) *app {
return &app{ctx, repository.NewRepository(dbpool)}
}
Если заметили, то в application.go в методе StartPage мы ссылаемся на файлик «motivation.html», который находится по пути «public/html/motivation.html». Создадим его!
{{define "motivation"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Motivations</title>
</head>
<body>
<div>
"{{.Content}}" - {{.Author}}
</div>
</body>
</html>
{{end}}
Теперь у нас осталось только соединить все в main.go:
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/julienschmidt/httprouter"
"github.com/alextonkonogov/atonko-authorization/internal/application"
"github.com/alextonkonogov/atonko-authorization/internal/repository"
)
func main() {
ctx := context.Background()
dbpool, err := repository.InitDBConn(ctx)
if err != nil {
log.Fatalf("%w failed to init DB connection", err)
}
defer dbpool.Close()
a := application.NewApp(ctx, dbpool)
r := httprouter.New()
a.Routes(r)
srv := &http.Server{Addr: "0.0.0.0:8080", Handler: r}
fmt.Println("It is alive! Try http://localhost:8080")
srv.ListenAndServe()
}
Если запустить приложение через go run main.go, то должны увидеть вот такое сообщение в консоли:

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

Текущая структура проекта:

Состав go.mod:
module github.com/alextonkonogov/atonko-authorization
go 1.15
require (
github.com/jackc/pgx/v4 v4.15.0
github.com/julienschmidt/httprouter v1.3.0
)
Pull request с кодом: https://github.com/alextonkonogov/atonko-authorization/pull/1