JSON Web Token (JWT) – токен, который содержит минимально необходимую информацию для аутентификации и авторизации. В зашифрованном виде выглядит, как строка, которая состоит из трех частей:

- Header (заголовок). Содержит тип токена (в данном случае это JWT), и какой используется алгоритм шифрования.
- Payload (нагрузка). Содержит всевозможные данные (например, информацию о пользователе или время, через которое токен будет не действителен).
- Signature (подпись). Нужна для проверки, что токен не подделан и выдан именно вашим сервисом (проверка валидности токена).
В сравнении с обычным токеном, JWT самодостаточен (self-contained), потому что содержит минимально необходимую информацию о пользователе. Благодаря этому не нужны повторные обращения к базе данных. Обычный же токен – рандомная строка, и валидность этого токена проверяется через запросы к базе данных.
JWT компактен. Его легко можно передать через URL или HTTP header.
Процесс аутентификации через JSON Web Token

Шаг 1. Пользователь вводит данные (имя пользователя, пароль, т.д.) и логинится в системе.
Шаг 2. Сервер создает JWT, в котором будет зашифрована информация о текущем пользователе.
Шаг 3. Сервер возвращает созданный токен пользователю. Этот токен нужно сохранить на локальном компьютере (в куках или local storage), иначе придется постоянно логиниться и получать токен по-новому.
Шаг 4. При запросе к серверу, пользователь отправляет JWT вместе с запросом через Authorization header:
Authorization: Bearer <token>
Шаг 5. Сервер проверяет валидность токена и получает информацию о пользователе.
Шаг 6. Сервер отправляет ответ пользователю.
Аутентификация на примере
Напишем приложение с аутентификацией пользователя используя Django (backend) и Elm (frontend). Django будет предоставлять API для получения токена. Чтобы получить токен, необходимо отправить запрос с именем пользователя и паролем к API. Если данные пользователя введены корректно, то Django отправит в ответ сгенерированный JWT. Конечный результат на картинке:

Исходный код приложения: https://github.com/apirobot/django-elm-auth-with-jwt
Backend с Django
Писать свой велосипед не обязательно, уже есть хорошая библиотека django-rest-framework-jwt для Django, в которой реализована аутентификация с помощью JWT.
Для начала создаем и активируем виртуальное окружение:
$ virtualenv -p python3 venv $ source venv/bin/activate
Теперь устанавливаем зависимости:
$ pip install django djangorestframework djangorestframework-jwt
Создаем Django проект:
$ django-admin startproject jwtme $ mv jwtme backend
Обновляем настройки проекта:
# backend/jwtme/settings.py
INSTALLED_APPS = [
...
'rest_framework',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
Добавляем URL, по-которому будем отправлять POST запрос с данными пользователя (имя пользователя, пароль) и в ответе получать JWT при успешной аутентификации:
# backend/jwtme/urls.py
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
url(r'^api/v1/token/obtain/', obtain_jwt_token),
...
]
По умолчанию, токен протухает каждые 5 минут. Если нужно, можно изменить время протухания в настройках:
# backend/jwtme/settings.py
import datetime
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1)
}
Все доступные настройки для django-rest-framework-jwt в документации.
Чтобы в дальнейшем, при обращении к API через Elm, не возникла ошибка, как на скриншоте ниже, нужно добавить специальные response headers на сервере. Благодаря им можно выполнять кросс-доменные запросы.

