没有 cookie 的 Spring Security 会话

Posted

技术标签:

【中文标题】没有 cookie 的 Spring Security 会话【英文标题】:Spring Security Sessions without cookies 【发布时间】: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)); 【参考方案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 服务器发送。然后客户端浏览器可以将令牌保存在本地存储/会话存储中(也有到期时间)。然后,客户端可以在每个请求的标头中发送令牌。希望这会有所帮助。

【讨论】:

以上是关于没有 cookie 的 Spring Security 会话的主要内容,如果未能解决你的问题,请参考以下文章

没有 cookie 的 Spring Security 会话

Java中Cookie常用操作类(Spring中操作Cookie)

如何在 Spring Boot 中设置同站点 cookie 标志?

Spring Security常用过滤器介绍

Spring Security 中会话 cookie 的同站点标志

Spring Security 返回 403 而不是 401 并创建无效的 Redis 会话 cookie