I'm attempting to implement a microservices architecture using Spring Cloud's Zuul, Eureka, and my own services. I have multiple services that have UIs and services and each can authenticate users using x509 security. Now I'm trying to place Zuul in front of those services. Since Zuul can't forward client certs to the backend, I thought the next best thing would be to authenticate the user at the front door in Zuul, then use Spring Session to replicate their authenticated state across the backend services. I have followed the tutorial here from Dave Syer and it almost works, but not on the first request. Here is my basic setup:
If you clear all the sessions and start fresh and try to hit the proxy, then it sends the request to the backend using the zuul server's certificate. The backend service then looks up the user based on that user cert and thinks the user is the server, not the user that was authenticated in the Zuul proxy. If you just refresh the page, then you suddenly become the correct user on the back end (the user authenticated in the Zuul proxy). The way I'm checking is to print out the Principal user in the backend controller. So on first request, I see the server user, and on second request, I see the real user. If I disable x509 on the back end, on the first request, I get a 403, then on refresh, it lets me in.
It seems like the session isn't replicated to the backend fast enough so when the user is authenticated in the frontend, it hasn't made it to the backend by the time Zuul forwards the request.
Is there a way to guarantee the session is replicated on the first request (i.e. session creation)? Or am I missing a step to ensure this works correctly?
Here are some of the important code snippets:
Zuul Proxy:
@SpringBootApplication
@Controller
@EnableAutoConfiguration
@EnableZuulProxy
@EnableRedisHttpSession
public class ZuulEdgeServer {
public static void main(String[] args) {
new SpringApplicationBuilder(ZuulEdgeServer.class).web(true).run(args);
}
}
Zuul Config:
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
Backend Service:
@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);
}
}
Backend Service Config:
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}
One of the Backend Controllers:
@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";
}
}
Thanks to shobull for pointing me to Justin Taylor's answer to this problem. For completeness, I wanted to put the full answer here too. It's a two part solution:
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE)
which saves session data into Redis immediately. Documentation here.Simple Zuul filter for adding session into current request's header:
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;
}
}
Both of these should be within your Zuul Proxy.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With