Устанавливаем django-cors-headers:
$ pip install django-cors-headers
Обновляем настройки проекта:
# backend/jwtme/settings.py
INSTALLED_APPS = [
...
'corsheaders',
]
MIDDLEWARE = [
...
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
CORS_ORIGIN_ALLOW_ALL = True
Итак, запрос к серверу на получение токена и его ответ выглядит так (для запроса использовал HTTPie):

Frontend с Elm
Создаем папку:
$ mkdir frontend && cd frontend
Устанавливаем зависимости:
$ elm-package install elm-lang/html $ elm-package install elm-lang/http
Так как весь исходный код проекта будем хранить в папке src, то нужно явно указать это в файле frontend/elm-package.json:
{
"source-directories": [
"src"
]
}
Создаем HTML и добавляем bulma.css стили, чтобы не писать свои:
<!-- frontend/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>JWT Me</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.5.0/css/bulma.min.css">
</head>
<body>
<div id="main"></div>
<script src="elm.js"></script>
<script>
var app = Elm.Main.fullscreen();
</script>
</body>
</html>
Создаем новый тип (union type) для токена:
-- frontend/src/Data/Token.elm module Data.Token exposing (..) type Token = Token String -- Распаковываем Token и получаем строку tokenToString : Token -> String tokenToString (Token token) = token
Создаем каркас приложения:
-- frontend/src/Main.elm
module Main exposing (..)
import Html exposing (Html, div, text)
import Data.Token as Token exposing (Token)
main : Program Never Model Msg
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = \_ -> Sub.none
}
-- MODEL
type alias Model =
{ token : Maybe Token
}
initialModel : Model
initialModel =
{ token = Nothing
}
init : ( Model, Cmd Msg )
init =
( initialModel, Cmd.none )
-- MESSAGES
type Msg
= NoOp
-- UPDATE
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
( model, Cmd.none )
-- VIEW
view : Model -> Html Msg
view model =
case model.token of
Nothing ->
Html.text
"Token: none"
Just token ->
Html.text <|
"Token: " ++ ( Token.tokenToString token )
Для запуска приложения будем использовать elm-live. Установите его, если еще этого не сделали, либо используйте другой способ.
Запускаем приложение:
$ elm-live --port=3000 --output=elm.js src/Main.elm --pushstate --open --debug
Напишем декодер для токена, который будет декодить JSON в Elm. Он понадобиться после получения ответа (response) с сервера после успешной аутентификации.
-- frontend/src/Data/Token.elm ... import Json.Decode as Decode exposing (Decoder) decoder : Decoder Token decoder = Decode.map Token Decode.string
Проверяем работу декодера с помощью elm-repl:
> import Data.Token > import Json.Decode > Json.Decode.decodeString Data.Token.decoder "\"123.fds.OSf\"" Ok (Token "123.fds.OSf") : Result.Result String Data.Token.Token
Напишем функцию, которая создает POST запрос (request) для получения токена:
-- frontend/src/Request/Token.elm
module Request.Token exposing (obtain)
import Json.Decode as Decode
import Json.Encode as Encode
import Http
import Data.Token as Token exposing (Token)
apiUrl : String -> String
apiUrl str =
"http://localhost:8000/api/v1" ++ str
obtain : { r | username : String, password : String } -> Http.Request Token
obtain { username, password } =
let
body =
Http.jsonBody <|
Encode.object
[ ("username", Encode.string username)
, ("password", Encode.string password)
]
-- Декодим JSON ответ с сервера ( {"token": "blah.blah.blah"} )
-- в Elm ( Token "blah.blah.blah" )
decoder =
Decode.field "token" Token.decoder
in
Http.post (apiUrl "/token/obtain/") body decoder
Создадим компонент Login, в котором будем рендериться форма с авторизацией. В этой форме пользователь вводит username и password. После нажатия кнопки Submit выполняется запрос на получение токена к API (/api/v1/token/obtain/). После успешного получения токена, отправляем сообщение с полученным токеном для файла Main.elm:
-- frontend/src/Components/Login.elm
module Components.Login exposing
( Model
, initialModel
, Msg
, ExternalMsg(..)
, update
, view
)
import Html exposing (..)
import Html.Attributes exposing (class, placeholder, type_)
import Html.Events exposing (onInput, onSubmit)
import Http
import Data.Token as Token exposing (Token)
import Request.Token
-- MODEL
type alias Model =
{ username : String
, password : String
}
initialModel : Model
initialModel =
{ username = ""
, password = ""
}
-- MESSAGES
type Msg
= SubmitForm
| SetUsername String
| SetPassword String
-- TokenObtained обрабатывает полученный ответ (response) с сервера.
-- Если произошла ошибка, то получаем Http.Error.
-- Если все прошло успешно, то получаем Token.
| TokenObtained (Result Http.Error Token)
{- ExternalMsg - сообщение для главного файла (src/Main.elm)
Так как функцию `Login.update` мы будем вызывать в файле Main.elm,
то там нам нужно знать, получили ли мы токен с сервера или еще нет.
Сообщение `SetToken Token` как раз для этого. Этим сообщением мы передаем
токен в файл Main.elm, а затем добавляем его там в Model.
Если все еще не понятно, то после того, как мы закончим с кодом,
все должно стать на свои места.
-}
type ExternalMsg
= NoOp
| SetToken Token
-- UPDATE
update : Msg -> Model -> ( Model, Cmd Msg, ExternalMsg )
update msg model =
case msg of
-- Отправляет запрос к серверу на получение токена
SubmitForm ->
( model
, Http.send TokenObtained (Request.Token.obtain model)
, NoOp
)
SetUsername username ->
( { model | username = username }, Cmd.none, NoOp )
SetPassword password ->
( { model | password = password }, Cmd.none, NoOp )
-- Токен успешно получен после запроса к серверу
TokenObtained (Ok token) ->
( model
, Cmd.none
, SetToken token -- Сообщение, в котором мы передаем token для Main.elm
)
-- Возникла ошибка при получении токена
TokenObtained (Err err) ->
( model, Cmd.none, NoOp )
-- VIEW
view : Model -> Html Msg
view model =
section [ class "section" ]
[ div [ class "container" ]
[ div [ class "columns" ]
[ div [ class "column is-one-third" ]
[]
, div [ class "column is-one-third" ]
[ div []
[ h1 [ class "title has-text-centered" ] [ text "Log in" ]
, viewSignInForm
]
]
, div [ class "column is-one-third" ]
[]
]
]
]
-- Форма с input для username, input для password и кнопкой Submit
viewSignInForm : Html Msg
viewSignInForm =
Html.form [ onSubmit SubmitForm ]
[ div [ class "field" ]
[ label [ class "label" ]
[ text "Username" ]
, div [ class "control has-icons-left has-icons-right" ]
[ input
[ onInput SetUsername
, class "input"
, placeholder "Username"
, type_ "text"
]
[]
, span [ class "icon is-small is-left" ]
[ i [ class "fa fa-user" ]
[]
]
]
]
, div [ class "field" ]
[ label [ class "label" ]
[ text "Password" ]
, div [ class "control has-icons-left has-icons-right" ]
[ input
[ onInput SetPassword
, class "input"
, placeholder "Password"
, type_ "password"
]
[]
, span [ class "icon is-small is-left" ]
[ i [ class "fa fa-lock" ]
[]
]
]
]
, div [ class "control" ]
[ button [ class "button is-primary" ]
[ text "Submit" ]
]
]
Подключаем компонент Login в src/Main.elm:
-- frontend/src/Main.elm
...
import Components.Login as Login
-- MODEL
type alias Model =
...
, loginModel : Login.Model
}
initialModel : Model
initialModel =
...
, loginModel = Login.initialModel
}
-- MESSAGES
type Msg
...
| LoginMsg Login.Msg
-- UPDATE
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
...
LoginMsg subMsg ->
let
( newLoginModel, cmdFromLogin, msgFromLogin ) =
Login.update subMsg model.loginModel
newModel =
-- Обрабатываем сообщение, полученное в `Components/Login.elm`
case msgFromLogin of
Login.NoOp ->
model
Login.SetToken token ->
{ model | token = Just token }
in
( { newModel | loginModel = newLoginModel }
, Cmd.map LoginMsg cmdFromLogin
)
-- VIEW
view : Model -> Html Msg
view model =
case model.token of
-- Если токен отсутствует, то показываем форму с авторизацией
Nothing ->
Login.view model.loginModel
|> Html.map LoginMsg
...
Изменим view для случая, когда токен уже получен. Кроме этого добавим новое сообщение Logout:
-- frontend/src/Main.elm
...
import Html exposing (Html, section, div, button, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
type Msg
...
| Logout
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
...
Logout ->
( initialModel, Cmd.none )
view : Model -> Html Msg
view model =
case model.token of
Nothing ->
...
Just token ->
section [ class "section" ]
[ div [ class "container" ]
[ div [ class "columns" ]
[ div [ class "column is-one-third" ]
[]
, div [ class "column is-one-third" ]
[ div []
[ div [ class "notification is-success", style [("word-wrap", "break-word")] ]
[ text ( "You have been successfully logged in. Your token: " ++ (Token.tokenToString token) ) ]
, button [ onClick Logout, class "button is-primary" ]
[ text "Logout" ]
]
]
, div [ class "column is-one-third" ]
[]
]
]
]
Приложение теперь должно работать. Мы можем заполнить форму и получить токен при успешной аутентификации. Проблема в том, что после обновления страницы токен пропадает. Исправим это, сохранив токен в localstorage с помощью Javasript.
Для “общения” с Javascript через Elm создаем порт:
-- frontend/src/Ports.elm port module Ports exposing (storeToken) port storeToken : Maybe String -> Cmd msg
Напишем Javascript код, в котором будем добавлять токен в localstorage при вызове порта storeToken:
<!-- frontend/src/index.html -->
<body>
...
<script>
var app = Elm.Main.fullscreen(localStorage.token || null);
app.ports.storeToken.subscribe(function(token) {
localStorage.token = token;
});
</script>
</body>
Добавим функцию, которая преобразует токен в строку, а затем отправит эту строку к Javascript:
-- frontend/src/Data/Token.elm
...
import Ports
encode : Token -> Value
encode (Token token) =
Encode.string token
storeToken : Token -> Cmd msg
storeToken token =
encode token
|> Encode.encode 0
|> Just -- Преобразуем String в Maybe String
|> Ports.storeToken -- Отправляем Maybe String к Javascript через порт
Смысла в одном сохранении нет. Кроме сохранения токена в localstorage, нужно его еще от туда как-то получить после обновления страницы. Это уже обратное “общение”. Из Javascript в Elm. Добавим функцию, которая декодит JSON в Token:
-- frontend/src/Data/Token.elm
...
decodeTokenFromStore : Value -> Maybe Token
decodeTokenFromStore json =
json
|> Decode.decodeValue Decode.string -- Декодим json и получаем Result String String
|> Result.toMaybe -- Преобразуем Result String String в Maybe String
|> Maybe.andThen (Decode.decodeString decoder >> Result.toMaybe) -- Пропускаем String через Token.decoder и получаем Maybe Token
Теперь сохраняем токен в localstorage. Для этого обновляем случай с успешным получением токена в компоненте Login:
-- frontend/src/Components/Login.elm
...
update : Msg -> Model -> ( Model, Cmd Msg, ExternalMsg )
update msg model =
case msg of
...
TokenObtained (Ok token) ->
( model
, Token.storeToken token
, SetToken token
)
Получаем токен из localstorage при инициализации в Main.elm. Для этого используем флаги (flags) и функцию Html.programWithFlags. Флагами называют данные, которые передаются из Javascript в Elm.
-- frontend/src/Main.elm
...
import Json.Encode as Encode exposing (Value)
main : Program Value Model Msg
main =
Html.programWithFlags
{ init = init
, view = view
, update = update
, subscriptions = \_ -> Sub.none
}
initialModel : Maybe Token -> Model
initialModel token =
{ token = token
, loginModel = Login.initialModel
}
init : Value -> ( Model, Cmd Msg )
init value =
( initialModel (Token.decodeTokenFromStore value), Cmd.none )
Последний штрих. Обновляем сообщение Logout, в котором удаляем токен из Model и localstorage:
-- frontend/src/Main.elm
...
import Ports
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
...
Logout ->
( initialModel Nothing, Ports.storeToken Nothing )