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 安全性
实现此目的
查看 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。
再次感谢您的回答,很抱歉没有在这里选择答案 - 忙碌的一周。
我正在尝试在不利用 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 安全性 实现此目的
查看 Spring 的相关文档,使用 enableSessionUrlRewriting 应该允许
所以我这样做了:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.enableSessionUrlRewriting(true)
这并没有将 JSESSIONID 添加到 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。
再次感谢您的回答,很抱歉没有在这里选择答案 - 忙碌的一周。