使用 Spring Boot、Session 和 Redis 创建会话时未复制会话
Session not replicated on session creation with Spring Boot, Session, and Redis
我正在尝试使用 Spring Cloud 的 Zuul、Eureka 和我自己的服务来实现微服务架构。我有多个具有 UI 和服务的服务,每个服务都可以使用 x509 安全性对用户进行身份验证。现在我正试图将 Zuul 放在这些服务的前面。由于 Zuul 无法将客户端证书转发到后端,我认为下一个最好的办法是在 Zuul 的前门对用户进行身份验证,然后使用 Spring Session 在后端服务中复制他们的身份验证状态。我遵循了 Dave Syer 的教程 here,它几乎可以工作,但不是在第一次请求时。这是我的基本设置:
- 它自己的应用程序中的 Zuul 代理设置为路由到后端服务。已启用 Spring 安全性以执行 x509 身份验证。成功授权用户。还有 Spring 与 @EnableRedisHttpSession
的会话
- 后端服务还启用了 spring 安全性。我在这里都尝试了 enabling/disabling x509,但始终要求用户针对特定端点进行身份验证。还使用 Spring Session 和 @EnableRedisHttpSession。
如果您清除所有会话并重新开始并尝试访问代理,那么它会使用 zuul 服务器的证书将请求发送到后端。后端服务然后根据该用户证书查找用户,并认为该用户是服务器,而不是在 Zuul 代理中经过身份验证的用户。如果你只是刷新页面,那么你突然变成了后端的正确用户(在 Zuul 代理中认证的用户)。我检查的方式是在后端控制器中打印出 Principal 用户。所以在第一次请求时,我看到了服务器用户,在第二次请求时,我看到了真实用户。如果我在后端禁用 x509,在第一次请求时,我会收到 403,然后在刷新时,它会让我进入。
似乎会话没有足够快地复制到后端,所以当用户在前端进行身份验证时,在 Zuul 转发请求时它还没有到达后端。
有没有办法保证会话在第一次请求时被复制(即会话创建)?还是我错过了确保其正常工作的步骤?
以下是一些重要的代码片段:
Zuul 代理:
@SpringBootApplication
@Controller
@EnableAutoConfiguration
@EnableZuulProxy
@EnableRedisHttpSession
public class ZuulEdgeServer {
public static void main(String[] args) {
new SpringApplicationBuilder(ZuulEdgeServer.class).web(true).run(args);
}
}
Zuul 配置:
info:
component: Zuul Server
endpoints:
restart:
enabled: true
shutdown:
enabled: true
health:
sensitive: false
zuul:
routes:
service1: /**
logging:
level:
ROOT: INFO
# org.springframework.web: DEBUG
net.acesinc: DEBUG
security.sessions: ALWAYS
server:
port: 8443
ssl:
key-store: classpath:dev/localhost.jks
key-store-password: thepassword
keyStoreType: JKS
keyAlias: localhost
clientAuth: want
trust-store: classpath:dev/localhost.jks
ribbon:
IsSecure: true
后端服务:
@SpringBootApplication
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class, ThymeleafAutoConfiguration.class, org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration.class })
@EnableEurekaClient
@EnableRedisHttpSession
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
后端服务配置:
spring.jmx.default-domain: ${spring.application.name}
server:
port: 8444
ssl:
key-store: classpath:dev/localhost.jks
key-store-password: thepassword
keyStoreType: JKS
keyAlias: localhost
clientAuth: want
trust-store: classpath:dev/localhost.jks
#Change the base url of all REST endpoints to be under /rest
spring.data.rest.base-uri: /rest
security.sessions: NEVER
logging:
level:
ROOT: INFO
# org.springframework.web: INFO
# org.springframework.security: DEBUG
net.acesinc: DEBUG
eureka:
instance:
nonSecurePortEnabled: false
securePortEnabled: true
securePort: ${server.port}
homePageUrl: https://${eureka.instance.hostname}:${server.port}/
secureVirtualHostName: ${spring.application.name}
后端控制器之一:
@Controller
public class SecureContent1Controller {
private static final Logger log = LoggerFactory.getLogger(SecureContent1Controller.class);
@RequestMapping(value = {"/secure1"}, method = RequestMethod.GET)
@PreAuthorize("isAuthenticated()")
public @ResponseBody String getHomepage(ModelMap model, Principal p) {
log.debug("Secure Content for user [ " + p.getName() + " ]");
model.addAttribute("pageName", "secure1");
return "You are: [ " + p.getName() + " ] and here is your secure content: secure1";
}
}
Spring 会话支持当前在提交请求时写入数据存储。这是为了尝试通过一次写入所有属性来减少"chatty traffic"。
我们认识到这对于某些场景(比如您所面对的场景)来说并不理想。对于这些,我们有 spring-session/issues/250. The workaround is to copy RedisOperationsSessionRepository and invoke saveDelta 随时 属性 更改。
感谢 shobull 指出 Justin Taylor's answer 这个问题。为了完整起见,我也想把完整的答案放在这里。这是一个分为两部分的解决方案:
- 使 Spring Session 热切地提交 - 因为 spring-session v1.0 有注释 属性
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE)
将 session 数据保存到Redis马上。文档 here.
用于将 session 添加到当前请求的 header:
的简单 Zuul 过滤器
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import javax.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.stereotype.Component;
@Component
public class SessionSavingZuulPreFilter extends ZuulFilter {
@Autowired
private SessionRepository repository;
private static final Logger log = LoggerFactory.getLogger(SessionSavingZuulPreFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpSession httpSession = context.getRequest().getSession();
Session session = repository.getSession(httpSession.getId());
context.addZuulRequestHeader("Cookie", "SESSION=" + httpSession.getId());
log.trace("ZuulPreFilter session proxy: {}", session.getId());
return null;
}
}
这两个都应该在您的 Zuul 代理中。
我正在尝试使用 Spring Cloud 的 Zuul、Eureka 和我自己的服务来实现微服务架构。我有多个具有 UI 和服务的服务,每个服务都可以使用 x509 安全性对用户进行身份验证。现在我正试图将 Zuul 放在这些服务的前面。由于 Zuul 无法将客户端证书转发到后端,我认为下一个最好的办法是在 Zuul 的前门对用户进行身份验证,然后使用 Spring Session 在后端服务中复制他们的身份验证状态。我遵循了 Dave Syer 的教程 here,它几乎可以工作,但不是在第一次请求时。这是我的基本设置:
- 它自己的应用程序中的 Zuul 代理设置为路由到后端服务。已启用 Spring 安全性以执行 x509 身份验证。成功授权用户。还有 Spring 与 @EnableRedisHttpSession 的会话
- 后端服务还启用了 spring 安全性。我在这里都尝试了 enabling/disabling x509,但始终要求用户针对特定端点进行身份验证。还使用 Spring Session 和 @EnableRedisHttpSession。
如果您清除所有会话并重新开始并尝试访问代理,那么它会使用 zuul 服务器的证书将请求发送到后端。后端服务然后根据该用户证书查找用户,并认为该用户是服务器,而不是在 Zuul 代理中经过身份验证的用户。如果你只是刷新页面,那么你突然变成了后端的正确用户(在 Zuul 代理中认证的用户)。我检查的方式是在后端控制器中打印出 Principal 用户。所以在第一次请求时,我看到了服务器用户,在第二次请求时,我看到了真实用户。如果我在后端禁用 x509,在第一次请求时,我会收到 403,然后在刷新时,它会让我进入。
似乎会话没有足够快地复制到后端,所以当用户在前端进行身份验证时,在 Zuul 转发请求时它还没有到达后端。
有没有办法保证会话在第一次请求时被复制(即会话创建)?还是我错过了确保其正常工作的步骤?
以下是一些重要的代码片段:
Zuul 代理:
@SpringBootApplication
@Controller
@EnableAutoConfiguration
@EnableZuulProxy
@EnableRedisHttpSession
public class ZuulEdgeServer {
public static void main(String[] args) {
new SpringApplicationBuilder(ZuulEdgeServer.class).web(true).run(args);
}
}
Zuul 配置:
info:
component: Zuul Server
endpoints:
restart:
enabled: true
shutdown:
enabled: true
health:
sensitive: false
zuul:
routes:
service1: /**
logging:
level:
ROOT: INFO
# org.springframework.web: DEBUG
net.acesinc: DEBUG
security.sessions: ALWAYS
server:
port: 8443
ssl:
key-store: classpath:dev/localhost.jks
key-store-password: thepassword
keyStoreType: JKS
keyAlias: localhost
clientAuth: want
trust-store: classpath:dev/localhost.jks
ribbon:
IsSecure: true
后端服务:
@SpringBootApplication
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class, ThymeleafAutoConfiguration.class, org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration.class })
@EnableEurekaClient
@EnableRedisHttpSession
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
后端服务配置:
spring.jmx.default-domain: ${spring.application.name}
server:
port: 8444
ssl:
key-store: classpath:dev/localhost.jks
key-store-password: thepassword
keyStoreType: JKS
keyAlias: localhost
clientAuth: want
trust-store: classpath:dev/localhost.jks
#Change the base url of all REST endpoints to be under /rest
spring.data.rest.base-uri: /rest
security.sessions: NEVER
logging:
level:
ROOT: INFO
# org.springframework.web: INFO
# org.springframework.security: DEBUG
net.acesinc: DEBUG
eureka:
instance:
nonSecurePortEnabled: false
securePortEnabled: true
securePort: ${server.port}
homePageUrl: https://${eureka.instance.hostname}:${server.port}/
secureVirtualHostName: ${spring.application.name}
后端控制器之一:
@Controller
public class SecureContent1Controller {
private static final Logger log = LoggerFactory.getLogger(SecureContent1Controller.class);
@RequestMapping(value = {"/secure1"}, method = RequestMethod.GET)
@PreAuthorize("isAuthenticated()")
public @ResponseBody String getHomepage(ModelMap model, Principal p) {
log.debug("Secure Content for user [ " + p.getName() + " ]");
model.addAttribute("pageName", "secure1");
return "You are: [ " + p.getName() + " ] and here is your secure content: secure1";
}
}
Spring 会话支持当前在提交请求时写入数据存储。这是为了尝试通过一次写入所有属性来减少"chatty traffic"。
我们认识到这对于某些场景(比如您所面对的场景)来说并不理想。对于这些,我们有 spring-session/issues/250. The workaround is to copy RedisOperationsSessionRepository and invoke saveDelta 随时 属性 更改。
感谢 shobull 指出 Justin Taylor's answer 这个问题。为了完整起见,我也想把完整的答案放在这里。这是一个分为两部分的解决方案:
- 使 Spring Session 热切地提交 - 因为 spring-session v1.0 有注释 属性
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE)
将 session 数据保存到Redis马上。文档 here. 用于将 session 添加到当前请求的 header:
的简单 Zuul 过滤器import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import javax.servlet.http.HttpSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.session.Session; import org.springframework.session.SessionRepository; import org.springframework.stereotype.Component; @Component public class SessionSavingZuulPreFilter extends ZuulFilter { @Autowired private SessionRepository repository; private static final Logger log = LoggerFactory.getLogger(SessionSavingZuulPreFilter.class); @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext context = RequestContext.getCurrentContext(); HttpSession httpSession = context.getRequest().getSession(); Session session = repository.getSession(httpSession.getId()); context.addZuulRequestHeader("Cookie", "SESSION=" + httpSession.getId()); log.trace("ZuulPreFilter session proxy: {}", session.getId()); return null; } }
这两个都应该在您的 Zuul 代理中。