React Apollo, Gqlgen – authorization. Part 2
The article is divided into several parts:
– part 1
– part 3 – in progress
– sources of this part

In the last part, we deployed business logic methods, protected against CSRF, and set up a GraphQL server.
In this part, you need to authorize the Client and create listeners ready to accept user authorization. It is necessary to be able to remotely terminate the Client session.
Implementation
Simplified diagram:
-
When creating a connection, the Client receives
ClientID
-
The server creates a websocket session bound to
ClientID
-
Messages are sent to all listeners attached to
ClientID
-
Listeners
ClientID
can be at least 2, before transferring the connection to a new tab
Tasks
-
Session manager
-
How Gqlgen handles websocket
-
Creation of the Client and his observers
-
Save the Client session to a JWT token
-
Creating websocket listeners
-
Sending a websocket message
1. Session manager
We already have a session model /models/models_gen.go
generated from schema session.graphqls
… Create in the same directory a file with methods for secure work with the session /models/session.go
// Создает новую сессию
func NewSession() *Session {
return &Session{
// Идентификатором будет UUID
// go get github.com/google/uuid
// или go mod vendor при указании импорта
Sid: uuid.New().String(),
}
}
// Создаст сессию с существующим идентификатором
func NewSessionWithSid(sid string) *Session {
return &Session{
Sid: sid,
}
}
// Получить идентификатор сессии
func (s *Session) GetSid() (sid string, err error) {
if s.Sid == "" {
return "", fmt.Errorf("session: not found")
}
return s.Sid, nil
}
// Подтверждает активность клиента
func (s *Session) SetOnline() {
s.Online = true
return
}
// Сохраняет сессию в контекст
func (s *Session) WithContext(ctx context.Context) context.Context {
return context.WithValue(ctx, sessionCtxKey{"session"}, s)
}
// Ключ контекста
type sessionCtxKey struct {
name string
}
Create a new file session.go
in the directory pkg/store
// Обрабатывает сессию клиента
func (s *Store) SessionHandleClient(w http.ResponseWriter, r *http.Request) *http.Request {
// Получим контекст
ctx := r.Context()
// Создадим сессию
var sess *model.Session
// Проверим наличие токена c ClientID
cookie, err := r.Cookie("_sid")
if err != nil {
// Нет ClientID, создадим сессию
sess = model.NewSession()
} else {
// Тут должна быть логика валидации
// Но нам сейчас удобно видеть действительную запись
sess = model.NewSessionWithSid(cookie.Value)
// Клиент имеет ID
// Вебсокет не может устанавливать cookie,
// значит если идентификатор отсутствует,
// то возможные соединения websocket
// являются не авторизованными и должны быть отклонены
//
// В данный кейс попадает Клиент с ранее имеющимся
// ClientId, в этом случае соединение
// по websocket возможно настроим это
sess.SetOnline()
}
// Если есть ошибка – устанавливаем новые cookie
if err != nil {
// Получим ID клиента
sid, err2 := sess.GetSid()
if err2 != nil {
fmt.Printf(err.Error())
return r
}
// Создадим cookie
cookie = &http.Cookie{
Name: "_sid",
// Сid следует завернуть в токен, например JWT
Value: sid,
HttpOnly: true,
//Secure: true,
}
// Установим cookie
http.SetCookie(w, cookie)
}
// Сохраним сессию в контекст и вернем *http.Request
return r.WithContext(sess.WithContext(ctx))
}
Now we are able to handle the Client session.
Let’s create a method for handling HTTP requests in a file pkg/store/auth.go
and in it we connect the newly created method:
// Вызывается в AuthMiddleware
// Обрабатывает HTTP заголовки
// Проводит авторизации клиента и пользователя
func (s *Store) HandleAuthHTTP(w http.ResponseWriter, r *http.Request) *http.Request {
// Обработаем сессию клиента
r = s.SessionHandleClient(w, r)
return r
}
Must publish HandleAuthHTTP
as an HTTP router middleware. To do this, create a file auth.go
in the directory pkg/middleware
:
func AuthMiddleware(store *store.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Метод из Store, обрабатывает логику авторизации
r = store.HandleAuthHTTP(w, r)
next.ServeHTTP(w, r)
})
}
}
Let’s connect it in main.go
:
func main() {
// ...
router := mux.NewRouter()
// Подключим Auth middleware и передадим store в качестве параметра
router.Use(middleware.AuthMiddleware(store))
// ...
}
We start the server, open the browser. Now Customer has its identifier in the form cookie
With name _sid

