Безопасность Spring: как реализовать «двухэтапную» аутентификацию (oauth, затем на основе форм)? - PullRequest
0 голосов
/ 10 декабря 2018

В моем приложении есть два разных шлюза для аутентификации, которые работают довольно неплохо по отдельности:

  1. OAuth 1.0 ( service-to-service ), который дает пользователю ROLE_OAUTH;Здесь я ничего не знаю о пользователе и имею только некоторую контекстную информацию об услуге, которую он использует внутри объекта Principal;
  2. Стандартная аутентификация на основе форм, которая дает пользователю ROLE_USER;Здесь у меня есть полная информация о моем пользователе, но нет контекстной информации об услуге, которую он использует внутри объекта Principal;

Теперь я хочу реализовать двухэтапную аутентификацию: 1) OAuth, затем на основе форм.

Сложность состоит в том, что Я не хочу терять контекстно-зависимую информацию, хранящуюся в Принципале после шага 1 (OAuth) ;Я просто хочу добавить новую пользовательскую информацию после завершения проверки подлинности на основе форм в контекст безопасности, а также новую роль ROLE_USER, все в одном сеансе проверки подлинности.

Возможно ли реализовать плавно?Как я могу извлечь уже существующую информацию о Принципале во время второго шага (аутентификация на основе форм) и добавить ее к новому Принципалу?

Существует ли какое-либо "шаблонное решение" без повторного изобретения колеса?

Мое текущее простое решение будет таким:

  1. Я аутентифицировал пользователя с ролью ROLE_OAUTH и открыл сеанс аутентификации;
  2. Создайте отдельный путь для 2-d шаг как / oauth / login ;
  3. После того, как пользователь вводит свои учетные данные, я обрабатываю их вне цепочки безопасности в контроллере, вручную проверяя учетные данные;
  4. В случае успеха,вручную обновите контекст безопасности, не теряя сеанс аутентификации, затем перенаправьте пользователя на запрошенный защищенный ресурс ROLE_USER;

Но мне это не нравится, это кажется неэффективным, поскольку мне приходится вручную обрабатывать второй запрос безопасности..

Как я могу правильно реализовать это по-весеннему?Спасибо.

PS Мне нужно использовать Oauth 1.0 по устаревшим причинам, я не могу обновить его до версии v.2 или любого другого решения.

1 Ответ

0 голосов
/ 18 декабря 2018

ОК, вот как мне удалось выполнить эту задачу.

  1. У меня есть аутентифицированный пользователь (на самом деле сервис) с ролью ROLE_OAUTH и открытым сеансом аутентификации, а также некоторая важная информация о контексте в сеансе, который был встроен в запрос OAuth;
  2. Теперь при попытке доступа к защищенному ресурсу, требующему другой роли, скажем ROLE_USER, Spring дает мне AccessDeniedException и отправляет403 запрещенный ответ (см. AccessDeniedHandlerImpl ), любезно предлагая переопределить поведение по умолчанию, если это необходимо, в пользовательском AccessDeniedHandler.Вот пример кода:

   public class OAuthAwareAccessDeniedHandler implements AccessDeniedHandler {
   private static final Log LOG = LogFactory.getLog(OAuthAwareAccessDeniedHandler.class);

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (oauthSecurityUtils.isUserWithOnlyOAuthRole(auth)) {
            LOG.debug("Prohibited to authorize OAuth user trying to access protected resource.., redirected to /login");
            // Remember the request pathway
            RequestCache requestCache = new HttpSessionRequestCache();
            requestCache.saveRequest(request, response);
            response.sendRedirect(request.getContextPath() + "/login");
            return;
        }
        LOG.debug("Ordinary redirection to /accessDenied URL..");
        response.sendRedirect(request.getContextPath() + "/accessDenied");
    }
}
Теперь нам нужно добавить этот новый обработчик в конфигурацию:
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        // all the config
        .and()
            .exceptionHandling().accessDeniedHandler(oauthAwareAccessDeniedHandler());
}
После этого шага значение по умолчанию UsernamePasswordAuthenticationFilter обработает ввод путем создания другого объекта аутентификации с введенными учетными данными, а поведение по умолчанию просто теряет существующую информацию, связанную с предыдущим объектом аутентификации OAuth.Поэтому нам нужно переопределить это поведение по умолчанию, расширив этот класс, например, добавив этот фильтр перед стандартным UsernamePasswordAuthenticationFilter.
public class OAuthAwareUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

private static final Log LOG = LogFactory.getLog(LTIAwareUsernamePasswordAuthenticationFilter.class);


@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    Authentication previousAuth = SecurityContextHolder.getContext().getAuthentication();
    // Check for OAuth authentication in place
    if (oauthSecurityUtils.isUserWithOnlyOAuthRole(previousAuth)) {
        LOG.debug("OAuth authentication exists, try to authenticate with UsernamePasswordAuthenticationFilter in the usual way");
        SecurityContextHolder.clearContext();
        Authentication authentication = null;
        try {// Attempt to authenticate with standard UsernamePasswordAuthenticationFilter
            authentication = super.attemptAuthentication(request, response);
        } catch (AuthenticationException e) {
            // If fails by throwing an exception, catch it in unsuccessfulAuthentication() method
            LOG.debug("Failed to upgrade authentication with UsernamePasswordAuthenticationFilter");
            SecurityContextHolder.getContext().setAuthentication(previousAuth);
            throw e;
        }
        LOG.debug("Obtained a valid authentication with UsernamePasswordAuthenticationFilter");
        Principal newPrincipal = authentication.getPrincipal();
        // Here extract all needed information about roles and domain-specific info
        Principal rememberedPrincipal = previousAuth.getPrincipal();
       // Then enrich this remembered principal with the new information and return it
        LOG.debug("Created an updated authentication for user");
        return newAuth;
    }
    LOG.debug("No OAuth authentication exists, try to authenticate with UsernamePasswordAuthenticationFilter in the usual way");
    return super.attemptAuthentication(request, response);
}

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
        throws IOException, ServletException {
    Authentication previousAuth = SecurityContextHolder.getContext().getAuthentication();
    if (oauthSecurityUtils.isUserWithOnlyOAuthRole(previousAuth)) {
        LOG.debug("unsuccessfulAuthentication upgrade for OAuth user, previous authentication :: "+ previousAuth);
        super.unsuccessfulAuthentication(request, response, failed);
        LOG.debug("fallback to previous authentication");
        SecurityContextHolder.getContext().setAuthentication(previousAuth);
    } else {
        LOG.debug("unsuccessfulAuthentication for a non-OAuth user with UsernamePasswordAuthenticationFilter");
        super.unsuccessfulAuthentication(request, response, failed);
    }
}

}

Единственное, что нужноОсталось добавить этот фильтр перед Имя пользователяPasswordAuthenticationFilter и применить его только к указанной конечной точке:


@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        .addFilterBefore(oauthAwareUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
        // here come ant rules
        .and()
        .formLogin()
        .and()
            .exceptionHandling().accessDeniedHandler(oauthAwareAccessDeniedHandler());
}

Вот и все.Этот пример проверен на работоспособность.Может быть, некоторые побочные эффекты будут найдены позже, не уверен.Также я уверен, что это может быть сделано более изощренным способом, но сейчас я пойду с этим кодом.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...