Spring 没有 cookie 的安全会话

Spring Security Sessions without cookies

我正在尝试在不利用 cookie 的情况下以 Spring 安全管理会话。原因是——我们的应用程序显示在来自另一个域的 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 安全建议)是将 JSESSIONID 作为 GET 参数包含在 URL 中。我不想这样做,但我想不出替代方案。

所以这个问题是关于:

查看 Spring 的相关文档,使用 enableSessionUrlRewriting 应该允许

所以我这样做了:

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

这并没有将 JSESSIONID 添加到 URL,但现在应该允许了。然后我利用找到的一些代码 将 "tracking mode" 设置为 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。

有人可以帮我指明正确的方向吗?

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

你看过Spring Session: HttpSession & RestfulAPI which uses HTTP headers instead of cookies. See the REST sample projects in REST Sample吗。

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

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

我们可以将 JWT 与 Spring 集成。

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

  • 生成按键逻辑
  • 在 Spring 安全性中使用 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);
    }


}

网络安全

Spring添加 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 的有效令牌

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 的所有者为域 B 创建了一个 DNS 记录。domainA.com -> domainB.com
  • domainB.com 的所有者(我)通过 "email validation" 为域 B.domainA.com 申请了一个 public SSL 证书(我通过 AWS 完成了此操作,但我确定通过其他提供商还有其他机制)
  • 以上请求已发送给 domainA.com 的网站管理员 -> 他们批准并颁发了 public 证书
  • 颁发后 - 我能够配置我的应用程序(或负载平衡器)以使用这个新证书,他们将他们的应用程序配置为指向 "domainB.domainA.com"(随后路由到 domainB.com DNS)
  • 现在,浏览器为域 B 发布 cookie。domainA.com 由于它们是相同的主域,因此无需任何变通方法即可创建 cookie。

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