2. How Gqlgen handles websocket
As the project develops, we are going to subscribe not only for authorization. It is necessary to implement a common subscription logic for all project entities.
It is important to consider:
-
ClientID
– can only be assigned to the browser -
Different browser tabs have their own unique websocket identifiers that are not associated with
ClientID
The client connecting via websocket must have a cookie with ClientID
which it receives after the GET request. We cut off requests that do not have authorization: the parameters can be found from model.Session
which we extract from the context – it is necessary to create a method.
The frontend receives authorization status on the websocket, listening to changes in subscription.auth
… For these purposes, the server has Auth method processing subscriptionResolver
… Using his example, we will analyze the process of adding and removing a listener. To do this, add 2 methods v models/session
:
// Получает сессию из контекста
func SessionFromContext(ctx context.Context) (*Session, error) {
if meta := ctx.Value(sessionCtxKey{"session"}); meta != nil {
return meta.(*Session), nil
}
return nil, fmt.Errorf("meta: not found")
}
// Подтверждает активность клиента
func (s *Session) CheckOnline() bool {
return s.Online
}
Let’s edit the subscription method itself Auth
, it imports AuthWebsocket – from the Store. Let’s open the file /pkg/store/auth.go
and edit this method:
// Авторизовывает websocket
// Создает сессию
// Обрабатывает подключение и создает канал
//
// Каждый клиент вызывавший данный
// метод – является уникальным
func (r *Store) AuthWebsocket(ctx context.Context) (<-chan *model.Auth, error) {
// Получим сессию из контекста
sess, err := model.SessionFromContext(ctx)
if err != nil {
// Если произошла ошибка то не стоит здесь
// ее отправлять дальше.
//
// Ее нужно логировать и вернуть на фронт
// что-то более обобщенное
fmt.Printf("Auth subscriptionResolver. %v", err)
return nil, gqlerror.Errorf("internal error")
}
// Проверим инициатора запроса.
// Если запрос поступил по вебсокет и от Клиента
// ранее не имеющего ClientID – не обрабатываем его
if ok := sess.CheckOnline(); !ok {
// Если клиент не имеет авторизации
return nil, gqlerror.Errorf("unauthorized")
}
// Подключившийся клиент – уникален
// Создадим websocket ID
wsid := uuid.New().String()
// Создаем канал в который будем писать сообщения
in := make(chan *model.Auth)
// Выведем в терминал сообщение при подключении
fmt.Printf("WS connect. ID: %vn", wsid)
// Обработаем остановку соединения
go func() {
// Чтобы узнать об отключении websocket
// достаточно слушать сигнал из контекста
<- ctx.Done()
fmt.Printf("WS disconnect. ID: %vn", wsid)
}()
// Тестовая публикация сообщения
go func() {
// Сразу опубликуем сообщение
in <- &model.Auth{
ClientID: time.Now().String(),
}
// Небольшая задержка и отправим следующее
time.Sleep(time.Second * 2)
in <- &model.Auth{
ClientID: time.Now().String(),
}
}()
// Вернем канал
return in, nil
}
Let’s start the server, open http: // localhost: 2000 / in the Playground window that opens, execute the request:
subscription{
auth{
client_id
}
}
We receive 2 messages with a delay:

Now let’s check the answer if Customer is not authorized. On connection, the browser received a cookie with ClientID
, go to the developer tools, delete the cookie and try to connect again:

We have analyzed the subscription mechanism in Gqlgen, now you can proceed to the stage of assembling the listener.
3. Creation of the Client and his observers
The structure of the Client and its listener:
-
Client
: unites listeners into oneClientID
… In our context, the browser -
Observer
: is the direct recipient of messages – different browser tabs. Has its own unique identifier
An important point!
Observer
-th, it can be not only a new tab. But also a new request in the current tab.
In the diagram:

Observer – browser tab
Blue denotes connections Client-1
… We see that through Observer-1
there are 2 channels. This is due to the lack of an identifier on the Observer.
How do we decide?
When creating a new listener, we create a session with our own identifier SessionID
…
-
Create a session for each browser tab
-
Create a session, only for websocket connections
-
Client receives
Session-ID
in websocket response -
Session active – while the connection is active
-
If Customer It has
SessionID
, then for any requests includes it in the HTTP header:Session-ID
Scheme after creating the identifier:

Let’s get down to implementation
Let’s open the file /pkg/store/session.go
, we are interested in code in method SessionHandleClient
:
func (s *Store) SessionHandleClient(w http.ResponseWriter, r *http.Request) *http.Request {
// ...
// Проверим наличие токена c ClientID
cookie, err := r.Cookie("_cid")
if err != nil {
// Нет ClientID, создадим сессию
sess = model.NewSession()
} else {
// ...
}
// ...
}
Here, in the absence of the Client’s session, we create a new session, then save it into the context. In case the client has ClientID
, we create a session again with ClientID
obtained from the cookie.
It would be wiser if, in the presence of a cookie, we do not create, but receive an existing session.
It is not beneficial for us to create a server-side storage of session data. So we will keep it with the client. Two options would be reasonable here:
-
Save session to cookie as JSON
-
Save session to JWT token
4. Save the Client session to a JWT token
In the last chapter, we discussed the need to create a session for each listener, and also came to the conclusion that it needs to be saved somewhere.
At first glance, the simplest solution is to store the session in a cookie. But we need a session tied to a specific tab. In the case of a cookie, it will become visible to the entire browser: you will have to create unique cookies. In the case of unique cookies, how do you clear them? We take into account that websocket does not work with cookies and will not be able to delete them after unsubscribing.
In order not to invent: save the session to a JWT token and pass it To the client… Next time you contact Customer: will send the token in the HTTP header Session-ID
…
Why Session-ID?
1. We can store the session on the side Server and upon receiptSession-ID
request it from the repository
2. Technically, a JWT token is not a session. In encrypted form, it is useless for the Client
JWT validation
Token validation states:
-
Token invalid
– not working: when what came is not a token at all -
Claims invalid
– not working: when it was not possible to extract the payload -
Expired
– suitable for further validation: in this case, the token must be updated. Possibly after additional session validation -
Valid
– working token
When developing authorization User, we need the parameter
Expired
… In this case: the token will have to contain the field:AccessToken
… We will use it in the session storage at Server… And if it is valid, we will update the JWT token.– This is how OAuth works
We will not delve into the structure of the token. Moreover, there is a good article:
Five Easy Steps to Understand JSON Web Tokens (JWT)
We are interested in how to work with JWT in Golang, for this we take the popular package jwt-go… Let’s add it to the project:
go get github.com/dgrijalva/jwt-go
Let’s create a file /pkg/token/jwt.go
:
package token
import (
"fmt"
"github.com/dgrijalva/jwt-go"
model "react-apollo-gqlgen-tutorial/backoffice/models"
"time"
)
// Создадим структуру Jwt
type Jwt struct {
SecretKey string
Issuer string
Expiration int64
}
// Опции при генерации токена
type JwtClaims struct {
// Нужен для обновления токена
AccessToken string
// Прикрепим сессию
Sess *model.Session
jwt.StandardClaims
}
// Генерация токена
func (j *Jwt) Generate(opt JwtClaims) (token string, err error) {
// Получим Claims
claims := &opt
// Инициализация StandardClaims
//
// Здесь "подключаются" все настройки
// Необходимые для валидации токена
//
// Указываются при инициализации структуры Jwt
claims.StandardClaims = jwt.StandardClaims{
ExpiresAt: time.Now().Local().Add(time.Second * time.Duration(j.Expiration)).Unix(),
Issuer: j.Issuer,
}
// Генерация токена
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return t.SignedString([]byte(j.SecretKey))
}
// Опции при валидации токена
type JwtValidateOptions struct {
Token string
}
// Валидация токена
func (j *Jwt) Validate(opt JwtValidateOptions) (claims *JwtClaims, err error) {
// Попробуем получить полезную нагрузку
token, err := jwt.ParseWithClaims(
opt.Token,
&JwtClaims{},
func(token *jwt.Token) (interface{}, error) {
return []byte(j.SecretKey), nil
},
)
// Полезной нагрузки нет
// Что-то явно не валидное – вернем ошибку
if token == nil {
return nil, fmt.Errorf("token invalid")
}
// Получим Claims
claims, ok := token.Claims.(*JwtClaims)
if !ok {
return nil, fmt.Errorf("error token claims")
}
// Проверим срок жизни токена
if j.Expiration > 0 && claims.ExpiresAt < time.Now().Local().Unix() {
// Если токен протух
// Вернем полезную нагрузку вместе с ошибкой
//
// Для дальнейшей валидации:
// claims будет содержать AccessToken
return claims, fmt.Errorf("token is expired")
}
return claims, nil
}
// Опции структуры Jwt
type JwtOptions struct {
SecretKey string
Issuer string
ExpSeconds int64
}
func NewJwt(opt JwtOptions) *Jwt {
return &Jwt{
SecretKey: opt.SecretKey,
Issuer: opt.Issuer,
Expiration: opt.ExpSeconds,
}
}
Now we can save the session in a token, let’s go back to the file /pkg/store/session.go
… Let’s add a method validating a token and a method validating a listener’s session:
// Валидирует сессию слушателя
func (s *Store) ValidateClientSession(ctx context.Context) (sessionID string, err error) {
// Получим сессию из контекста
sess, err := model.SessionFromContext(ctx)
if err != nil {
return "", fmt.Errorf("internal error")
}
if ok := sess.CheckOnline(); !ok {
// Если клиент не авторизован: SessionID отсутствует
// Создадим SessionID, и отправим клиенту
sessionToken, err2 := s.token.SessionID.Generate(token.JwtClaims{
Sess: sess,
})
if err2 != nil {
fmt.Println(err2)
return "", fmt.Errorf("internal error")
}
return sessionToken, nil
}
return "", nil
}
// Валидирует токен слушателя
func (s *Store) ValidateSessionToken(sid string) (*model.Session, error) {
// Валидируем токен
// Считаем токен не валидным если нет claims
if claims, _ := s.token.SessionID.Validate(token.JwtValidateOptions{
Token: sid,
}); claims != nil {
sess := claims.Sess
// Сессию получили из заголовка: клиент онлайн
sess.SetOnline()
// Сохраним сессию в контекст
return sess, nil
}
return nil, fmt.Errorf("invalid session token")
}
In the same file /pkg/store/session.go
, let’s move on to the method SessionHandleClient
, let’s edit it like this:
// Обрабатывает сессию клиента
func (s *Store) SessionHandleClient(w http.ResponseWriter, r *http.Request) *http.Request {
// Получим контекст
ctx := r.Context()
// Сюда запишем сессию, если сработает кейс
var sess *model.Session
var ClientID string
// Проверим наличие cookie c ClientID
cookie, err := r.Cookie("_cid")
if err == nil {
ClientID = cookie.Value
// У клиента есть ClientID
// 1. Проверим наличие заголовка Session-ID
// 2. Получаем токен и валидируем его
// 2.1. Токен валидный: сохраним сессию из токена
// 2.2. Токен протух: сохраним сессию из токена
// 2.3. Токен Invalid: создадим новую сессии
// Ищем заголовок Session-ID
if t := r.Header.Get("Session-ID"); t != "" {
// Нашли сессию
if ss, err2 := s.ValidateSessionToken
sess = ss
}
}
// Этот метод теперь удален
//sess = model.NewSessionWithSid(cookie.Value)
}
// Если сессии нет: создаем сессию
if sess == nil {
sess = model.NewSession()
if ClientID != "" {
sess.AddClientID(ClientID)
}
}
// Если есть ошибка при чтении cookie
if err != nil {
// Получим ID клиента
cid, err2 := sess.GetSid()
if err2 != nil {
fmt.Printf(err.Error())
return r
}
// Создадим cookie
cookie = &http.Cookie{
Name: "_cid",
Value: cid,
HttpOnly: true,
//Secure: true,
}
// Установим cookie
http.SetCookie(w, cookie)
}
// Сохраним сессию в контекст и вернем *http.Request
return r.WithContext(sess.WithContext(ctx))
}
We receive HTTP headers. But websocket is authorized differently. Let’s open the file /pkg/graph/resolver.go
… Here you need to accept the title Session-ID
, validate the token, if successful: get the session and save it to the context.
func NewServer(opt Options) *handler.Server {
// ...
srv.AddTransport(transport.Websocket{
// ...
InitFunc: transport.WebsocketInitFunc(func(ctx context.Context, initPayload transport.InitPayload) (context.Context, error) {
// Тут обрабатываются websocket соединения
// Получим заголовок "Session-ID"
if sid, ok := initPayload["Session-ID"]; ok {
if sess, err := opt.Store.ValidateSessionToken(sid.(string)); err == nil {
// Сохраним сессию в контекст
ctx = sess.WithContext(ctx)
}
}
return ctx, nil
}),
})
// ...
}
Authorization methods
We prepared everything, now we need to get somewhere token SessionID
… For any request model.Auth
, you need to create a session. Except when it is available.
Every request model.Auth
: returns the current authorization state, the method is responsible for it Auth()
in file /pkg/store/auth.go
… Let’s open and edit it:
// Возвращает состояние Auth исходя из текущего контекста
func (s *Store) Auth(ctx context.Context) (*model.Auth, error) {
// создадим модель
auth := &model.Auth{}
// Проверим сессию
sid, err := s.ValidateClientSession(ctx)
if err != nil {
return nil, gqlerror.Errorf("internal error")
}
// Если есть sid – добавим его к Auth
if sid != "" {
auth.AddSessionId(sid)
}
// Отправим текущее состояние
return auth, nil
}
We will also edit the websocket subscription method model.Auth
:
func (s *Store) AuthWebsocket(ctx context.Context) (<-chan *model.Auth, error) {
// Получим текущее состояние авторизации
auth, err := s.Auth(ctx)
if err != nil {
return nil, gqlerror.Errorf("internal error")
}
// Создаем канал в который будем писать сообщения
ch := make(chan *model.Auth)
// Нужно вернуть текущее состояние
go func() {
ch <- auth
}()
// Вернем канал
return ch, nil
}
For the purity of the experiment
Let’s add a websocket listener for User
… Let’s open the file schema.graphqls
and add a method:
"""
Подписки на websocket
"""
type Subscription {
"""
Добавлен метод:
Подписка на Auth
"""
user: User!
"""
Подписка на Auth
"""
auth: Auth!
}
Let’s enter the command to generate laughter in the terminal:
go run cmd/gqlgen.go
Move the generated method:User(ctx context.Context) (<-chan *model.User, error)
v /pkg/graph/user
and edit it:
func (r *subscriptionResolver) User(ctx context.Context) (<-chan *model.User, error) {
user := make(chan *model.User)
// Получим сессию из контекста
sess, err := model.SessionFromContext(ctx)
if err != nil {
return nil, gqlerror.Errorf("internal error")
}
fmt.Printf("User. Session: %vn", sess.Sid)
return user, nil
}
We will also add similar code to the subscription method for Auth
:
func (s *Store) AuthWebsocket(ctx context.Context) (<-chan *model.Auth, error) {
// Получим сессию из контекста
sess, err := model.SessionFromContext(ctx)
if err != nil {
return nil, gqlerror.Errorf("internal error")
}
fmt.Printf("Auth. Session: %vn", sess.Sid)
// ...
}
Let’s open a terminal:
go run cmd/main.go
In the browser: http: // localhost: 2000 /
We enter the request:
query{
auth{
sessionId
}
}
We get the token:

