没有 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 标志?