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

Заканчиваем очередной цикл простейшим примером регистрации пользователей на сайте. Для этого нам понадобятся:

  • страница с формой регистрации;
  • эндпоинт для приема данных с формы и выполнения какой-то логики;
  • метод, добавляющий пользователя в базу.

Начать предлагаю с конца — с создания метода для добавления нового пользователя в базу данных. Для этого в файле user.go добавьте следующий метод:

func (r *Repository) AddNewUser(ctx context.Context, name, surname, login, hashedPassword string) (err error) {
	_, err = r.pool.Exec(ctx, `insert into users (name, surname, login, hashed_password) values ($1, $2,$3, $4)`, name, surname, login, hashedPassword)
	if err != nil {
		err = fmt.Errorf("failed to exec data: %w", err)
		return
	}
	return
}

Здесь мы принимаем на вход имя, фамилию, логин и хеш пароля и пробуем добавить такого пользователя. Если вдруг у нас не получится (например, пользователь с таким логином уже существует), то мы вернем ошибку (кстати, за это отвечает сама БД: вспоминаем, что когда мы создавали табличку с пользователями, то для login мы прописали признак unique).

Теперь добавим новый эндпоинт, который будет направлять нас напрямую на страницу регистрации. Для этого идем в application.go и в методе Routes добавим следующее:

	r.GET("/signup", func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
		a.SignupPage(rw, "")
	})

Теперь Routes должна выглядеть так:

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)
	r.GET("/signup", func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
		a.SignupPage(rw, "")
	})
}

Добавим соответствующий метод:

func (a app) SignupPage(rw http.ResponseWriter, message string) {
	sp := filepath.Join("public", "html", "signup.html")
	tmpl, err := template.ParseFiles(sp)
	if err != nil {
		http.Error(rw, err.Error(), http.StatusBadRequest)
		return
	}
	type answer struct {
		Message string
	}
	data := answer{message}
	err = tmpl.ExecuteTemplate(rw, "signup", data)
	if err != nil {
		http.Error(rw, err.Error(), http.StatusBadRequest)
		return
	}
}

И обязательно html шаблон (public/html/signup.html):

{{define "signup"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Sign up page</title>
</head>
<body>
<form id="loginForm" name="signUpForm" action="/signup" method="post" >
    <label for="name">Имя</label><br>
    <input type="text" id="name" name="name" required><br>
    <label for="surname">Фамилия</label><br>
    <input type="text" id="surname" name="surname" required><br>
    <label for="login">Логин</label><br>
    <input type="login" id="login" name="login" required><br>
    <label for="password">Пароль</label><br>
    <input type="password" id="password" name="password" required><br>
    <label for="password2">Повторите пароль</label><br>
    <input type="password" id="password2" name="password2" required><br>
    <br>
    <button type="submit" name="submitBtn">Зарегистрироваться</button>
</form>
{{if . }}
<div>
    {{.Message}}
</div>
{{end}}
</body>
</html>
{{end}}

Запустим через go run main.go и перейдем на http://localhost:8080/signup

Теперь в application.go нужно добавить эндпоинт для приема данных с формы и метод-хендлер, который будет отвечать за внутреннюю логику.

В Routes добавляем r.POST(«/signup», a.Signup):

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)
	r.GET("/signup", func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
		a.SignupPage(rw, "")
	})
	r.POST("/signup", a.Signup)
}

Добавляем метод-хендлер:

func (a app) Signup(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
	name := strings.TrimSpace(r.FormValue("name"))
	surname := strings.TrimSpace(r.FormValue("surname"))
	login := strings.TrimSpace(r.FormValue("login"))
	password := strings.TrimSpace(r.FormValue("password"))
	password2 := strings.TrimSpace(r.FormValue("password2"))
	if name == "" || surname == "" || login == "" || password == "" {
		a.SignupPage(rw, "Все поля должны быть заполнены!")
		return
	}
	if password != password2 {
		a.SignupPage(rw, "Пароли не совпадают! Попробуйте еще")
		return
	}
	hash := md5.Sum([]byte(password))
	hashedPass := hex.EncodeToString(hash[:])
	err := a.repo.AddNewUser(a.ctx, name, surname, login, hashedPass)
	if err != nil {
		a.SignupPage(rw, fmt.Sprintf("Ошибка создания пользователя: %v", err))
		return
	}
	a.LoginPage(rw, fmt.Sprintf("%s, вы успешно зарегистрированы! Теперь вам доступен вход через страницу авторизации", name))
}

Что у нас здесь за портянка с кодом:

  • получаем данные с формы;
  • проверяем, что они не пустые;
  • сравниваем пароли;
  • генерируем хеш пароля;
  • записываем нового пользователя в базу;

