没有cookie的Spring安全会话

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

我能想到实现这一目标的唯一方法(针对OWASP安全建议) – 将URL中的JSESSIONID作为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。

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

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

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

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

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

我们可以将JWT与Spring集成。

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

  • 生成关键逻辑
  • 在Spring Security中使用JWT
  • validation每次通话的密钥

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

的pom.xml

  io.jsonwebtoken jjwt 0.9.0  

TokenHelper.java

包含用于validation,检查和解析令牌的有用函数。

 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 * eg 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

检查每个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的所有者为domainB.domainA.com – > domainB.com创建了DNS记录
  • domainB.com(我)的所有者通过“电子邮件validation”为domainB.domainA.com申请了公共SSL证书(我通过AWS做到了这一点,但我确信通过其他提供商还有其他机制)
  • 上述请求已发送至domainA.com的网站管理员 – >他们批准并颁发了公共证书
  • 一旦发布 – 我能够配置我的应用程序(或负载均衡器)以使用此新证书,并且他们将其应用程序配置为指向“domainB.domainA.com”(随后在DNS中路由到domainB.com)
  • 现在,浏览器为domainB.domainA.com发布cookie,因为它们是相同的主域,所以创建cookie时无需任何解决方法。

再次感谢您的回答,不要在这里选择答案而道歉 – 忙碌的一周。