
Это базовое руководство описывающее схему работы «отложенной» авторизации. При которой права пользователю выдаются с задержкой по времени. Здесь мы разберем только базовый принцип и авторизацию.
Задача
Разработать сервис позволяющий авторизовываться «нестандартным» образом: код в СМС либо код в URL. Пароль у user отсутствует.
Кейс
Пользователь в форме входа вводит свой Username, и в зависимости от настроек метода авторизации получает:
-
Письмо с кнопкой на емайл, при ее активации – происходит авторизация. Почтовый клиент может находится на другом устройстве
-
Открывается форма ввода кода из СМС, при успехе пользователь авторизовывается
Проблема
Мы не знаем когда пользователь нажмет на кнопку. Но в случае ее активации фронтенд должен понять что авторизация произошла, выполнить HTTP запрос и получить токен.
Пользователь получает токен сессии по предьявлению – идентификатора клиента ClientID.
Как решать?
-
При первом запросе
браузерКлиент получает cookie c идентификаторомClientID -
При отправке Пользователем формы авторизации, клиенту возвращается токен авторизации
Auth_token– обменивается на токен Пользователя -
Создается авторизационная сессия, хранит
Auth_token,ClientIDиUserID -
Пользователь подтверждает авторизацию. Ищем клиента по
ClientIDв соединениях websocket – отправляем сигнал об авторизации -
Клиент получил сигнал о наличии авторизации. Выполняется GET-запрос с токеном
Auth_tokenв HTTP-заголовке Authorization. ИщемAuth_tokenв сессиях, в случае успеха авторизовываем Пользователя
Статья разбита на несколько частей:
— часть 2
— часть 3 — в работе
— исходники этой части
Подготовка
-
Развернем Gqlgen
-
Напишем схемы GraphQL, сгенерируем модели и методы АПИ
-
Настроим CORS
-
GraphQL сервер
-
Реализуем websocket соединение
-
Store
-
Первый запуск
1. Развернем Gqlgen
Способов развернуть проект на Gqlgen существует не мало. Мы выбрали тот, который нам кажется наиболее простым.
Структура:
-
/backoffice/cmd– основные файлы -
/backoffice/graph– файлы GraphQL -
/backoffice/schema– GraphQL схемы -
/backoffice/models– сгенерированные модели GraphQL -
/backoffice/pkg– библиотека
Создадим структуру, в директории backoffice, выполним:
go mod init react-apollo-gqlgen-tutorial/backoffice go get github.com/99designs/gqlgen go run github.com/99designs/gqlgen init
Отредактируем файл gqlgen.yml:
# Изменим экспорт файлов схемы schema: - schema/*.graphqls # Изменим расположение моделей model: filename: models/models_gen.go package: model # Добавим исключение # Чтобы генератор моделей не перезатирал уже имеющееся autobind: - "react-apollo-gqlgen-tutorial/backoffice/models"
Переименуем файл server.go в main.go и поместим его в /backoffice/cmd.
В директории /backoffice/cmd создадим файл gqlgen.go:
// +build tools – не просто комментарий, подробнее здесь
// +build tools package main import ( "fmt" "github.com/99designs/gqlgen/cmd" ) func main() { fmt.Println("Building Graphql schema") cmd.Execute() }
Этот файл позволит генерировать модели из схемы GraphQL выполнением команды:
go run cmd/gqlgen.go
2. Напишем схемы GraphQL, сгенерируем модели и методы АПИ
Необходимо сгенерировать 3 модели данных: Auth, Session, User и методы GraphQL.
Schema schema/schema.graphqls:
""" Запросы GET """ type Query { """ Первый запрос. Необходим для авторизации Вызывается при наличии флага Auth.authorized 1. Если есть Auth.auth_token и Auth.authorized передаем его в HTTP заголовке Authorization 2. Если есть Auth.client_id передаем в заголовке Client-ID """ auth: Auth! user: User! } """ Запросы POST """ type Mutation { """ Метод авторизации. Принимает username Вернет Auth.auth_token, должен быть в HTTP заголовке Authorization при получении токена пользователя """ authorization(login: String!): Auth! smsCode(code: String!): Auth! } """ Подписки на websocket """ type Subscription { """ Подписка на Auth """ auth: Auth! }
Session schema/session.graphqls:
type Session { """ Токен авторизации, должен совпасть с тем что отдали клиенту при отправке формы авторизации """ auth_token: String! """ Идентификатор пользователя """ uid: Int! """ Метод авторизации установленный пользователем """ method: String! }
Auth schema/auth.graphqls:
type Auth { """ Должен быть предъявлен при запросе токена пользователя Получаем в ответе на GET запрос формы авторизации Отправляем в HTTP заголовке Client-ID """ client_id: String! """ Должен быть предъявлен при запросе токена пользователя Получаем в ответе на GET запрос формы авторизации Отправляем в HTTP заголовке Authorization """ token: String! """ Указывает на наличие авторизации """ authorized: Boolean! """ Метод авторизации """ method: String! }
На User можно посмотреть здесь
Генерация моделей и методов
После того как изменения в файлах схем GraphQL были сохранены, выполним команду:
go run cmd/gqlgen.go
Эта команда создаст /backoffice/models но нам интересна директория /backoffice/graph.
backoffice/graph:
-
Удаляем папку models в
/backoffice/graph -
Перемещаем файлы:
resolver.goиschema.resolvers.goв/backoffice/pkg/graph
backoffice/pkg/qraph:
Оредактируем файл schema.resolvers.go заберем методы относящиеся к *model.Auth и *model.User . Поместим их в auth.go и user.go текущего каталога
Затянем зависимости:
go mod vendor
Получившаяся структура:

