2017–07–19 13:59:54 -0700
Для моего последнего проекта в Программе веб-разработчиков полного стека Flatiron School я решил создать одностраничное приложение с использованием React и Redux. Я написал базовый серверный API с использованием Rails для обеспечения хранения и извлечения данных, а затем приступил к работе с внешним интерфейсом. Создание API было довольно простым делом, и здесь мы не будем его подробно обсуждать.
Природа приложения - хранение и получение данных о погружениях с аквалангом - требует какой-то аутентификации, чтобы вы могли быть уверены, что ваши данные не были подделаны. Кроме того, я ненавижу необходимость создавать новые учетные записи для каждого веб-приложения, которое я посещаю, поэтому я хотел использовать OAuth2. Это кажется обычным сценарием, поэтому я подумал, что наверняка будет какая-то сторонняя библиотека, которая упростит это.
Я поискал и нашел пару многообещающих: redux-auth и react-native-oauth, но они казались излишне сложными. Полагаю, частично это было связано с тем, что я не понимал JWT так хорошо, как должен.
Прочитав множество блогов и руководств, я наконец решил, что это не так уж и сложно сделать самому. Процесс выглядит примерно так, как на следующей диаграмме, например, с использованием Facebook в качестве поставщика OAuth:
Итак, прямо сейчас вы, вероятно, думаете, что это действительно непросто. Но большая часть сложности заключена в прямоугольниках с красным текстом, с которыми без особых усилий обработали гем Omniauth или devise. По общему признанию, разработка - это не совсем прогулка по парку, но она достаточно популярна, вы, возможно, уже сталкивались с ней. Лично для этого проекта devise является излишним, так как мне нужен был только OAuth, и мне не нужно было сбрасывать пароль, двухфакторную аутентификацию и т. Д., Поэтому я выбрал гем omniauth.
1. Запуск процесса
Это так же просто, как предоставить пользователю кнопку для нажатия, указывающую, что он хочет войти в систему. Я выбрал поддержку входа через Facebook или Github, но вы можете использовать любую службу OAuth, которую вы выберете, при условии, что она поддерживается omniauth. Каждая кнопка вызывает действие #authenticate в SessionsController, которое перенаправляет на соответствующий путь аутентификации omniauth.
def authenticate redirect_to '/auth/facebook' if params[:type] == 'facebook' redirect_to '/auth/github' if params[:type] == 'github' end
2. Аутентифицируйте пользователя с помощью OmniAuth.
API аутентифицирует пользователя с помощью гема omniauth. Я не буду вдаваться в подробности, так как самоцвет хорошо задокументирован здесь.
3. Найдите или создайте пользователя
API использует результат обратного вызова omniauth для поиска или создания пользователя. В SessionsController у нас есть:
def create
user = User.find_or_create_by(uid: auth['uid']) do |u|
u.name = auth['info']['name']
u.email = auth['info']['email']
u.image = auth['info']['image']
end
...
end
def auth
request.env['omniauth.auth']
end
Это все, что нам нужно для аутентификации пользователя и определения его идентификатора.
ПРИМЕЧАНИЕ. Если вы смешиваете вход в систему OAuth с «традиционным» именем пользователя и паролями, вам следует создать случайный пароль для учетных записей, созданных с помощью OAuth. Иначе у них его не будет!
Затем мы должны отправить данные обратно клиенту в форме JWT.
4. Создайте JWT и отправьте его клиенту.
JWT - это веб-токен JSON, который является отраслевым стандартом для безопасной передачи данных с цифровой подписью. Чтобы создать этот JWT, API объединяет данные, заголовок и секретный ключ и смешивает все это в защищенный пакет, который может быть передан клиенту.
Есть много ресурсов о том, как создавать и использовать JWT:
Мое любимое на самом деле было написано студентом / инструктором Flatiron:
На стороне сервера я использовал jwt gem для кодирования и декодирования токенов. Я написал вспомогательный класс для выполнения грязной работы:
class Auth
def self.encode_uid(user_id)
payload = { user_id: user_id }
JWT.encode payload, ENV['AUTH_SECRET'], 'HS256'
end
def self.decode_uid(token)
payload = JWT.decode token, ENV['AUTH_SECRET'], true,
{ :algorithm => 'HS256' }
payload[0]['user_id']
end
end
Обратите внимание, что мы храним секрет в переменной окружения, чтобы случайно не загрузить его на github!
Теперь, когда мы создали класс Auth, вернуть идентификатор пользователя клиенту внутри JWT очень просто. В нашем SessionsController действие create теперь становится следующим:
def create
user = User.find_or_create_by(uid: auth['uid']) do |u|
u.name = auth['info']['name']
u.email = auth['info']['email']
u.image = auth['info']['image']
end
if user
jwt = Auth.encode_uid(user.uid)
redirect_to(ENV['DIVE_LOG_CLIENT_URL'] + "?token=#{jwt}")
end
end
ПРИМЕЧАНИЕ. JWT будет отправляться клиенту в строке запроса, которая представляет собой обычный текст. Любой, кто получит ваш JWT, может использовать его для доступа к API и вашим данным. Чтобы избежать этого, вы должны использовать на клиенте конечную точку https, которая будет шифровать строку запроса.
5. Использование JWT клиентом.
На стороне клиента JWT хранится в хранилище сеанса:
sessionStorage.setItem(‘jwt’, jwt)
Затем все, что нужно сделать клиенту, - это поместить его в заголовок для каждого запроса, который он делает к api. Например, метод get выглядит так:
get(url) {
const jwt = sessionStorage.getItem('jwt');
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer: ${jwt}`
}
return fetch(`${API_URL}${url}`, {
method: 'GET',
headers: headers
}).then(response => (response.json()));
}
6. Использование JWT для идентификации текущего пользователя.
В контроллере приложения у нас есть несколько вспомогательных методов, которые выполняют аутентификацию и извлекают current_user:
class ApplicationController < ActionController::API
before_action :authenticate
def logged_in?
!!current_user
end
def current_user
return @current_user if @current_user
if auth_present?
uid = Auth.decode_uid(read_token_from_request)
@current_user = User.find_by({uid: uid})
return @current_user if @current_user
end
end
def authenticate
render json: {error: "unauthorized"},
status: 401 unless logged_in?
end
private
def read_token_from_request
token = request.env["HTTP_AUTHORIZATION"]
.scan(/Bearer: (.*)$/).flatten.last
end
def auth_present?
!!request.env.fetch("HTTP_AUTHORIZATION", "")
.scan(/Bearer/).flatten.first
end
end
Резюме
Это довольно простая реализация OAuth для приложения React с бэкэндом rails. Однако он не лишен недостатков:
- Перенаправление от API обратно к клиенту вызывает полную перезагрузку страницы. Это, вероятно, не о чем беспокоиться, но имейте это в виду.
- Хотя HTTPS становится все более распространенным явлением, он может быть недоступен для вас. Я ищу возможные улучшения. Одна из идей состоит в том, чтобы случайным образом сгенерировать «секрет клиента» и отправить его через POST в API во время аутентификации. Тогда API будет использовать как свой собственный секрет, так и секрет клиента при формировании JWT. С этим изменением одного JWT будет недостаточно для получения доступа, вам также понадобится секрет клиента.
Наконец, эта работа выполняется в рамках моего учебного процесса, и я, конечно же, не являюсь экспертом в области безопасности. Если вы видите вещи, которые требуют исправления, или если у вас есть для меня дополнительные ресурсы, не стесняйтесь обращаться ко мне.
Первоначально опубликовано на michaelries.info.