Я уже немного проспойлерил в первой части, что здесь мы будем разбирать как сделать так, чтобы наше приложение запоминало авторизованных пользователей и не кошмарило их требованиями вводить логин и пароль по каждому чиху.
В первую очередь убираем использование переменной flag (вспоминаем, что у нас была внешняя переменная и мы обращались к ней из методов Login, StartPage и authorized.). Вместо нее мы будем использовать мапу (map специальный объект, позволяющий хранить и получать доступ к значениям по ключу).
Теперь добавляем в структуру app новый параметр cache с типом map[string]repository.User. В качестве ключа, мы будем записывать токен, полученный при успешной авторизации, а в качестве значения данные пользователя из базы.
type app struct {
ctx context.Context
repo *repository.Repository
cache map[string]repository.User
}
И инициализируем мапу в функции-конструкторе:
func NewApp(ctx context.Context, dbpool *pgxpool.Pool) *app {
return &app{ctx, repository.NewRepository(dbpool), make(map[string]repository.User)}
}
Теперь давайте подумаем вот о чем. Если при авторизации мы сгенерируем токен и запишем его в памяти, то каким образом наш пользователь узнает о нем и сможет использовать для повторного входа? Нам нужно как-то его передать пользователю, ведь чтобы найти потом по токену пользователя у нас, мы должны сравнить токен от пользователя с имеющимися. Почему бы не использовать для этого печеньки? Здесь я , конечно, имею в виду cookies ))
В жизни это похоже на посещение клуба — при входе вы покупаете билет и проходите фейс-контроль, после которого вам на запястье надевают бумажный браслет. Таким образом, вы сможете ходить внутри, посещать уборную или бар, не беспокоясь, что охранник выставит вас наружу 🙂
Доработаем метод 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[:])
user, err := a.repo.Login(a.ctx, login, hashedPass)
if err != nil {
a.LoginPage(rw, "Вы ввели неверный логин или пароль!")
return
}
//логин и пароль совпадают, поэтому генерируем токен, пишем его в кеш и в куки
time64 := time.Now().Unix()
timeInt := string(time64)
token := login + password + timeInt
hashToken := md5.Sum([]byte(token))
hashedToken := hex.EncodeToString(hashToken[:])
a.cache[hashedToken] = user
livingTime := 60 * time.Minute
expiration := time.Now().Add(livingTime)
//кука будет жить 1 час
cookie := http.Cookie{Name: "token", Value: url.QueryEscape(hashedToken), Expires: expiration}
http.SetCookie(rw, &cookie)
http.Redirect(rw, r, "/", http.StatusSeeOther)
}
Попробуем подробнее остановиться, что у нас здесь изменилось.
- Во-первых, в случае успешной авторизации мы получаем нашего пользователя.
- Во-вторых, мы генерируем токен, который состоит у нас из хеша, полученного из соединенных значений логина, пароля и текущего времени.
- В-третьих, мы записываем токен и пользователя в кеш.
- И наконец, в-четвертых, мы создаем куку с токеном, которая будет жить один час.
Теперь доработаем метод authorized и отдельно добавим вспомогательную функцию для чтения cookies:
func (a app) authorized(next httprouter.Handle) httprouter.Handle {
return func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) {
token, err := readCookie("token", r)
if err != nil {
http.Redirect(rw, r, "/login", http.StatusSeeOther)
return
}
if _, ok := a.cache[token]; !ok {
http.Redirect(rw, r, "/login", http.StatusSeeOther)
return
}
next(rw, r, ps)
}
}
func readCookie(name string, r *http.Request) (value string, err error) {
if name == "" {
return value, errors.New("you are trying to read empty cookie")
}
cookie, err := r.Cookie(name)
if err != nil {
return value, err
}
str := cookie.Value
value, _ = url.QueryUnescape(str)
return value, err
}
Здесь у нас все максимально просто: если нет куки с токеном — идите авторизовываться, если токен есть, но его нет в кеше — идите авторизовываться.
Запускаем приложение через go run main.go, авторизуемся и видим, что приложение пускает и после перезагрузок больше не редиректит на страницу авторизации. А все потому, что теперь у нас есть бумажный браслет в виде куки с токеном!

К слову реализовать выход из сессии или logout мы можем просто через удаление куки и последующий редирект на страницу авторизации:
Добавляем новый эндпоинт — r.GET(«/logout», a.Logout):
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)
r.GET("/logout", a.Logout)
}
И реализовываем соответствующий метод-хендлер:
func (a app) Logout(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
for _, v := range r.Cookies() {
c := http.Cookie{
Name: v.Name,
MaxAge: -1}
http.SetCookie(rw, &c)
}
http.Redirect(rw, r, "/login", http.StatusSeeOther)
}
То есть для выхода, вам нужно просто перейти на страницу http://localhost:8080/logout
Текущая структура проекта:

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