Сохраните токен с сервера OAuth2 в файле cookie, используя Spring OAuth

Есть ли какая-либо конфигурация, предоставляемая Spring OAuth2, которая создает файл cookie с непрозрачным токеном или токеном JWT? Конфигурация, которую я пока нашел в Интернете, описывает создание Сервера авторизации и клиента для него. В моем случае клиент представляет собой шлюз с приложением Angular 4, расположенным поверх него в том же развертываемом объекте. Внешний интерфейс отправляет запросы к шлюзу, который направляет их через Zuul. Настройка клиента с использованием @EnableOAuth2Sso, application.yml и WebSecurityConfigurerAdapter делает все необходимые запросы и перенаправления, добавляет информацию в SecurityContext, но сохраняет информацию в сеансе, отправляя обратно файл cookie JSESSIONID в пользовательский интерфейс.

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

    @SpringBootApplication
    @EnableOAuth2Sso
    @RestController
    public class ClientApplication extends WebSecurityConfigurerAdapter{

        @RequestMapping("/user")
        public String home(Principal user) {
            return "Hello " + user.getName();
        }

        public static void main(String[] args) {
            new SpringApplicationBuilder(ClientApplication.class).run(args);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .antMatcher("/**").authorizeRequests()
                    .antMatchers("/", "/login**", "/webjars/**").permitAll()
                    .anyRequest()
                    .authenticated()
                    .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }


    server:
      port: 9999
      context-path: /client
    security:
      oauth2:
        client:
          clientId: acme
          clientSecret: acmesecret
          accessTokenUri: http://localhost:9080/uaa/oauth/token
          userAuthorizationUri: http://localhost:9080/uaa/oauth/authorize
          tokenName: access_token
          authenticationScheme: query
          clientAuthenticationScheme: form
        resource:
          userInfoUri: http://localhost:9080/uaa/me


person Juan Vega    schedule 05.07.2017    source источник


Ответы (3)


В итоге я решил проблему, создав фильтр, который создает файл cookie с токеном, и добавив две конфигурации для Spring Security: одну, когда файл cookie находится в запросе, и одну, когда его нет. Я как бы думаю, что это слишком много работы для чего-то, что должно быть относительно простым, поэтому я, вероятно, что-то упустил в том, как все это должно работать.

public class TokenCookieCreationFilter extends OncePerRequestFilter {

  public static final String ACCESS_TOKEN_COOKIE_NAME = "token";
  private final UserInfoRestTemplateFactory userInfoRestTemplateFactory;

  @Override
  protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
    try {
      final OAuth2ClientContext oAuth2ClientContext = userInfoRestTemplateFactory.getUserInfoRestTemplate().getOAuth2ClientContext();
      final OAuth2AccessToken authentication = oAuth2ClientContext.getAccessToken();
      if (authentication != null && authentication.getExpiresIn() > 0) {
        log.debug("Authentication is not expired: expiresIn={}", authentication.getExpiresIn());
        final Cookie cookieToken = createCookie(authentication.getValue(), authentication.getExpiresIn());
        response.addCookie(cookieToken);
        log.debug("Cookied added: name={}", cookieToken.getName());
      }
    } catch (final Exception e) {
      log.error("Error while extracting token for cookie creation", e);
    }
    filterChain.doFilter(request, response);
  }

  private Cookie createCookie(final String content, final int expirationTimeSeconds) {
    final Cookie cookie = new Cookie(ACCESS_TOKEN_COOKIE_NAME, content);
    cookie.setMaxAge(expirationTimeSeconds);
    cookie.setHttpOnly(true);
    cookie.setPath("/");
    return cookie;
  }
}

/**
 * Adds the authentication information to the SecurityContext. Needed to allow access to restricted paths after a
 * successful authentication redirects back to the application. Without it, the filter
 * {@link org.springframework.security.web.authentication.AnonymousAuthenticationFilter} cannot find a user
 * and rejects access, redirecting to the login page again.
 */
public class SecurityContextRestorerFilter extends OncePerRequestFilter {

  private final UserInfoRestTemplateFactory userInfoRestTemplateFactory;
  private final ResourceServerTokenServices userInfoTokenServices;

  @Override
  public void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException {
    try {
      final OAuth2AccessToken authentication = userInfoRestTemplateFactory.getUserInfoRestTemplate().getOAuth2ClientContext().getAccessToken();
      if (authentication != null && authentication.getExpiresIn() > 0) {
        OAuth2Authentication oAuth2Authentication = userInfoTokenServices.loadAuthentication(authentication.getValue());
        SecurityContextHolder.getContext().setAuthentication(oAuth2Authentication);
        log.debug("Added token authentication to security context");
      } else {
        log.debug("Authentication not found.");
      }
      chain.doFilter(request, response);
    } finally {
      SecurityContextHolder.clearContext();
    }
  }
}

Это конфигурация, когда файл cookie находится в запросе.

@RequiredArgsConstructor
  @EnableOAuth2Sso
  @Configuration
  public static class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private final UserInfoRestTemplateFactory userInfoRestTemplateFactory;
    private final ResourceServerTokenServices userInfoTokenServices;

