Весенние сеансы безопасности без файлов cookie

Я пытаюсь управлять сеансами в Spring Security без использования файлов cookie. Причина в том, что наше приложение отображается в iframe из другого домена, нам нужно управлять сеансами в нашем приложении, а Safari ограничивает создание междоменных файлов cookie.. (контекст: domainA.com отображает domainB.com в iframe. domainB.com устанавливает cookie JSESSIONID для использования на domainB.com, но поскольку браузер пользователя показывает domainA.com - Safari запрещает domainB.com создавать cookie) .

Единственный способ добиться этого (вопреки рекомендациям по безопасности OWASP) - это включить JSESSIONID в URL-адрес в качестве параметра GET. Я не ХОЧУ этого делать, но не могу придумать альтернативы.

Итак, этот вопрос касается как:

  • Есть ли лучшие альтернативы решению этой проблемы?
  • Если нет - как я могу добиться этого с помощью Spring Security

Просмотр документации Spring по этому поводу, использование enableSessionUrlRewriting должно позволить это

Итак, я сделал это:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
            .enableSessionUrlRewriting(true)

Это не добавляло JSESSIONID к URL-адресу, но теперь это должно быть разрешено. Затем я использовал некоторый код, найденный в этом вопросе, чтобы установить "режим отслеживания" на URL

@SpringBootApplication
public class MyApplication extends SpringBootServletInitializer {

   @Override
   public void onStartup(ServletContext servletContext) throws ServletException {
      super.onStartup(servletContext);

      servletContext
        .setSessionTrackingModes(
            Collections.singleton(SessionTrackingMode.URL)
      );

Даже после этого приложение по-прежнему добавляет JSESSIONID в качестве файла cookie, а не в URL-адрес.

Может ли кто-нибудь помочь мне указать здесь правильное направление?

Вы можете решить это и другим способом. Если у вас есть SPA, вы можете использовать заголовки и память. Кроме того, если вы используете iframe, вам должно быть хорошо управлять своими собственными файлами cookie, вам следует создать сервлет, который "разделяет" сеанс в двух доменах, или я думаю, что лучшим решением было бы иметь прокси на domainA.com, который указывает на domainB.com. Таким образом, вы можете настроить любое сопоставление файлов cookie, все, что захотите. Если вы можете это использовать, я бы использовал github.com/mitre/HTTP-Proxy-Servlet. Я отправлю это как подробный ответ, если вы сможете использовать любой из этих методов. :)

Hash 08.06.2018 16:47

Попробуйте это и дайте мне знать. servletContext.setSessionTrackingModes (EnumSet.of (SessionTrackMode.URL));

Shubham Kadlag 13.06.2018 10:47
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
27
2
16 462
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Между сервером сайта DomainB.com и клиентским браузером может быть установлена ​​связь на основе токенов. Токен может быть отправлен с сервера DomainB.com в заголовке ответа после аутентификации. Затем клиентский браузер может сохранить токен в локальном хранилище / хранилище сеансов (также имеет срок действия). Затем клиент может отправлять токен в заголовок каждого запроса. Надеюсь это поможет.

Ответ принят как подходящий

Вы смотрели на Весенняя сессия: HttpSession и RestfulAPI, который использует заголовки HTTP вместо файлов cookie. См. Примеры проектов REST в REST Образец.

Я опубликовал решение, которое в итоге выбрало (больше изменений на уровне инфраструктуры, а не на уровне приложений), но если бы мне понадобилось другое решение, оно было бы вторым. Спасибо!

Phas1c 19.06.2018 06:56

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

David Canós 05.12.2018 01:25

@ DavidCanós, так и должно быть. Этот подход меняет только то, как информация о сеансе передается между клиентом и сервером, то есть в заголовке вместо файла cookie или части URL-адреса, но я не пробовал ваш вариант использования.

Jean Marois 05.12.2018 16:01

@JeanMarois, мне удалось это сделать. Он работает так, как ожидалось, вы должны отправить x-auth-token в заголовке, и он действительно работает как файл cookie (сеансы авторизации или не авторизации). Он создает сеанс на сервере и позволяет вам работать в обычном режиме. Спасибо

David Canós 07.12.2018 01:50

Логины на основе форм - это в основном сеансы с сохранением состояния. В вашем сценарии лучше всего использовать сеансы без сохранения состояния.

JWT обеспечивает реализацию для этого. В основном это ключ, который вам нужно передавать в качестве заголовка в каждом HTTP-запросе. Итак, пока у вас есть ключ. API доступен.

Мы можем интегрировать JWT с Spring.

В основном вам нужно написать эту логику.

  • Создать ключевую логику
  • Используйте JWT в Spring Security
  • Подтверждать ключ при каждом вызове

Я могу дать вам фору

pom.xml

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

TokenHelper.java

Содержат полезные функции для проверки, проверки и анализа токена.

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import com.test.dfx.common.TimeProvider;
import com.test.dfx.model.LicenseDetail;
import com.test.dfx.model.User;


@Component
public class TokenHelper {

    protected final Log LOGGER = LogFactory.getLog(getClass());

    @Value("${app.name}")
    private String APP_NAME;

    @Value("${jwt.secret}")
    public String SECRET;    //  Secret key used to generate Key. Am getting it from propertyfile

    @Value("${jwt.expires_in}")
    private int EXPIRES_IN;  //  can specify time for token to expire. 

    @Value("${jwt.header}")
    private String AUTH_HEADER;


    @Autowired
    TimeProvider timeProvider;

    private SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512;  // JWT Algorithm for encryption