Если что-то идет не так, мы отправляем нашего гостя обратно с сообщением, в чем ошибка. Ну а если, все хорошо, то поздравляем нового пользователя с регистрацией и перенаправляем его на страницу авторизации.

Для удобства добавим на странице авторизации (файл 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><br>
    <input type="login" id="login" name="login" autocomplete="on"><br>
    <label for="password">Пароль</label><br>
    <input type="password" id="password" name="password" autocomplete="on"><br>
    <br>
    <button type="submit" name="submitBtn">Войти</button>
</form>
{{if . }}
<div>
    {{.Message}}
</div>
{{end}}
<br>
<a href="/signup">Зарегистрироваться</a>
</body>
</html>
{{end}}

Проверяем:

Проверяем в базе, что пользователь действительно создался.

Структура проекта:

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

Прошу предложенное решение рассматривать исключительно в образовательных целях. Мы могли бы много чего в нем улучшить и сделать более безопасным, однако это бы только усложнило и без того непростой материал. Очень надеюсь, что статьи с моими уроками окажутся для кого-то полезными. Всем добра 🙂

10 комментариев

  1. Я новичок в мире Go. Хорошее приложение, много проясняет. Когда стал разбираться глубже, то думал что непонятным окажется type Repository struct {pool *pgxpool.Pool} и httprouter.Router , но нет ) Мне непонятно как впринципе работает это приложение.
    1. Я логинюсь, потом делаю логаут, потом снова логинюсь. Каким образом форма с login.html кнопкой «Войти» вызывает именно функцию Login() ? Я не вижу привязки ни на форме, ни где-либо к структуре документа в коде программы.
    2. Сколько может быть сессий у данного приложения?
    3. У нас есть куки и он хранится в «var dbpool *pgxpool.Pool» фукнции main(), а если заходит 2ой пользователь, то куда будет помещен 2ой куки? Программа создаст еще один экземпляр в памяти или это просто не предусмотрено программой?
    4. Почему в «func (a app) Login(rw, r *http.Request…)» строка r.FormValue(«login») находит введенное значение логина на странице .html ? func FormValue() обращается к структуре Request.(Form url.Values), там нет поля input со страницы .html, в котором логин вводился.
    5. В целом изначально пытался разобраться каким образом программой Go залогиниться на рандомный сайт, но совсем не понимаю что отправлять на сервер. Например, http.Post() , что в нем дожно уйти, чтобы сайт на той стороне видел значения логина и пароля, авторизовал их у себя и в *Response вернул токен?

    1. Привет!
      1. Веб интерфейс (HTML/CSS/JS) это по сути отдельный мир, который может и чаще всего существует отдельно. Вместо него или в дополнение могут существовать и другие варианты интерфейсов (декстопное приложение, мобильное приложение, чат боты и пр). В данном примере рассматривается встроенная возможность генерировать HTML страницы силами go, но они также могут быть сгенерированы отдельно, поэтому не рекомендую воспринимать веб интерфейс как часть приложения go.
      В нашем случае мы по нажатию на кнопку собираем введенные данные с формы и отправляем их на эндпоинт нашего web-сервера (на go) где это все принимается и далее по написанной логике обрабатывается.
      — Ввели данные
      — Нажали на кнопку — она собрала данные с конкретными именами (name=»login» и name=»password») и отправила методом POST (method=»post») на сервер (action=»/login»)
      — Бекенд принимает это по роуту r.POST(«/login», a.Login) и вызывает метод- обработчик (a.Login())
      — Внутри метода мы вытаскиваем полученные данные по имени r.FormValue(«login») и r.FormValue(«password») и далее уже их сверяем/проверяем, принимаем решение, пускать с такими кредами дальше или нет.

      Рекомендую перечитать статью (часть 2)

    2. 2. Если ты грамотно написал и оптимизировал все, то столько, сколько потянет железо (RAM/CPU), на котором ты запускаешь свое go приложение (бэкенд)
      3. Туда же в пул к остальным. Будет еще один экземпляр
      4. См ответ на вопрос 1
      5. Если ты сам пишешь и фронтэнд и бекенд, то ответ очевиден, так как ты задаешь правила игры. Если это чужой сайт, то нужно либо искать документацию по API, либо самому изучать как устроена текущая форма авторизации (куда и что отправляется), либо смотреть js код. Универсального ответа на этот вопрос не существует. Ну думаю, что владельцы сайтов, хотели бы чтобы к ним заходили программным методом, ведь это потенциальные DoS-атаки.

Leave a Comment

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