Using zuul is possible to redirect a request to an external link like http://www.google.com ?
I have this scenario.
In a webpage there are a bunch of links pointing to a several websites. When you click to one of these zuul checks if you have the permission to visit this page and redirect the browser to the external link.
I've created a route filter.
public class TestZuulFilter extends ZuulFilter {
#Override
public String filterType() {
return "route";
}
#Override
public int filterOrder() {
return 5;
}
#Override
public boolean shouldFilter() {
// ... filter logic ...
}
#Override
public Object run() {
// ... permission check ...
RequestContext ctx = RequestContext.getCurrentContext();
//todo redirect
}
}
How can i redirect the user browser to google.com ?
Thank you.
Update 20/09/2016
I've managed to solve my problem changing filter type from "pre" to "post" and adding the Location HTTP header to the response.
public class TestZuulFilter extends ZuulFilter {
#Override
public String filterType() {
return "post";
}
#Override
public int filterOrder() {
return 5;
}
#Override
public boolean shouldFilter() {
// ... filter logic ...
}
#Override
public Object run() {
// ... permission check ...
RequestContext ctx = RequestContext.getCurrentContext();
//redirect
HttpServletResponse response = ctx.getResponse();
response.setStatus(HttpServletResponse.SC_FOUND);
response.setHeader("Location", "http://www.google.com");
return null;
}
}
Now it works, but is this the right way to do it ?
Related
Let's say I have the following controller with its parent class:
#RestController
public class BusinessController extends RootController {
#GetMapping(value = "users", produces = {"application/json"})
#ResponseBody
public String users() {
return "{ \"users\": [] }"
}
#GetMapping(value = "companies", produces = {"application/json"})
#ResponseBody
public String companies() {
return "{ \"companies\": [] }"
}
}
#RestController
#RequestMapping(path = "api")
public class RootController {
}
Data is retrieved by calling such URL's:
http://app.company.com/api/users
http://app.company.com/api/companies
Now let's say I want to rename the /api path to /rest but keep it "available" by returning a 301 HTTP status code alongside the new URI's
e.g. client request:
GET /api/users HTTP/1.1
Host: app.company.com
server request:
HTTP/1.1 301 Moved Permanently
Location: http://app.company.com/rest/users
So I plan to change from "api" to "rest" in my parent controller:
#RestController
#RequestMapping(path = "rest")
public class RootController {
}
then introduce a "legacy" controller:
#RestController
#RequestMapping(path = "api")
public class LegacyRootController {
}
but now how to make it "rewrite" the "legacy" URI's?
That's what I'm struggling with, I can't find anything Spring-related on the matter, whether on StackOverflow or elsewhere.
Also I have many controllers AND many methods-endpoints so I can not do this manually (i.e. by editing every #RequestMapping/#GetMapping annotations).
And project I'm working on is based on Spring Boot 2.1
Edit: I removed the /business path because actually inheritance doesn't work "by default" (see questions & answers like Spring MVC #RequestMapping Inheritance or Modifying #RequestMappings on startup ) - sorry for that.
I finally found a way to implement this, both as a javax.servlet.Filter AND a org.springframework.web.server.WebFilter implementation.
In fact, I introduced the Adapter pattern in order to transform both:
org.springframework.http.server.ServletServerHttpResponse (non-reactive) and
org.springframework.http.server.reactive.ServerHttpResponse (reactive)
because on the contrary of the Spring's HTTP requests' wrappers which share org.springframework.http.HttpRequest (letting me access both URI and HttpHeaders), the responses's wrappers do not share a common interface that does it, so I had to emulate one (here purposely named in a similar fashion, HttpResponse).
#Component
public class RestRedirectWebFilter implements Filter, WebFilter {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
ServletServerHttpRequest request = new ServletServerHttpRequest((HttpServletRequest) servletRequest);
ServletServerHttpResponse response = new ServletServerHttpResponse((HttpServletResponse) servletResponse);
if (actualFilter(request, adapt(response))) {
chain.doFilter(servletRequest, servletResponse);
}
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (actualFilter(exchange.getRequest(), adapt(exchange.getResponse()))) {
return chain.filter(exchange);
} else {
return Mono.empty();
}
}
/**
* Actual filtering.
*
* #param request
* #param response
* #return boolean flag specifying if filter chaining should continue.
*/
private boolean actualFilter(HttpRequest request, HttpResponse response) {
URI uri = request.getURI();
String path = uri.getPath();
if (path.startsWith("/api/")) {
String newPath = path.replaceFirst("/api/", "/rest/");
URI location = UriComponentsBuilder.fromUri(uri).replacePath(newPath).build().toUri();
response.getHeaders().setLocation(location);
response.setStatusCode(HttpStatus.MOVED_PERMANENTLY);
response.flush();
return false;
}
return true;
}
interface HttpResponse extends HttpMessage {
void setStatusCode(HttpStatus status);
void flush();
}
private HttpResponse adapt(ServletServerHttpResponse response) {
return new HttpResponse() {
public HttpHeaders getHeaders() {
return response.getHeaders();
}
public void setStatusCode(HttpStatus status) {
response.setStatusCode(status);
}
public void flush() {
response.close();
}
};
}
private HttpResponse adapt(org.springframework.http.server.reactive.ServerHttpResponse response) {
return new HttpResponse() {
public HttpHeaders getHeaders() {
return response.getHeaders();
}
public void setStatusCode(HttpStatus status) {
response.setStatusCode(status);
}
public void flush() {
response.setComplete();
}
};
}
}
Since it looks like you want to preserve the 301 but also have it return a response, you do have the option to wire in your RootController into your LegacyRootController
That way you can provide reuse the logic you have in the RootController but return different response codes and serve different paths on your LegacyRootController
#RestController
#RequestMapping(path = "api")
public class LegacyRootController {
private final RootController rootController;
public LegacyRootController(RootController rootController) {
this.rootController = rootController;
}
#GetMapping(value = "users", produces = {"application/json"})
#ResponseStatus(HttpStatus.MOVED_PERMANENTLY) // Respond 301
#ResponseBody
public String users() {
return rootController.users(); // Use rootController to provide appropriate response.
}
#GetMapping(value = "companies", produces = {"application/json"})
#ResponseStatus(HttpStatus.MOVED_PERMANENTLY)
#ResponseBody
public String companies() {
return rootController.companies();
}
}
This will allow you to serve /api/users to serve up a response with a 301, while also allowing you to serve /rest/users with your standard response.
If you would like to add the Location headers, you can have your LegacyRootController return a ResponseEntity to provide the body, code and header values.
#GetMapping(value = "users", produces = {"application/json"})
public ResponseEntity<String> users() {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setLocation("...");
return new ResponseEntity<String>(rootController.users(), responseHeaders, HttpStatus.MOVED_PERMANENTLY);
}
If you want to serve multiple endpoints that does not serve different status codes, you can simply provide multiple paths
#RequestMapping(path = {"api", "rest"})
For learning purposes, I have made a custom authentication system where I pass a token from the client to the server through the Authorization header.
In the server side, I'd like to know if it's possible to create in the interceptor, before the request reaches a method in the controller, an User object with the email from the token as a property, and then pass this user object to every request where I require it.
This what I'd like to get, as an example:
#RestController
public class HelloController {
#RequestMapping("/")
public String index(final User user) {
return user.getEmail();
}
}
public class User {
private String email;
}
Where user is an object that I created in the pre-interceptor using the request Authorization header and then I can pass, or not, to any method in the RestController.
Is this possible?
#Recommended solution
I would create a #Bean with #Scope request which would hold the user and then put the appropriate entity into that holder and then take from that holder inside the method.
#Component
#Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class CurrentUser {
private User currentUser;
public User getCurrentUser() {
return currentUser;
}
public void setCurrentUser(User currentUser) {
this.currentUser = currentUser;
}
}
and then
#Component
public class MyInterceptor implements HandlerInterceptor {
private CurrentUser currentUser;
#Autowired
MyInterceptor(CurrentUser currentUser) {
this.currentUser = currentUser;
}
#Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
this.currentUser.setCurrentUser(new User("whatever"));
return true;
}
}
and in the Controller
#RestController
public class HelloController {
private CurrentUser currentUser;
#Autowired
HelloController(CurrentUser currentUser) {
this.currentUser = currentUser;
}
#RequestMapping("/")
public String index() {
return currentUser.getCurrentUser().getEmail();
}
}
#Alternative solution
In case your object that you would like to have, only contains one field, you can just cheat on that and add that field to the HttpServletRequest parameters and just see the magic happen.
#Component
public class MyInterceptor implements HandlerInterceptor {
#Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//TRY ONE AT THE TIME: email OR user
//BOTH SHOULD WORK BUT SEPARATELY OF COURSE
request.setAttribute("email", "login#domain.com");
request.setAttribute("user", new User("login#domain.com"));
return true;
}
}
You can use a local thread context object as follows - which will be handling one parameter per request thread (thread safe):
public abstract class LoggedUserContext {
private static ThreadLocal<User> currentLoggedUser = new ThreadLocal<>();
public static void setCurrentLoggedUser(User loggedUser) {
if (currentLoggedUser == null) {
currentLoggedUser = new ThreadLocal<>();
}
currentLoggedUser.set(loggedUser);
}
public static User getCurrentLoggedUser() {
return currentLoggedUser != null ? currentLoggedUser.get() : null;
}
public static void clear() {
if (currentLoggedUser != null) {
currentLoggedUser.remove();
}
}
}
Then in the interceptor prehandle function:
LoggedUserContext.setCurrentLoggedUser(loggedUser);
And in the interceptor postHandler function:
LoggedUserContext.clear();
From any other place:
User loggedUser = LoggedUserContext.getCurrentLoggedUser();
I have a Zuul custom filter of type PRE_TYPE.
When I receive the request I want to prevent its routing and instead send a response, in this case a SOAP message, since I am simulating a web service response.
My custom filter:
#Component
public class CustomFilter extends ZuulFilter {
private ThreadLocal<byte[]> buffers;
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
RequestContext ctx = getCurrentContext();
ctx.unset();
String s= "<soap:Envelope xmlns:......</soap:Envelope>";
}
#Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
#Override
public int filterOrder() {
return 0;
}
}
I need to create a HttpServletResponse and fill it with my response and write it to the output stream, so the client receives that response.
How can I create the servletresponse object?
Try something like this:
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.setSendZuulResponse(false);
ctx.setResponseBody("<soap:Envelope xmlns:......</soap:Envelope>");
ctx.setResponseStatusCode(...);
return null;
}
How to make Zuul dynamic routing based on HTTP method (GET/POST/PUT...)?
For example, when you need to route the POST request to the different host instead of the default one described in 'zuul.routes.*'...
zuul:
routes:
first-service:
path: /first/**
serviceId: first-service
stripPrefix: false
second-service:
path: /second/**
serviceId: second-service
stripPrefix: false
I.e. when we request 'GET /first' then Zuul route the request to the 'first-service', but if we request 'POST /first' then Zuul route the request to the 'second-service'.
To implement dynamic routing based on HTTP method we can create a custom 'route' type ZuulFilter and resolve 'serviceId' through DiscoveryClient. Fore example:
#Component
public class PostFilter extends ZuulFilter {
private static final String REQUEST_PATH = "/first";
private static final String TARGET_SERVICE = "second-service";
private static final String HTTP_METHOD = "POST";
private final DiscoveryClient discoveryClient;
public PostOrdersFilter(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
#Override
public String filterType() {
return "route";
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String method = request.getMethod();
String requestURI = request.getRequestURI();
return HTTP_METHOD.equalsIgnoreCase(method) && requestURI.startsWith(REQUEST_PATH);
}
#Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
List<ServiceInstance> instances = discoveryClient.getInstances(TARGET_SERVICE);
try {
if (instances != null && instances.size() > 0) {
context.setRouteHost(instances.get(0).getUri().toURL());
} else {
throw new IllegalStateException("Target service instance not found!");
}
} catch (Exception e) {
throw new IllegalArgumentException("Couldn't get service URL!", e);
}
return null;
}
}
#Cepr0's solution is right. Here I am proposing just a simpler way (without service discovery). Assuming you have that route:
zuul:
routes:
first:
path: /first/**
# No need for service id or url
Then you can route requests for '/first' route in 'route' type filter just by setting location to request context.
#Component
public class RoutingFilter extends ZuulFilter {
#Override
public String filterType() {
return ROUTE_TYPE;
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() throws ZuulException {
/* Routing logic goes here */
URL location = getRightLocationForRequest();
ctx.setRouteHost(location);
return null;
}
}
I have a simple use case, where I need to send 302 HTTP status with the Location header if the request comes to the gateway when the url contains /logout. This has to happen without routing to the back-end service.
Below is my zuul fillter:
public class LogoutFillter extends ZuulFilter{
#Override
public boolean shouldFilter() {
if(RequestContext.getCurrentContext().getRequest().getRequestURI().toLowerCase().contains("/logout")){
return true;
}else{
return false;
}
}
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 3;
}
#Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpSession excistingSession = context.getRequest().getSession(false);
if(excistingSession != null){
excistingSession.invalidate();
context.unset()
//context.addZuulResponseHeader("Location", "/abc/def/logout.do"); //notworking
context.setResponseStatusCode(302);
}
return null;
}
}
I have tried to do this like below:
HttpServletResponse response = context.getResponse();
response.setStatus(302)
response.setHeader("Location", "/abc/logout.to");
context.unset();
context.setResponse(response);
However, that didn't work either. Any suggestion would be appreciated.
Finally I managed to resolve the issue. Below is my code
#Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpSession excistingSession = context.getRequest().getSession(false);
if(excistingSession != null){
excistingSession.invalidate();
context.setSendZuulResponse(false);
context.addZuulResponseHeader("Location", "/abc/def/logout.do");
context.setResponseStatusCode(HttpServletResponse.SC_MOVED_TEMPORARILY);
}
return null;
}
In the above code setSendZuulResponse(false) will stop the routing to the back-end service, and the addZuulResponseHeader will add the response header.