Java 没有 cookie 的 Spring Security Sessions

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/50691187/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-10 23:45:51  来源:igfitidea点击:

Spring Security Sessions without cookies

javaiframespring-securitycross-domainjsessionid

提问by Phas1c

I'm trying to manage sessions in Spring Security without leveraging cookies. The reasoning is - our application is displayed within an iframe from another domain, we need to manage sessions in our application, and Safari restricts cross-domain cookie creation. (context : domainA.com displays domainB.com in an iframe. domainB.com is setting a JSESSIONID cookie to leverage on domainB.com, but since the user's browser is showing domainA.com - Safari restricts domainB.com from creating the cookie).

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

The only way I can think to achieve this (against OWASP security recommendations) - is to include the JSESSIONID in the URL as a GET parameter. I don't WANT to do this, but I can't think of an alternative.

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

So this question is both about :

所以这个问题是关于:

  • Are there better alternatives to tackling this problem?
  • If not - how can I achieve this with Spring Security
  • 有没有更好的替代方法来解决这个问题?
  • 如果没有 - 我如何使用 Spring Security 实现这一点

Reviewing Spring's Documentation around this, using enableSessionUrlRewritingshould allow for this

围绕这个查看 Spring 的文档,使用enableSessionUrlRewriting应该允许这个

So I've done this :

所以我这样做了:

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

This didn't add the JSESSIONID to the URL, but it should be allowed now. I then leveraged some code found in this questionto set the "tracking mode" to URL

这没有将 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)
      );

Even after this - the application still adds the JSESSIONID as a cookie and not in the URL.

即使在此之后 - 应用程序仍将 JSESSIONID 添加为 cookie 而不是在 URL 中。

Can someone help point me in the right direction here?

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

采纳答案by Jean Marois

Have you looked at Spring Session: HttpSession & RestfulAPIwhich uses HTTP headers instead of cookies. See the REST sample projects in REST Sample.

您是否看过使用 HTTP 标头而不是 cookie 的Spring Session: HttpSession & RestfulAPI。看到REST示例项目REST样品

回答by Amit Parashar

You can have a token based communication between the site DomainB.com server and the client browser. The token can be sent from the DomainB.com server in the response's header , after authentication. The client browser can then save the token in localstorage/session storage (have a expiry time too). The client can then send the token in every request's header. Hope this helps.

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

回答by MyTwoCents

Form based logins are mainly stateful sessions. In your scenario using stateless sessions would be best.

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

JWTprovide implementation for this. Its basically a key which you need to pass as header in each HTTP request. So as long as you have the key. API is available.

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

We can integrate JWT with Spring.

我们可以将 JWT 与 Spring 集成。

Basically you need to write these logic.

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

  • Generate Key Logic
  • Use JWT in Spring Security
  • Validate key on each call
  • 生成密钥逻辑
  • 在 Spring Security 中使用 JWT
  • 每次调用时验证密钥

I can give you a head start

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

pom.xml

pom.xml

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

TokenHelper.java

TokenHelper.java

Contain useful functions for validating, checking, and parsing Token.

包含用于验证、检查和解析 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);
    }


}

WebSecurity

网络安全

SpringSecurity Logic to add JWT check

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

TokenAuthenticationFilter.java

Check each Rest Call for valid Token

检查每个 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);
    }


}

回答by Phas1c

I appreciate all the answers above - I ended up opting for an easier solution without making any application-level changes because the owner of domainA.com was willing to work with us. Posting it here for others, as I didn't even think of this originally...

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

Basically :

基本上 :

  • Owner of domainA.com created a DNS record to for domainB.domainA.com -> domainB.com
  • Owner of domainB.com (me) requested a public SSL certificate for domainB.domainA.com via "email validation" (I did this through AWS, but I'm sure there are other mechanisms via othe providers)
  • The above request was sent to the webmasters of domainA.com -> they approved and issued the public certificate
  • Once issued - I was able to configure my application (or load balancer) to use this new certificate, and they configured their application to point to "domainB.domainA.com" (which subsequently routed to domainB.com in DNS)
  • Now, the browsers issue cookies for domainB.domainA.com and since they are the same primary domain, the cookies get created without any work-arounds needed.
  • 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。

Thanks again for the answers, apologies for not selecting an answer here - busy week.

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