    public Date getIssuedAtDateFromToken(String token) {
        Date issueAt;
        try {
            final Claims claims = this.getAllClaimsFromToken(token);
            issueAt = claims.getIssuedAt();
        } catch (Exception e) {
            LOGGER.error("Could not get IssuedDate from passed token");
            issueAt = null;
        }
        return issueAt;
    }

    public String getAudienceFromToken(String token) {
        String audience;
        try {
            final Claims claims = this.getAllClaimsFromToken(token);
            audience = claims.getAudience();
        } catch (Exception e) {
            LOGGER.error("Could not get Audience from passed token");
            audience = null;
        }
        return audience;
    }

    public String refreshToken(String token) {
        String refreshedToken;
        Date a = timeProvider.now();
        try {
            final Claims claims = this.getAllClaimsFromToken(token);
            claims.setIssuedAt(a);
            refreshedToken = Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith( SIGNATURE_ALGORITHM, SECRET )
                .compact();
        } catch (Exception e) {
            LOGGER.error("Could not generate Refresh Token from passed token");
            refreshedToken = null;
        }
        return refreshedToken;
    }

    public String generateToken(String username) {
        String audience = generateAudience();
        return Jwts.builder()
                .setIssuer( APP_NAME )
                .setSubject(username)
                .setAudience(audience)
                .setIssuedAt(timeProvider.now())
                .setExpiration(generateExpirationDate())
                .signWith( SIGNATURE_ALGORITHM, SECRET )
                .compact();
    }



    private Claims getAllClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.error("Could not get all claims Token from passed token");
            claims = null;
        }
        return claims;
    }

    private Date generateExpirationDate() {
        long expiresIn = EXPIRES_IN;
        return new Date(timeProvider.now().getTime() + expiresIn * 1000);
    }

    public int getExpiredIn() {
        return EXPIRES_IN;
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        User user = (User) userDetails;
        final String username = getUsernameFromToken(token);
        final Date created = getIssuedAtDateFromToken(token);
        return (
                username != null &&
                username.equals(userDetails.getUsername()) &&
                        !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())
        );
    }

    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        return (lastPasswordReset != null && created.before(lastPasswordReset));
    }

    public String getToken( HttpServletRequest request ) {
        /**
         *  Getting the token from Authentication header
         *  e.g Bearer your_token
         */
        String authHeader = getAuthHeaderFromHeader( request );
        if ( authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }

        return null;
    }

    public String getAuthHeaderFromHeader( HttpServletRequest request ) {
        return request.getHeader(AUTH_HEADER);
    }


}

WebSecurity

SpringSecurity Logic для добавления проверки JWT

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS ).and()
        .exceptionHandling().authenticationEntryPoint( restAuthenticationEntryPoint ).and()
        .authorizeRequests()
        .antMatchers("/auth/**").permitAll()
        .antMatchers("/login").permitAll()
        .antMatchers("/home").permitAll()
        .antMatchers("/actuator/**").permitAll()
        .anyRequest().authenticated().and()
        .addFilterBefore(new TokenAuthenticationFilter(tokenHelper, jwtUserDetailsService), BasicAuthenticationFilter.class);

        http.csrf().disable();
    }

TokenAuthenticationFilter.java

Проверяйте каждый вызов на отдых на предмет наличия действующего жетона

package com.test.dfx.security;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.filter.OncePerRequestFilter;

public class TokenAuthenticationFilter extends OncePerRequestFilter {

    protected final Log logger = LogFactory.getLog(getClass());

    private TokenHelper tokenHelper;

    private UserDetailsService userDetailsService;

    public TokenAuthenticationFilter(TokenHelper tokenHelper, UserDetailsService userDetailsService) {
        this.tokenHelper = tokenHelper;
        this.userDetailsService = userDetailsService;
    }


    @Override
    public void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain
    ) throws IOException, ServletException {

        String username;
        String authToken = tokenHelper.getToken(request);

        logger.info("AuthToken: "+authToken);

        if (authToken != null) {
            // get username from token
            username = tokenHelper.getUsernameFromToken(authToken);
            logger.info("UserName: "+username);
            if (username != null) {
                // get user
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (tokenHelper.validateToken(authToken, userDetails)) {
                    // create authentication
                    TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails);
                    authentication.setToken(authToken);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }else{
                logger.error("Something is wrong with Token.");
            }
        }
        chain.doFilter(request, response);
    }


}

Я ценю все приведенные выше ответы - в итоге я выбрал более простое решение, не внося никаких изменений на уровне приложения, потому что владелец domainA.com был готов работать с нами. Размещаю это здесь для других, так как изначально я даже не думал об этом ...

По сути :

  • Владелец domainA.com создал DNS-запись для domainB.domainA.com -> domainB.com
  • Владелец domainB.com (я) запросил общедоступный SSL-сертификат для domainB.domainA.com с помощью «проверки электронной почты» (я сделал это через AWS, но уверен, что есть другие механизмы через других поставщиков)
  • Вышеупомянутый запрос был отправлен веб-мастерам domainA.com -> они утвердили и выпустили публичный сертификат.
  • После выпуска - я смог настроить свое приложение (или балансировщик нагрузки) для использования этого нового сертификата, и они настроили свое приложение так, чтобы оно указывало на «domainB.domainA.com» (который впоследствии перенаправлялся на domainB.com в DNS)
  • Теперь браузеры выпускают файлы cookie для domainB.domainA.com, и, поскольку они являются одним и тем же основным доменом, файлы cookie создаются без каких-либо дополнительных действий.

Еще раз спасибо за ответы, извиняюсь за то, что не выбрал ответ здесь - напряженная неделя.

Другие вопросы по теме