/**
 * Filters are created directly here instead of creating them as Spring beans to avoid them being added as filters      * by ResourceServerConfiguration security configuration. This way, they are only executed when the api gateway      * behaves as a SSO client.
 */
@Override
protected void configure(final HttpSecurity http) throws Exception {
  http
    .requestMatcher(withoutCookieToken())
      .authorizeRequests()
    .antMatchers("/login**", "/oauth/**")
      .permitAll()
    .anyRequest()
      .authenticated()
    .and()
      .exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
    .and()
      .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    .and()
      .csrf().requireCsrfProtectionMatcher(csrfRequestMatcher()).csrfTokenRepository(csrfTokenRepository())
    .and()
      .addFilterAfter(new TokenCookieCreationFilter(userInfoRestTemplateFactory), AbstractPreAuthenticatedProcessingFilter.class)
      .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)
      .addFilterBefore(new SecurityContextRestorerFilter(userInfoRestTemplateFactory, userInfoTokenServices), AnonymousAuthenticationFilter.class);
}

private RequestMatcher withoutCookieToken() {
  return request -> request.getCookies() == null || Arrays.stream(request.getCookies()).noneMatch(cookie -> cookie.getName().equals(ACCESS_TOKEN_COOKIE_NAME));
}

И это конфигурация, когда есть куки с токеном. Существует экстрактор файлов cookie, который расширяет функциональность BearerTokenExtractor с Spring для поиска маркера в файле cookie и точки входа для проверки подлинности, срок действия файла cookie которой истекает при сбое аутентификации.

@EnableResourceServer
  @Configuration
  public static class ResourceSecurityServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(final ResourceServerSecurityConfigurer resources) {
      resources.tokenExtractor(new BearerCookiesTokenExtractor());
      resources.authenticationEntryPoint(new InvalidTokenEntryPoint());
    }

    @Override
    public void configure(final HttpSecurity http) throws Exception {
      http.requestMatcher(withCookieToken())
        .authorizeRequests()
        .... security config
        .and()
        .exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/"))
        .and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .logout().logoutSuccessUrl("/your-logging-out-endpoint").permitAll();
    }

    private RequestMatcher withCookieToken() {
      return request -> request.getCookies() != null && Arrays.stream(request.getCookies()).anyMatch(cookie -> cookie.getName().equals(ACCESS_TOKEN_COOKIE_NAME));
    }

  }

/**
 * {@link TokenExtractor} created to check whether there is a token stored in a cookie if there wasn't any in a header
 * or a parameter. In that case, it returns a {@link PreAuthenticatedAuthenticationToken} containing its value.
 */
@Slf4j
public class BearerCookiesTokenExtractor implements TokenExtractor {

  private final BearerTokenExtractor tokenExtractor = new BearerTokenExtractor();

  @Override
  public Authentication extract(final HttpServletRequest request) {
    Authentication authentication = tokenExtractor.extract(request);
    if (authentication == null) {
      authentication = Arrays.stream(request.getCookies())
        .filter(isValidTokenCookie())
        .findFirst()
        .map(cookie -> new PreAuthenticatedAuthenticationToken(cookie.getValue(), EMPTY))
        .orElseGet(null);
    }
    return authentication;
  }

  private Predicate<Cookie> isValidTokenCookie() {
    return cookie -> cookie.getName().equals(ACCESS_TOKEN_COOKIE_NAME);
  }

}

/**
 * Custom entry point used by {@link org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter}
 * to remove the current cookie with the access token, redirect the browser to the home page and invalidate the
 * OAuth2 session. Related to the session, it is invalidated to destroy the {@link org.springframework.security.oauth2.client.DefaultOAuth2ClientContext}
 * that keeps the token in session for when the gateway behaves as an OAuth2 client.
 * For further details, {@link org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2RestOperationsConfiguration.SessionScopedConfiguration.ClientContextConfiguration}
 */
@Slf4j
public class InvalidTokenEntryPoint implements AuthenticationEntryPoint {

  public static final String CONTEXT_PATH = "/";

  @Override
  public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException {
    log.info("Invalid token used. Destroying cookie and session and redirecting to home page");
    request.getSession().invalidate(); //Destroys the DefaultOAuth2ClientContext that keeps the invalid token
    response.addCookie(createEmptyCookie());
    response.sendRedirect(CONTEXT_PATH);
  }

