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

Я уже немного проспойлерил в первой части, что здесь мы будем разбирать как сделать так, чтобы наше приложение запоминало авторизованных пользователей и не кошмарило их требованиями вводить логин и пароль по каждому чиху.

В первую очередь убираем использование переменной 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

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *