Как реализовать вход в систему на основе формы с безопасностью Spring в Grails и существующем провайдере oauth2

Привет всем, у меня есть секретный вопрос для новичков в Grails / Spring. Мы настроили oauth2 в нашем проекте Grails 3, используя spring-security-oauth2- провайдер, и, кажется, все работает для защиты наших REST API.

Но затем мы начали добавлять веб-интерфейс в тот же проект, используя GSP, и пришли к распутью. Обычно oauth2 работает, аутентифицируясь в конечной точке и получая токен, который он может использовать в заголовке HTTP в последующих запросах для сохранения доступа к защищенным ресурсам. Но у моего веб-интерфейса есть страница входа. Поэтому изначально мы думали рассматривать веб-интерфейс как одного из клиентов (у нас есть 1 клиент для нашего приложения для iOS и 1 клиент для нашего приложения для Android, так почему бы не иметь еще 1 для нашего веб-приложения). Но кажется странным делать HTTP-запрос из кода нашего контроллера (для входа в систему) к конечной точке нашего провайдера oauth2, потому что он находится в том же проекте; и большинство последующих запросов, которые должен сделать мой веб-интерфейс, мы хотим получить прямой доступ к базовым службам и объектам домена, поэтому добавление дополнительного прыжка кажется контрпродуктивным.

Итак, что мы выбрали, так это то, что когда я вхожу в систему, используя свой login.gsp, в коде контроллера, я обхожу вход в систему oauth2 и просто выполняю прямую аутентификацию безопасности spring, используя authenticationManager.authenticate() с UsernamePasswordAuthenticationToken, который я создаю из имени пользователя и поля пароля, переданные в мою форму, а затем вызов SecurityContextHolder.getContext().setAuthentication() в ответе. Эта половина решает мою проблему, потому что мне сказали из это сообщение о том, что при этом SecurityContextHolder будет установлен только в текущем потоке, а поскольку SecurityContextHolder очищается в конце цепочки фильтров, последующие запросы не будут аутентифицироваться. Итак, что я сделал, так это то, что если эта аутентификация проходит (т. Е. Никаких исключений), то я помещаю свой пользовательский объект в сеанс HTTP и во всех последующих запросах пытаюсь получить его как способ «сказать», что я аутентифицирован. Но это кажется и хакерским, и грязным; и это приводит к тому, что мой пользовательский объект не подключен к сеансу БД (вызывая ленивые исключения инициализации).

Я нашел другие подобные предложения из таких сообщений, как , который помещает весь SecurityContext в сеанс HTTP, но, похоже, не указывает, как использовать этот SecurityContext в последующих запросах.

Я предполагаю, что мой главный вопрос в том, что мы идем по этому пути неправильно? Есть ли лучший и более чистый способ выполнить то, что я хочу сделать? Я полагаю, что мы не можем быть первыми, кто попытается это сделать.


person paulito415    schedule 13.07.2017    source источник


Ответы (2)


Немного грустно, что я отвечаю на свой вопрос. Но я хотел посмотреть, является ли мое решение правильным способом решения этой проблемы.

Итак, в итоге мы использовали фильтры. Мы знаем, что в Grails 3 должны быть перехватчики, но перед тем, как приступить к этому, мы просто хотим иметь рабочую версию с фильтрами.

В нашем методе AuthController.signIn мы проходим через процесс аутентификации UsernamePasswordAuthenticationToken и фактически вставляем SecurityContext в сеанс HTTP. Кстати, довольно забавно, что позже я обнаружил, что это на самом деле уже сделано для меня (с использованием ключа «SPRING_SECURITY_CONTEXT») каким-то образом, без необходимости явно указывать session.setAttribute. Мое обоснование заключается в том, что, поскольку SecurityContext аутентифицируется только для этого запроса с использованием UsernamePasswordAuthenticationToken, мне нужно сохранить его в сеансе HTTP, чтобы я мог перезагрузить его для последующих запросов. Итак, в моем фильтре, который я разместил последовательно первым, я проверяю SecurityContext в сеансе HTTP и копирую его «аутентификацию» в текущий SecurityContext. Обратите внимание, что у нас также уже есть фильтр, последовательно размещенный последним, для перенаправления пользователя на страницу входа, если в HTTP-сеансе ничего не было найдено.

С этим изменением мы теперь можем получить доступ к springSecurityService в нашем контроллере, чтобы получить доступ к текущему вошедшему в систему пользователю (раньше мы не могли). И другие приятности, которые мы получаем, включают возможность использовать sec:ifAllGranted на наших страницах GSP. Но что-то по-прежнему невозможно — мы все еще не можем использовать аннотацию @Secured("#oauth2.isUser()") для защиты наших методов контроллера, всегда нажимая 401. Возможно, наше использование UsernamePasswordAuthenticationToken для аутентификации все еще не "полная" аутентификация нам нужна?

Я попытался дальше, запихнув в HTTP-сеанс токен доступа OAuth2 вместо SecurityContext, а в свои фильтры добавив HTTP-заголовок «Авторизация» со значением «Bearer» +accessToken. Это не работает, и предварительное ведение журнала показало, что в одном HTTP-запросе (просмотре страницы) я много раз заходил в свой фильтр, и только в первый раз мне удалось получить токен доступа OAuth2 для помещения в HTTP-заголовки запроса.

Итак, в конце концов, я хотел выяснить, был ли мой описанный выше подход правильным, чтобы использовать фильтры для удержания пользователя на связи в сеансе? Или я должен был сделать это по-другому? Также как я могу защитить свой контроллер с помощью аннотации @Secured?

person paulito415    schedule 18.09.2017
comment
Меня больше не должно удивлять, что я снова отвечаю на свой вопрос. Возможно, это потому, что ответ был слишком очевиден, а возможно, никто никогда не сталкивался с этим; в любом случае, я просто собираюсь вести блог о своем путешествии здесь, на SO, на случай, если кто-то еще сочтет это полезным. Короткий ответ на мой вопрос: нет, все это абсолютно не нужно. Spring Security поставляется с множеством стандартных / встроенных функций, которые мы можем настроить. Но поскольку в комментариях не более 600 символов, я опубликую свое дополнение в виде другого ответа, но не раньше, чем исчерпаю все предоставленное мне место. - person paulito415; 18.10.2017

Интересно, как SO отнесется к этому ответу, учитывая, что я уже дал ответ. Увы, кого это действительно волнует - я все равно задал вопрос в первую очередь.

Через всю документацию, которую я просмотрел, например. Безопасность Grails — справочная документация и Плагин Spring Security Core — справочная документация и Пользовательская аутентификация плагина Grails Spring Security Core и Как настроить страницу входа плагина безопасности Spring в Grails Я смог найти это, хотя мы используем подключаемый модуль Spring Security OAuth2, нам также были предоставлены все возможности, предоставляемые подключаемым модулем Spring Security Core. Это означает, что вход в систему на основе формы поддерживается из коробки, а также поддерживается настраиваемая форма входа.

А с настраиваемой формой входа вместо того, чтобы login.gsp вызывал наш собственный метод signIn AuthController, мы все равно могли бы использовать метод «аутентификации» LoginController (который поставляется с подключаемым модулем Spring Security), используя ${request.contextPath}/ войти/авторизоваться.

Итак, я на самом деле попробовал это, сначала, конечно, отключив наши перехватчики, но затем в итоге получился цикл перенаправления, который заставил браузер вылететь. Я думаю, что нам нужно было выполнить некоторую настройку (с chainMap или фильтрами), которой у нас нет, поэтому она все еще не работает.

Одна вещь, которая помогла, это добавить следующие две строки в grails-app/conf/logback.groovy.

logger 'org.springframework.security', DEBUG, ['STDOUT'], false
logger 'grails.plugin.springsecurity', DEBUG, ['STDOUT'], false

Это дало мне много информации о регистрации, чтобы помочь в отладке. Я почему-то понял, что оно как-то пихнуло мне в запрос анонимный токен, потому что что-то не аутентифицировалось.

Потратив некоторое время на отладку, я наконец обнаружил, что ответ кроется в application.groovy. Проблема, с которой я столкнулся, заключалась в том, что, поскольку все приложения по умолчанию имеют Spring Security filterChain, последняя строка /** соответствует JOINED_FILTERS. И строка, которую мы добавили при установке Spring Security OAuth2 Provider, имеет /** match JOINED_FILTERS минус кучу других фильтров. Эта строка на самом деле правильная, но поскольку мы никогда не удаляли исходную строку /**, а порядок приоритета таков, что одна идет перед этой. Итак, все, что мне нужно было сделать, это удалить первую строку /**, соответствующую JOINED_FILTERS, и теперь вход в систему работает.

В конце строки, которые у нас есть в filterChain:

[pattern: '/rest/**', filters: 'JOINED_FILTERS,-securityContextPersistenceFilter,-logoutFilter,-authenticationProcessingFilter,-rememberMeAuthenticationFilter,-oauth2BasicAuthenticationFilter,-exceptionTranslationFilter'],
// We want all the other resources to be Web-based
[pattern: '/web/**',  filters: 'JOINED_FILTERS,-statelessSecurityContextPersistenceFilter,-oauth2ProviderFilter,-clientCredentialsTokenEndpointFilter,-oauth2BasicAuthenticationFilter,-oauth2ExceptionTranslationFilter'],
// This is just a catch-all that defaults to the same logic as before, it also would match things like /oauth/** or /auth/**
[pattern: '/**',      filters: 'JOINED_FILTERS']

И у нас также есть несколько других конфигураций для переопределений, указывающих на наш собственный AuthController:

grails.plugin.springsecurity.auth.loginFormUrl = '/auth/login' // This is our own Login Form
grails.plugin.springsecurity.failureHandler.defaultFailureUrl = '/auth/login' // Exception message shown in controller
grails.plugin.springsecurity.adh.errorPage = '/auth/denied' // Copied from LoginController
person paulito415    schedule 18.10.2017