Now we connect to Auth
using the given token. There is a tool for this in the Playground:

We enter the request:
subscription{
auth{
sessionId
}
}
Next, we enter the second:
subscription {
user{
uid
}
}
We look at the terminal, and we see that Auth
and User
have the same identifier. This means that the session is created and read:

We have created a session for each window, now we need to create the connection handler itself.
5. Creating websocket listeners
When subscribing a new listener Auth
or User
necessary:
-
Receive:
ClientID
andSessionID
-
Create Client by
ClientID
-
Create a listener by
SessionID
When unsubscribing:
-
Remove listener
Auth
orUser
-
Check for the presence of other listeners, in case of absence: completely delete the record with the Client’s session
We want to have a unified handler for Auth
and User
or for other entities of the project. Therefore, we cannot know the type of the received channel or message. To solve this problem, we will take an empty interface as a type and extract a channel according to its type.
Let’s create a file /pkg/websocket/observer.go
channel processing type:
type observer struct {
auth chan *model.Auth
user chan *model.User
}
func (o *observer) Add(ch interface{}) error {
// Получим тип из интерфейса
switch ch.(type) {
case chan *model.Auth:
o.auth = ch.(chan *model.Auth)
return nil
case chan *model.User:
o.user = ch.(chan *model.User)
return nil
default:
// Тип не обнаружен
return fmt.Errorf("observer: unknown type")
}
}
// Удаляет наблюдателя,
// если вернет true - можно удалить слушатель
func (o *observer) Delete(ch interface{}) bool {
// Получим тип из интерфейса
switch ch.(type) {
case chan *model.Auth:
o.auth = nil
case chan *model.User:
o.user = nil
}
return o.checkEmpty()
}
// Вернет истину если нет слушателей
func (o *observer) checkEmpty() bool {
switch {
case o.auth != nil:
return false
case o.user != nil:
return false
}
return true
}
The listeners are united by the Client, let’s create the /pkg/websocket/client.go file:
type client struct {
observers map[string]*observer
mu sync.Mutex
}
// Добавляет слушателя Клиента
func (c *client) Add(sid string, ch interface{}) error {
// Заблокируем мапу слушателей
// чтобы безопасно с ней работать работать
c.mu.Lock()
// Разблокируем мапу после выхода из функции
defer c.mu.Unlock()
// Поищем слушателя
obs, ok := c.observers[sid]
if !ok {
// Слушатель не найден, создадим
obs = &observer{}
// Добавим в мапу
c.observers[sid] = obs
}
err := obs.Add(ch)
if err != nil {
return err
}
return nil
}
// Удаляет слушателя
// Возвращает признак наличия других слушателей
func (c *client) Delete(sid string, ch interface{}) bool {
c.mu.Lock()
defer c.mu.Unlock()
obs, ok := c.observers[sid]
if !ok {
// Обсервер не найден?
fmt.Println("panic")
}
// Удаляем канал
if ok = obs.Delete(ch); ok {
// Если вернулся признак пустоты
// Удалим слушатель
delete(c.observers, sid)
}
// Посчитаем количество слушателей
// и вернем результат
return len(c.observers) == 0
}
func newClient() *client {
return &client{
observers: make(map[string]*observer),
}
}
There is a manager file with a single method that creates a new listener /pkg/websocket/manager.go
:
type Websocket struct {
clients map[string]*client
// Защищаем мапу
mu sync.Mutex
}
// Создает Клиента
func (w *Websocket) NewObserver(ctx context.Context, ch interface{}) error {
// Заблокируем мапу clients
// чтобы безопасно с ней работать работать
w.mu.Lock()
// Разблокируем мапу после выхода из функции
defer w.mu.Unlock()
// Получим сессию из контекста
sess, err := model.SessionFromContext(ctx)
if err != nil {
return err
}
cid := sess.ClientID
sid := sess.Sid
// Найдем, или создадим клиента
cli, ok := w.clients[cid]
if !ok {
// Клиент не найден, создадим
cli = newClient()
// Добавим в мапу
w.clients[cid] = cli
}
// Добавим слушателя клиента
err = cli.Add(sid, ch)
if err != nil {
return err
}
// Клиент отписывается – удаляем слушатель
go func() {
<- ctx.Done()
cli.Delete(sid, ch)
}()
return nil
}
func New() *Websocket {
return &Websocket{
clients: make(map[string]*client),
}
}
We have created a websocket listener manager, it remains to connect it.
Let’s open the file /pkg/store/auth.go, and add the created manager to the method AuthWebsocket
implementing a websocket connection for Auth
:
func (s *Store) AuthWebsocket(ctx context.Context) (<-chan *model.Auth, error) {
// Получим текущее состояние авторизации
auth, err := s.Auth(ctx)
if err != nil {
fmt.Println(err)
return nil, gqlerror.Errorf("internal error")
}
// Создаем канал в который будем писать сообщения
ch := make(chan *model.Auth)
// Подключим канал к менеджеру websocket
err = s.websocket.NewObserver(ctx, ch)
if err != nil {
fmt.Println(err)
return nil, gqlerror.Errorf("internal error")
}
// Нужно вернуть текущее состояние
go func() {
ch <- auth
}()
// Вернем канал
return ch, nil
}
Everything is ready, now our connections are bound to ClientID and SessionID
6. Sending a message via websocket
We have created listeners, it remains to implement methods for sending messages to listeners according to their destination.
Let’s open the file /pkg/websocket/observer.go
and add the method Send
:
func (o *observer) Send(ch interface{}) {
// Получим тип из интерфейса
switch ch.(type) {
case *model.Auth:
// Валидируем канал
if o.auth == nil {
fmt.Println("Auth sending error")
return
}
// Отправляем сообщение
o.auth <- ch.(*model.Auth)
case *model.User:
// Валидируем канал
if o.user == nil {
fmt.Println("User sending error")
return
}
// Отправляем сообщение
o.user <- ch.(*model.User)
default:
fmt.Println("unknown message type")
}
}
Next, let’s create a method in /pkg/websocket/client.go
:
func (c *client) Send(ch interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
// Необходимо отправить сообщение всем слушателям
// Пройдемся в цикле и запустим отправку
//
// Лучше всего это сделать в отдельной горутине
// Создадим WaitGroup
// Про применение описано в этой статье:
// https://habr.com/ru/company/otus/blog/557312/
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
for _, obs := range c.observers {
obs.Send(ch)
}
wg.Done()
}()
wg.Wait()
}
Now let’s add Send
to the manager /pkg/websoket/manager.go
:
func (w *Websocket) Send(ctx context.Context, ch interface{}) error {
w.mu.Lock()
defer w.mu.Unlock()
// Получим сессию из контекста
sess, err := model.SessionFromContext(ctx)
if err != nil {
return err
}
// Получим ClientID
cid := sess.ClientID
// Найдем клиента
cli, ok := w.clients[cid]
if !ok {
return fmt.Errorf("client not found")
}
// Отправляем сообщение
cli.Send(ch)
return nil
}
Sending Auth over websocket
We are now ready to send messages on the websocket. Implementing the method for sending the current state Auth
To the client:
func (s *Store) SendAuth(ctx context.Context) error {
// Получим текущее состояние
auth, err := s.Auth(ctx)
if err != nil {
return err
}
// Todo: удалить!!!
// Чтобы увидеть результат изменений
// Нужно что нибудь рандомное
auth.Method = time.Now().String()
if err = s.websocket.Send(ctx, auth); err != nil {
return err
}
return nil
}
Testing the connection
For the test, let’s change the method User
requesting user, in file /pkg/store/user.go
:
func (s *Store) User(ctx context.Context) (*model.User, error) {
err := s.SendAuth(ctx)
fmt.Println("Запрашиваем Auth из метода User")
fmt.Printf("Ошибка: %vn", err)
// ...
return &model.User{
Username: "LOLO",
}, nil
}
We start the server:
go run cmd/main.go
We open: http: // localhost: 2000 /
We execute the request:
subscription{
auth{
sessionId,
method
}
}
Open a new tab in the Playground, place the resulting token in the header:
{
"Session-ID": "TOKEN"
}
We execute the request:
query{
user{
username
}
}
We get the result in the auth tab:

Browser tabs, and other browsers: test it yourself.
This concludes this part. In the next part, we will develop a mechanism for delivering authorization to the User and deploy the frontend on React + Apollo.