
Это базовое руководство описывающее схему работы "отложенной" авторизации. При которой права пользователю выдаются с задержкой по времени. Здесь мы разберем только базовый принцип и авторизацию.
Задача
Разработать сервис позволяющий авторизовываться "нестандартным" образом: код в СМС либо код в 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:

На этом, подготовительную часть можно считать завершенной.
Продолжение в следующей части