  private Cookie createEmptyCookie() {
    final Cookie cookie = new Cookie(TokenCookieCreationFilter.ACCESS_TOKEN_COOKIE_NAME, EMPTY);
    cookie.setMaxAge(0);
    cookie.setHttpOnly(true);
    cookie.setPath(CONTEXT_PATH);
    return cookie;
  }
}
person Juan Vega    schedule 16.10.2018
comment
обрабатывает ли это автоматическое обновление токена доступа с помощью токена обновления? - person PragmaticProgrammer; 30.10.2018
comment
нет. Он был создан для приложения, у которого не возникало проблем с созданием долгоживущего токена доступа напрямую, вместо того, чтобы время от времени обновлять его. Токен обновления в любом случае должен где-то надежно храниться, поскольку другой файл cookie не улучшил бы реализацию и сделал бы ее более громоздкой. Созданный файл cookie является HttpOnly, поэтому XSS следует предотвращать в большинстве случаев, а в случае кражи токен может быть признан недействительным. Реализация этого не показывает, но настроена на проверку токена для каждого запроса. - person Juan Vega; 30.10.2018
comment
Я получаю сообщение об ошибке Пустое конечное поле userInfoRestTemplateFactory, возможно, не было инициализировано - person samuelj90; 07.08.2019
comment
@SamuelJMathew Это странно. Компонент должен быть создан @EnableOAuth2Sso, а именно ResourceServerTokenServicesConfiguration.class, импортированным предыдущим. Проверьте, нет ли у вас какой-либо другой конфигурации, которая может вызвать проблему. На ResourceServerTokenServicesConfiguration есть @ConditionalOnMissingBean(AuthorizationServerEndpointsConfiguration.class), поэтому убедитесь, что вы не создали его где-то еще. Кроме того, в примере используется Lombok для создания конструктора. Странно, что компилятор не жалуется на неинициализированное конечное поле. - person Juan Vega; 07.08.2019
comment
@JuanVega Вы нашли какой-нибудь другой лучший способ сделать это? - person Dharm; 25.02.2021
comment
@Dharm не совсем так, и это было сделано в проекте, который я больше не поддерживаю. Я не уверен, но я думаю, что сейчас есть другие библиотеки Spring Boot, которые занимаются этим, поэтому я не уверен, что это все еще актуально. - person Juan Vega; 25.02.2021
comment
@JuanVega Спасибо за ваш ответ. - person Dharm; 26.02.2021

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

Как вы уже поняли, мое решение состояло в том, чтобы добавить предварительные фильтры для удаления и добавления файлов cookie, где это необходимо. Вы также должны посмотреть OAuth2ClientConfiguration.. это определяет bean-компонент OAuth2ClientContext с областью сеанса. Чтобы все было просто, я изменил автоконфигурацию и сделал этот запрос bean-компонента ограниченным. Просто вызовите setAccessToken в предварительном фильтре, который удаляет файл cookie.

person Andy    schedule 22.08.2017
comment
Лично я нахожу реализацию Spring очень запутанной. Я случайно обнаружил контекст клиента с областью действия сеанса, когда выяснял, почему в браузере был JSSESSIONID, а не токен. Даже использование JWT кажется излишним, когда вам нужно закодировать черный список или что-то сложное, чтобы сделать его недействительным. В конце концов я отказался от JWT и вместо этого решил использовать непрозрачный токен, который проверяется для каждого запроса с помощью RemoteTokenService, который добавляет принципала пользователя в Spring Security. В браузере я сохраняю токен в файле cookie HttpOnly и Secure, чтобы разрешить длительные сеансы. - person Juan Vega; 22.08.2017

Убедитесь, что вы импортировали эти классы, присутствующие в javax.servlet:

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse; 

Инициализируйте cookie следующим образом:

Cookie jwtCookie = new Cookie(APP_COOKIE_TOKEN, token.getToken());
jwtCookie.setPath("/");
jwtCookie.setMaxAge(20*60);
//Cookie cannot be accessed via JavaScript
jwtCookie.setHttpOnly(true);

Добавьте cookie в HttpServletResponse:

response.addCookie(jwtCookie);

Если вы используете angular 4 и spring security+boot , то этот репозиторий github может стать большая помощь:

Ссылка на блог для этого репозитория является:

person Deepak Kumar    schedule 05.07.2017
comment
Спасибо, но я искал способ настроить Spring OAuth, чтобы он делал это автоматически. В итоге я создал файл cookie вручную с помощью фильтра, в основном делая что-то похожее на то, что вы описываете. Мне кажется странным, что Spring OAuth позволяет вам настроить все и сделать все перенаправления для получения токена, но в конце концов он просто сохраняет его в HttpSession. Я искал фильтр или конфигурацию, которая внедрила фильтр, который создал что-то похожее на то, что он делает для предоставления JSESSIONID - person Juan Vega; 06.07.2017
comment
@JuanVega, я борюсь с этим уже несколько дней. Вы нашли твердое решение. Не могли бы вы предоставить репозиторий Git или код? Ценить это. - person abedurftig; 16.10.2018
comment
@dasnervtdoch Я только что добавил ответ с кодом, который мы используем. - person Juan Vega; 17.10.2018
comment
Spring framework не дает для этого готового подхода. Как будто он обрабатывает JSESSIONID. Я разговаривал с парнем из службы безопасности Spring, он сказал, что использование Filter — правильный и единственный способ. Они также не планируют внедрять эту функцию в проект безопасности. Так как это может привести к некоторым уязвимостям безопасности. - person TheCoder; 14.02.2019