3. Настроим CORS
Создадим базовую защиту от CSRF атак
func CorsMiddleware() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Разрешаем подключаться только конкретному хосту // Заголовко всегда должен возвращать адрес действительного хоста // Устанавливая звездочку "*" - создаем уязвимость в безопасности w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000") // Разрешаем принимать файлы cookie только от http://localhost:3000 // // Подробнее про сочетания // Access-Control-Allow-Origin и Access-Control-Allow-Credentials // В этой таблице: // https://fetch.spec.whatwg.org/#cors-protocol-and-credentials w.Header().Set("Access-Control-Allow-Credentials", "true") // Разрешаем использовать методы w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") // Блокируем возможность CSRF // Access-Control-Allow-Headers = Content-Type // Подробнее здесь: // https://fetch.spec.whatwg.org/#concept-header w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization") if r.Method == "OPTIONS" { return } next.ServeHTTP(w, r) }) } }
Для комплексной защиты необходимо устранить XSS-уязвимости.
Интересная статья на эту тему англ.
4. GraphQL сервер
Мы уже переместили и структурировали файлы в /pkg/qraph. Теперь необходимо поправить resolver.go:
var ( mb int64 = 1 << 20 ) type Resolver struct{} // Создадим функцию NewServer func NewServer(opt Options) *handler.Server { // Переместим создание сервера из cmd/main.go srv := handler.New( generated.NewExecutableSchema( generated.Config{ Resolvers: &Resolver{}, }, ), ) srv.AddTransport(transport.MultipartForm{ MaxMemory: 32 * mb, MaxUploadSize: 50 * mb, }) srv.AddTransport(transport.POST{}) srv.AddTransport(transport.GET{}) srv.AddTransport(transport.Websocket{ KeepAlivePingInterval: 10 * time.Second, Upgrader: websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, ReadBufferSize: 1024, WriteBufferSize: 1024, }, InitFunc: transport.WebsocketInitFunc(func(ctx context.Context, initPayload transport.InitPayload) (context.Context, error) { return ctx, nil }), }) srv.Use(extension.Introspection{}) return srv } type Options struct {}
5. Websocket соединение
Для реализации websocket возьмем пакет Gorilla websocket и для работы с HTTP – Gorilla mux
Изменим cmd/main.go:
import ( //... "react-apollo-gqlgen-tutorial/backoffice/pkg/graph" "github.com/gorilla/mux" ) var ( defaultPort = "2000" ) func main() { port := os.Getenv("PORT") if port == "" { port = defaultPort } // Создадим GraphQL сервер srv := graph.NewServer(graph.Options{}) // Создадим роутер router := mux.NewRouter() // Подключим CORS middleware router.Use(middleware.CorsMiddleware()) router.Handle("/", playground.Handler("GraphQL playground", "/graphql")) router.Handle("/graphql", srv) log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) log.Fatal(http.ListenAndServe(":"+port, router)) }
6. Store
В нашей логике взаимодействует несколько сущностей:
-
Session: хранит данные об отложенной и пользовательской сессиях, работает с клиентом -
Auth: осуществляет авторизацию клиента и пользователя -
User: работает с пользователем
Store по факту является менеджером, управляющим различными сущностями проекта выполняющие общую логику.
Опишем Store /backoffice/pkg/store:
package store type Store struct { token TokenOptions } func NewStore(opt Options) *Store { return &Store{ token: opt.Token, } } type Options struct { Token TokenOptions } type TokenOptions struct {}
Создадим методы необходимые для работы сервера GraphQL.
Методы Auth:
// pkg/store/auth.go // Возвращает состояние Auth исходя из текущего контекста func (s *Store) Auth(ctx context.Context) (auth *model.Auth, err error) { // ... return } // Авторизовывает websocket, обрабатывает подключение и создает канал func (s *Store) AuthCreateWebsocket(ctx context.Context) (out <-chan *model.Auth, err error) { // ... return } // Авторизация по Username func (s *Store) AuthorizeForUsername(ctx context.Context, login string) (auth *model.Auth, err error) { // ... return } // Подтверждение авторизации из СМС сообщения func (s *Store) AuthSMSApprove(ctx context.Context, code string) (auth *model.Auth, err error) { // ... return }
Методы User:
// pkg/store/user.go // Вернет User согласно текущего состояния авторизации func (s *Store) User(ctx context.Context) (user *model.User, err error) { // ... return }
Подключим Store в pkg/graph/resolver.go:
type Resolver struct{ store *store.Store } func NewServer(opt Options) *handler.Server { srv := handler.New( generated.NewExecutableSchema( generated.Config{ Resolvers: &Resolver{ store: opt.Store, }, }, ), ) // ... } type Options struct { Store *store.Store }
Отредактируем pkg/graph/auth.go и pkg/graph/user.go
// pkg/graph/auth.go func (r *queryResolver) Auth(ctx context.Context) (*model.Auth, error) { return r.store.Auth(ctx) } func (r *subscriptionResolver) Auth(ctx context.Context) (<-chan *model.Auth, error) { return r.store.AuthCreateWebsocket(ctx) } func (r *mutationResolver) Authorization(ctx context.Context, login string) (*model.Auth, error) { return r.store.AuthorizeForUsername(ctx, login) } func (r *mutationResolver) SmsCode(ctx context.Context, code string) (*model.Auth, error) { return r.store.AuthSMSApprove(ctx, code) } // pkg/graph/user.go func (r *queryResolver) User(ctx context.Context) (*model.User, error) { return r.store.User(ctx) }
7. Первый запуск
В терминале набираем команду:
go run cmd/main.go
В адресной строке: http://localhost:2000/
Открывается GraphQL playground:

На этом, подготовительную часть можно считать завершенной.
Продолжение в следующей части
ссылка на оригинал статьи https://habr.com/ru/post/598359/
Добавить комментарий