【问题标题】:Spring Security Sessions without cookies没有 cookie 的 Spring Security 会话
【发布时间】:2018-11-14 09:57:42
【问题描述】:

我正在尝试在不利用 cookie 的情况下管理 Spring Security 中的会话。原因是 - 我们的应用程序显示在来自另一个域的 iframe 中,我们需要在我们的应用程序 and Safari restricts cross-domain cookie creation 中管理会话。 (上下文:domainA.com 在 iframe 中显示 domainB.com。domainB.com 正在设置 JSESSIONID cookie 以利用 domainB.com,但由于用户的浏览器显示 domainA.com - Safari 限制 domainB.com 创建 cookie) .

我能想到的唯一方法(违反 OWASP 安全建议)是在 URL 中包含 JSESSIONID 作为 GET 参数。我不想这样做,但我想不出替代方案。

所以这个问题是关于:

  • 有没有更好的方法来解决这个问题?
  • 如果不是 - 我如何使用 Spring Security 实现这一目标

使用enableSessionUrlRewriting查看Spring的文档应该允许这样做

所以我已经这样做了:

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

这并没有将 JSESSIONID 添加到 URL,但现在应该允许。然后我利用找到的一些代码 in this question 将“跟踪模式”设置为 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 应该没问题,您应该创建一个在两个域中“共享”会话的 servlet,或者我认为最好的解决方案是在 domainA.com 上拥有一个代理它指向 domainB.com。通过这种方式,您可以设置任何 cookie 映射,任何您想要的。如果您可以利用它,我会使用 github.com/mitre/HTTP-Proxy-Servlet 。如果您能够使用这些方法中的任何一种,我会将其作为详细答案发布。 :)
  • 试试这个,让我知道。 servletContext.setSessionTrackingModes(EnumSet.of(SessionTrackingMode.URL));

标签: java iframe spring-security cross-domain jsessionid


【解决方案1】:

感谢以上所有答案 - 因为 domainA.com 的所有者愿意与我们合作,我最终选择了一个更简单的解决方案,而无需进行任何应用程序级别的更改。在这里发给其他人,因为我原本什至没有想到这个......

基本上:

  • domainA.com 的所有者为 domainB.domainA.com -> domainB.com 创建了 DNS 记录
  • domainB.com 的所有者(我)通过“电子邮件验证”请求 domainB.domainA.com 的公共 SSL 证书(我通过 AWS 进行了此操作,但我确信通过其他提供商还有其他机制)
  • 上述请求已发送给 domainA.com 的网站管理员 -> 他们批准并颁发了公共证书
  • 发布后 - 我能够配置我的应用程序(或负载平衡器)以使用此新证书,并且他们将其应用程序配置为指向“domainB.domainA.com”(随后在 DNS 中路由到 domainB.com)
  • 现在,浏览器为 domainB.domainA.com 发出 cookie,由于它们是同一个主域,因此无需任何变通方法即可创建 cookie。

再次感谢您的回答,很抱歉没有在这里选择答案 - 忙碌的一周。

【讨论】:

    【解决方案2】:

    基于表单的登录主要是有状态的会话。在您的场景中,最好使用无状态会话。

    JWT 为此提供了实现。它基本上是您需要在每个 HTTP 请求中作为标头传递的密钥。 所以只要你有钥匙。 API 可用。

    我们可以将 JWT 与 Spring 集成。

    基本上你需要写这些逻辑。

    • 生成关键逻辑
    • 在 Spring Security 中使用 JWT
    • 每次调用时验证密钥

    我可以给你一个良好的开端

    pom.xml

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

    TokenHelper.java

    包含用于验证、检查和解析 Token 的有用函数。

    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);
        }
    
    
    }
    

    网络安全

    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

    检查每个 Rest Call 是否有有效的 Token

    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);
        }
    
    
    }
    

    【讨论】:

      【解决方案3】:

      您是否查看过Spring Session: HttpSession & RestfulAPI,它使用 HTTP 标头而不是 cookie。请参阅REST Sample 中的 REST 示例项目。

      【讨论】:

      • 我发布了我最终采用的解决方案(更多基础设施而不是应用程序级别的更改) - 但如果我需要另一个解决方案 - 这将是亚军。谢谢!
      • 此示例适用于身份验证,但我需要像常规会话一样为每个用户(有或没有身份验证)获取一个会话。这可能吗?
      • @DavidCanós,应该。这种方法只会改变会话信息在客户端和服务器之间的传输方式,即在标头而不是 cookie 或部分 url 中,但我没有尝试过你的用例。
      • @JeanMarois 我设法做到了。它只是按预期工作,您必须在标头中发送 x-auth-token 并且它确实作为 cookie 工作(auth 或非 auth 会话)。它在服务器中创建一个会话并允许您照常工作。谢谢
      【解决方案4】:

      您可以在站点 DomainB.com 服务器和客户端浏览器之间进行基于令牌的通信。身份验证后,令牌可以在响应的标头中从 DomainB.com 服务器发送。然后客户端浏览器可以将令牌保存在本地存储/会话存储中(也有到期时间)。然后,客户端可以在每个请求的标头中发送令牌。希望这会有所帮助。

      【讨论】:

        猜你喜欢
        • 2011-02-06
        • 2014-11-03
        • 2018-04-26
        • 2011-09-14
        • 2017-07-05
        • 1970-01-01
        • 2017-08-17
        • 2016-12-05
        相关资源
        最近更新 更多