I want to send a soap message with Spring Integration. I use Java Config.
I've tried the flolowing interceptor, but the spring integration converts the angle brackets (<>) into html escape characters.
import org.springframework.ws.client.WebServiceClientException;
import org.springframework.ws.client.support.interceptor.ClientInterceptor;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.soap.SoapHeader;
import org.springframework.ws.soap.SoapHeaderElement;
import org.springframework.ws.soap.SoapMessage;
public class MyAuthInterceptor implements ClientInterceptor {
#Override
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {
SoapMessage soapMessage = (SoapMessage) messageContext.getRequest();
SoapHeader sh = soapMessage.getSoapHeader();
QName name = new QName("http://...", "myAuth", "aut");
sh.addHeaderElement(name).setText("<username>TestUser</username>" + "<password>TestPass</password>");
return true;
}
Here is the generated soap header:
<SOAP-ENV:Header>
<aut:myAuth xmlns:aut="http://.../"><username>TestUser</username><password>TestPass</password></aut:myAuth>
</SOAP-ENV:Header>
Here is my configuration:
#Configuration
#EnableIntegration
public class SpringIntegrationConfiguration {
#Bean
public PublishSubscribeChannel inputChannel() {
return new PublishSubscribeChannel();
}
#Bean
public ClientInterceptor myAuthInterceptor() {
return new MyAuthInterceptor();
}
#Bean
#ServiceActivator(inputChannel = "inputChannel")
public SimpleWebServiceOutboundGateway myOutput(ClientInterceptor mekAuthInterceptor) {
SimpleWebServiceOutboundGateway simpleWebServiceOutboundGateway = new SimpleWebServiceOutboundGateway("http://...");
simpleWebServiceOutboundGateway.setInterceptors(myAuthInterceptor);
return simpleWebServiceOutboundGateway;
}
}
How can I set the soap header without escaping the angle brackets?
You have to build it up using addChildElements instead of setting it as text.
You are setting the tags as text which escapes the string since it is being added to an xml. Those needs to be set as elements
http://docs.oracle.com/javaee/5/api/javax/xml/soap/SOAPHeaderElement.html
Check the methods in the doc above and use it appropriately. Leave a comment in case you need more help.
#gary-russell - Your answer made sense from a logical point of view, but there are no "addChildElement" method in org.springframework.ws.soap.. I found them in javax.xml.soap..
So my result looks like the following:
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;
import javax.xml.soap.SOAPMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ws.client.WebServiceClientException;
import org.springframework.ws.client.support.interceptor.ClientInterceptor;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.soap.SoapMessage;
import org.springframework.ws.soap.saaj.SaajSoapMessage;
public class MySecurityInterceptor implements ClientInterceptor
{
private static final Logger log = LoggerFactory.getLogger(MySecurityInterceptor.class);
#Override
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException
{
SoapMessage soapMessage = (SoapMessage) messageContext.getRequest();
QName securityName = new QName(
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
"Security",
XMLConstants.DEFAULT_NS_PREFIX);
QName usernameToken = new QName(null, "UsernameToken", XMLConstants.DEFAULT_NS_PREFIX);
QName username = new QName(null, "Username", XMLConstants.DEFAULT_NS_PREFIX);
QName password = new QName(null, "Password", XMLConstants.DEFAULT_NS_PREFIX);
try
{
SOAPMessage mySoapMessage = ((SaajSoapMessage) soapMessage).getSaajMessage();
SOAPHeader header = mySoapMessage.getSOAPHeader();
SOAPHeaderElement securityElement = header.addHeaderElement(securityName);
SOAPElement usernameTokenElement = securityElement.addChildElement(usernameToken);
SOAPElement usernameElement = usernameTokenElement.addChildElement(username);
SOAPElement passwordElement = usernameTokenElement.addChildElement(password);
usernameElement.setTextContent("jxep-Zenmonics-1-i101#jxtest.local");
passwordElement.setTextContent("3Sg%T~1q4z!QnH6#+5pD");
}
catch (SOAPException e)
{
log.error("Error!", e);
}
return true;
}
#Override
public boolean handleResponse(MessageContext messageContext) throws WebServiceClientException
{
// Auto-generated method stub
return false;
}
#Override
public boolean handleFault(MessageContext messageContext) throws WebServiceClientException
{
// Auto-generated method stub
return false;
}
#Override
public void afterCompletion(MessageContext messageContext, Exception ex) throws WebServiceClientException
{
// Auto-generated method stub
}
}
Related
I have a self contained Jersey test using JerseyExtension (JerseyExtension) with JUnit5 (since JerseyTest does not work with JUnit5 unless you use the vintage engine) and subsequent calls to the container are getting different session. Is there a way to keep the session store same between the calls?
package com.test.jerseysession;
import com.github.hanleyt.JerseyExtension;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.glassfish.jersey.test.DeploymentContext;
import org.glassfish.jersey.test.ServletDeploymentContext;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.glassfish.jersey.test.spi.TestContainerFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.RegisterExtension;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class JerseyTestWithGrizzly {
private final static TestContainerFactory testContainerFactory;
private final ServletContainer servletContainer;
private final ResourceConfig resourceConfig;
private final DeploymentContext deploymentContext;
static {
testContainerFactory = new GrizzlyWebTestContainerFactory();
}
#RegisterExtension
#SuppressWarnings("unused")
JerseyExtension jerseyExtension = new JerseyExtension(
this::getTestContainerFactory,
this::configureDeploymentContext,
this::configureJerseyClient);
public JerseyTestWithGrizzly() {
this.resourceConfig = new ResourceConfig()
.packages("com.test.jerseysession")
.register(getClass());
this.servletContainer = new ServletContainer(resourceConfig);
this.deploymentContext = ServletDeploymentContext.builder(resourceConfig)
.servlet(servletContainer)
.servletPath("api")
.build();
}
#Path("session")
public static class SessionResource {
#GET
public String get(#Context HttpServletRequest request) {
HttpSession session = request.getSession(true);
Object obj = session.getAttribute("name");
return session.getId() + ": " + obj;
}
#PUT
public String put(#Context HttpServletRequest request) {
HttpSession session = request.getSession(true);
session.setAttribute("name", "foo");
return session.getId()+": Set name attribute called";
}
}
protected ClientConfig configureJerseyClient(ExtensionContext extensionContext, ClientConfig clientConfig) {
assertNotNull(extensionContext);
assertNotNull(clientConfig);
return clientConfig;
}
protected DeploymentContext configureDeploymentContext(ExtensionContext extensionContext) {
assertNotNull(extensionContext);
return deploymentContext;
}
protected TestContainerFactory getTestContainerFactory(ExtensionContext extensionContext) {
assertNotNull(extensionContext);
return testContainerFactory;
}
#Test
public void testSessionSet(WebTarget target) {
// Call PUT which sets attribute called 'name'
Response response0 = target.path("session").request().put(Entity.entity("{}", MediaType.APPLICATION_JSON_TYPE));
System.out.println("PUT: status="+response0.getStatus()+" response="+response0.readEntity(String.class));
// Call GET which should be able to find 'name' in session set by previous call
Response response1 = target.path("session").request().get();
System.out.println("GET: status="+response1.getStatus()+" response="+response1.readEntity(String.class));
}
}
Sample output:
PUT: status=200 response=8373522406385125383: Set name attribute called
GET: status=200 response=8264425692811867393: null
The session ID changed between the call to PUT and GET.
The client used by Jersey test framework, does not behave like a browser when it comes to Set-Cookie/Cookie headers. The two requests are not connected and JSESSIONID set by first response is not propagated to next request. While the framework is aware of the JSESSIONID if present, it does not span requests and needs to be manually copied forward.
Changing the test method to following works:
#Test
public void testSessionSet(WebTarget target) {
Response response0 = target.path("session").request().put(Entity.entity("{}", MediaType.APPLICATION_JSON_TYPE));
System.out.println("PUT: status="+response0.getStatus()+" response="+response0.readEntity(String.class));
Invocation.Builder nextRequestBuilder = target.path("session").request();
NewCookie jsessionid = response0.getCookies().get("JSESSIONID");
if (jsessionid != null) {
nextRequestBuilder.cookie(jsessionid);
}
Response response1 = nextRequestBuilder.get();
System.out.println("GET: status="+response1.getStatus()+" response="+response1.readEntity(String.class));
}
I need to make my Spring Boot application start/stop listening on a new port dynamically.
I understand a new tomcat connector needs to be injected into Spring context for this.
I'm able to add a connector using a ServletWebServerFactory bean and tomcatConnectorCustomizer. But this bean is loaded only during Spring Bootup.
#Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
TomcatConnectorCustomizer tomcatConnectorCustomizer = connector -> {
connector.setPort(serverPort);
connector.setScheme("https");
connector.setSecure(true);
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
protocol.setSSLEnabled(true);
protocol.setKeystoreType("PKCS12");
protocol.setKeystoreFile(keystorePath);
protocol.setKeystorePass(keystorePass);
protocol.setKeyAlias("spa");
protocol.setSSLVerifyClient(Boolean.toString(true));
tomcat.addConnectorCustomizers(tomcatConnectorCustomizer);
return tomcat;
}
}
Is there any way to add a tomcat connector during run time? Say on a method call?
Update: Connector created but all requests to it return 404:
I've managed to add a Tomcat connector at runtime. But the request made to that port are not going to my RestController.
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
TomcatConnectorCustomizer tomcatConnectorCustomizer = connector -> {
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
connector.setScheme("http");
connector.setSecure(false);
connector.setPort(8472);
protocol.setSSLEnabled(false);
};
tomcat.addConnectorCustomizers(tomcatConnectorCustomizer);
tomcat.getWebServer().start();
How should I proceed further?
Hi here is my sample project: sample project
1- Main Application (DemoApplication.java):
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
2 - Config file (AppConfig.java):
#Configuration
public class AppConfig {
#Autowired
private ServletWebServerApplicationContext server;
private static FilterConfig filterConfig = new FilterConfig();
#PostConstruct
void init() {
//setting default port config
filterConfig.addNewPortConfig(8080, "/admin");
}
#Bean
#Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public FilterConfig createFilterConfig() {
return filterConfig;
}
public void addPort(String schema, String domain, int port, boolean secure) {
TomcatWebServer ts = (TomcatWebServer) server.getWebServer();
synchronized (this) {
ts.getTomcat().setConnector(createConnector(schema, domain, port, secure));
}
}
public void addContextAllowed(FilterConfig filterConfig, int port, String context) {
filterConfig.addNewPortConfig(port, context);
}
public void removePort(int port) {
TomcatWebServer ts = (TomcatWebServer) server.getWebServer();
Service service = ts.getTomcat().getService();
synchronized (this) {
Connector[] findConnectors = service.findConnectors();
for (Connector connector : findConnectors) {
if (connector.getPort() == port) {
try {
connector.stop();
connector.destroy();
filterConfig.removePortConfig(port);
} catch (LifecycleException e) {
e.printStackTrace();
}
}
}
}
}
private Connector createConnector(String schema, String domain, int port, boolean secure) {
Connector conn = new Connector("org.apache.coyote.http11.Http11NioProtocol");
conn.setScheme(schema);
conn.setPort(port);
conn.setSecure(true);
conn.setDomain(domain);
if (secure) {
// config secure port...
}
return conn;
}
}
3 - Filter (NewPortFilter.java):
public class NewPortFilter {
#Bean(name = "restrictFilter")
public FilterRegistrationBean<Filter> retstrictFilter(FilterConfig filterConfig) {
Filter filter = new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// get allowed url contexts
Set<String> config = filterConfig.getConfig().get(request.getLocalPort());
if (config == null || config.isEmpty()) {
response.sendError(403);
}
boolean accepted = false;
for (String value : config) {
if (request.getPathInfo().startsWith(value)) {
accepted = true;
break;
}
}
if (accepted) {
filterChain.doFilter(request, response);
} else {
response.sendError(403);
}
}
};
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<Filter>();
filterRegistrationBean.setFilter(filter);
filterRegistrationBean.setOrder(-100);
filterRegistrationBean.setName("restrictFilter");
return filterRegistrationBean;
}
}
4 - Filter Config (FilterConfig.java):
public class FilterConfig {
private Map<Integer, Set<String>> acceptedContextsByPort = new ConcurrentHashMap<>();
public void addNewPortConfig(int port, String allowedContextUrl) {
if(port > 0 && allowedContextUrl != null) {
Set<String> set = acceptedContextsByPort.get(port);
if (set == null) {
set = new HashSet<>();
}
set = new HashSet<>(set);
set.add(allowedContextUrl);
acceptedContextsByPort.put(port, set);
}
}
public void removePortConfig(int port) {
if(port > 0) {
acceptedContextsByPort.remove(port);
}
}
public Map<Integer, Set<String>> getConfig(){
return acceptedContextsByPort;
}
}
5 - Controller (TestController.java):
#RestController
public class TestController {
#Autowired
AppConfig config;
#Autowired
FilterConfig filterConfig;
#GetMapping("/admin/hello")
String test() {
return "hello test";
}
#GetMapping("/alternative/hello")
String test2() {
return "hello test 2";
}
#GetMapping("/admin/addNewPort")
ResponseEntity<String> createNewPort(#RequestParam Integer port, #RequestParam String context) {
if (port == null || port < 1) {
return new ResponseEntity<>("Invalid Port" + port, HttpStatus.BAD_REQUEST);
}
config.addPort("http", "localhost", port, false);
if (context != null && context.length() > 0) {
config.addContextAllowed(filterConfig, port, context);
}
return new ResponseEntity<>("Added port:" + port, HttpStatus.OK);
}
#GetMapping("/admin/removePort")
ResponseEntity<String> removePort(#RequestParam Integer port) {
if (port == null || port < 1) {
return new ResponseEntity<>("Invalid Port" + port, HttpStatus.BAD_REQUEST);
}
config.removePort(port);
return new ResponseEntity<>("Removed port:" + port, HttpStatus.OK);
}
}
How to test it?
In a browser:
1 - try:
http://localhost:8080/admin/hello
Expected Response: hello test
2 - try:
http://localhost:8080/admin/addNewPort?port=9090&context=alternative
Expected Response: Added port:9090
3 - try:
http://localhost:9090/alternative/hello
Expected Response: hello test 2
4 - try expected errors:
http://localhost:9090/alternative/addNewPort?port=8181&context=alternative
Expected Response (context [alternative] allowed but endpoint not registered in controller for this context): Whitelabel Error Page...
http://localhost:9090/any/hello
Expected Response (context [any] not allowed): Whitelabel Error Page...
http://localhost:8888/any/hello
Expected Response (invalid port number): ERR_CONNECTION_REFUSED
http://localhost:8080/hello
Expected Response (no context allowed [/hello]): Whitelabel Error Page...
5 - try remove a port:
http://localhost:8080/admin/removePort?port=9090
6 - check removed port:
http://localhost:9090/alternative/hello
Expected Response (port closed): ERR_CONNECTION_REFUSED
I hope it helps.
You should create ServletWebServerFactory bean as a prototype bean using #Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE).
Now in bean where you need new tomcat connector to be injected into Spring context(MySingletonBean in example) autowire the application context and get ServletWebServerFactory bean(MyPrototypeBean in example) from getBean method. In this way you will always get new tomcat connector bean.
Following is a simple sample code:-
public class MySingletonBean {
#Autowired
private ApplicationContext applicationContext;
public void showMessage(){
MyPrototypeBean bean = applicationContext.getBean(MyPrototypeBean.class);
}
}
Based on the accepted answer of #ariel-carrera, I did it a little differently (IMO cleaner):
Define a normal Spring controller for handling the request on any dynamic port
import package.IgnoredBean;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import static org.apache.commons.lang3.reflect.MethodUtils.getMethodsWithAnnotation;
//must be declared in a separate java file so that it's not picked up by component scanning as inner class
#IgnoredBean
#RestController
#Slf4j
#RequiredArgsConstructor
class DynamicController {
static final Method HANDLER_METHOD = getMethodsWithAnnotation(DynamicController.class, RequestMapping.class)[0];
private final String myContext;
#RequestMapping
public Object handle(
#RequestBody Map<String, Object> body,
#RequestParam MultiValueMap<String, Object> requestParams,
#PathVariable Map<String, Object> pathVariables
) {
Map<String, Object> allAttributes = new HashMap<>(body.size() + requestParams.size() + pathVariables.size());
allAttributes.putAll(body);
allAttributes.putAll(pathVariables);
requestParams.forEach((name, values) -> allAttributes.put(name, values.size() > 1 ? values : values.get(0)));
log.info("Handling request for '{}': {}", myContext, allAttributes);
return allAttributes;
}
// this handler only affects this particular controller. Otherwise it will use any of your regular #ControllerAdvice beans or fall back to spring's default
#ExceptionHandler
public ResponseEntity<?> onError(Exception e) {
log.debug("something happened in '{}'", myContext, e);
return ResponseEntity.status(500).body(Map.of("message", e.getMessage()));
}
}
Must be declared in its own file
It is not a Spring Bean, we will instantiate it manually for each port and give it some context objects that are relevant to the port owner.
Note the #IgnoredBean, a custom annotation:
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
#Target(TYPE)
#Retention(RUNTIME)
public #interface IgnoredBean {
}
#SpringBootApplication
#ComponentScan(excludeFilters = #ComponentScan.Filter(IgnoredBean.class))
...
public class MyApplication{...}
The rest of it
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.coyote.http11.Http11NioProtocol;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toUnmodifiableSet;
#Service
#RequiredArgsConstructor
#Slf4j
class DynamicControllerService {
private final RequestMappingHandlerMapping requestHandlerMapper;
private final Map<Integer, RequestMappingInfo> mappingByPort = new ConcurrentHashMap<>();
private Tomcat tomcat;
#Autowired
void setTomcat(ServletWebServerApplicationContext context) {
tomcat = ((TomcatWebServer) context.getWebServer()).getTomcat();
}
public int addMapping(#Nullable Integer givenPort, RequestMethod method, String path, Object myContext) {
val connector = new Connector(new Http11NioProtocol());
connector.setThrowOnFailure(true);
//0 means it will pick any available port
connector.setPort(Optional.ofNullable(givenPort).orElse(0));
try {
tomcat.setConnector(connector);
} catch (IllegalArgumentException e) {
// if it fails to start the connector, the object will still be left inside here
tomcat.getService().removeConnector(connector);
val rootCause = ExceptionUtils.getRootCause(e);
throw new IllegalArgumentException(rootCause.getMessage(), rootCause);
}
int port = connector.getLocalPort();
val mapping = RequestMappingInfo
.paths(path)
.methods(method)
.customCondition(new PortRequestCondition(port))
.build();
requestHandlerMapper.registerMapping(
mapping,
new DynamicController("my context for port " + port),
DynamicController.HANDLER_METHOD
);
mappingByPort.put(port, mapping);
log.info("added mapping {} {} for port {}", method, path, port);
return port;
}
public void removeMapping(Integer port) {
Stream.of(tomcat.getService().findConnectors())
.filter(connector -> connector.getPort() == port)
.findFirst()
.ifPresent(connector -> {
try {
tomcat.getService().removeConnector(connector);
connector.destroy();
} catch (IllegalArgumentException | LifecycleException e) {
val rootCause = ExceptionUtils.getRootCause(e);
throw new IllegalArgumentException(rootCause.getMessage(), rootCause);
}
val mapping = mappingByPort.get(port);
requestHandlerMapper.unregisterMapping(mapping);
log.info("removed mapping {} {} for port {}",
mapping.getMethodsCondition().getMethods(),
Optional.ofNullable(mapping.getPathPatternsCondition())
.map(PathPatternsRequestCondition::getPatternValues)
.orElse(Set.of()),
port
);
});
}
#RequiredArgsConstructor
private static class PortRequestCondition implements RequestCondition<PortRequestCondition> {
private final Set<Integer> ports;
public PortRequestCondition(Integer... ports) {
this.ports = Set.of(ports);
}
#Override
public PortRequestCondition combine(PortRequestCondition other) {
return new PortRequestCondition(Stream.concat(ports.stream(), other.ports.stream()).collect(toUnmodifiableSet()));
}
#Override
public PortRequestCondition getMatchingCondition(HttpServletRequest request) {
return ports.contains(request.getLocalPort()) ? this : null;
}
#Override
public int compareTo(PortRequestCondition other, HttpServletRequest request) {
return 0;
}
}
}
I'm trying to work out a simple web app with angular as the front end and Spring Boot as the back end. My pre-flight request succeeds and I receive a valid token, however when trying to make a subsequent request to a route I get a 404. When I try to curl the url I get a 500 with the message "Missing or invalid authorization header" which seems to me to be saying that the route does indeed exist and is listening, but something else is wrong.
First of all the Typescript. Here is my login.component.ts
import { Component } from "#angular/core";
import { Observable } from "rxjs/index";
import { LoginService } from "../services/login.service";
#Component({
selector: "login",
templateUrl: "./login.component.html"
})
export class Login {
private model = {"username": "", "password": ""};
private currentUserName;
constructor (private loginService: LoginService) {
this.currentUserName = localStorage.getItem("currentUserName");
}
public onSubmit() {
console.log("submitting");
this.loginService.sendCredential(this.model).subscribe(
data => {
console.log(data);
localStorage.setItem("token", data);
console.log("Setting token");
this.loginService.sendToken(localStorage.getItem("token")).subscribe(
data => {
this.currentUserName = this.model.username;
localStorage.setItem("currentUserName", this.model.username);
this.model.username = "";
this.model.password = "";
},
error => console.log(error)
);
},
error => {
console.log("oh no");
console.log(error)
}
);
}
}
And then my LoginService
import { Injectable } from "#angular/core";
import { HttpClient } from '#angular/common/http';
import { HttpHeaders } from '#angular/common/http';
import { Observable } from "rxjs/index";
#Injectable()
export class LoginService {
token: string;
constructor (private http: HttpClient) {}
public sendCredential(model): Observable<String> {
const tokenUrlPreFlight = "http://localhost:8080/users/login/";
const httpOptions: {} = {
headers: new HttpHeaders({
'ContentType': 'application/json'
})
};
return this.http.post<String>(tokenUrlPreFlight, model, httpOptions);
}
public sendToken(token) {
const tokenUrlPostFlight = "http://localhost:8080/rest/users/";
console.log("Bearer " + token);
const httpOptions: {} = {
headers: new HttpHeaders({
'Authorization': 'Bearer ' + token
})
};
return this.http.get(tokenUrlPostFlight, httpOptions);
}
public logout(): void {
localStorage.setItem("token", "");
localStorage.setItem("currentUserName", "");
alert("You just logged out");
}
public checkLogin(): boolean {
if(localStorage.getItem("currentUserName") != null && localStorage.getItem("currentUserName") != "" && localStorage.getItem("token") != null && localStorage.getItem("token") != "") {
console.log(localStorage.getItem("currentUserName"));
console.log(localStorage.getItem("token"));
return true;
}
return false;
}
}
And now for the java. First here is my entry point:
import com.acb.app.configuration.JwtFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
#SpringBootApplication
public class App {
#Bean
public FilterRegistrationBean jwtFilter() {
final FilterRegistrationBean<JwtFilter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new JwtFilter());
filterRegistrationBean.addUrlPatterns("/rest/*");
return filterRegistrationBean;
}
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
My UserController.java
import com.acb.app.model.User;
import io.jsonwebtoken.*;
import com.acb.maki.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.ServletException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
#RestController
#RequestMapping("/users")
public class UserController {
#Autowired
private UserService userService;
#GetMapping("")
public List<User> userIndex() {
return userService.getAllUsers();
}
#GetMapping("{username}")
public Optional<User> getUserByUsername(#RequestBody String username) {
return userService.findByUsername(username);
}
#PostMapping("login")
public String login(#RequestBody Map<String, String> json) throws ServletException {
if(json.get("username") == null || json.get("password") == null) {
throw new ServletException("Please fill in username and password");
}
final String username = json.get("username");
final String password = json.get("password");
Optional<User> optionalUser = userService.findByUsername(username);
if(!optionalUser.isPresent()) {
throw new ServletException("Username not found");
}
User user = optionalUser.get();
if(!password.equals(user.getPassword())) {
throw new ServletException("Invalid login. Please check username and password");
}
final String response = Jwts.builder().setSubject(username).claim("roles", "user").setIssuedAt(new Date()).signWith(SignatureAlgorithm.HS256, "secretKey").compact();
final String jsonResponse = String.format("\"%s\"", response);
System.out.println(jsonResponse);
return jsonResponse;
}
#PostMapping(value="/register")
public User register(#RequestBody User user) {
return userService.save(user);
}
}
And last but not least my JwtFilter.java
import io.jsonwebtoken.*;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
final String authHeader = request.getHeader("Authorization");
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
filterChain.doFilter(servletRequest, servletResponse);
} else {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new ServletException("Missing or invalid authorization header");
}
final String token = authHeader.substring(7);
try {
final JwtParser jwtParser = Jwts.parser();
final Jws<Claims> claimsJws = jwtParser.setSigningKey("secretKey").parseClaimsJws(token);
final Claims claims = claimsJws.getBody();
request.setAttribute("claims", claims);
} catch (final SignatureException e) {
throw new ServletException("Invalid token.");
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
I am very aware of the bad practices here with comparing plain text passwords, etc. I'm just trying to get this to work for the time being. The token is correctly generated and returned. The token is also set correctly to local storage, but upon making the get request to the /rest/users route a 404 is returned. There are no errors on the java side.
So as I expected, I am indeed an idiot. As the user mentioned above my service maps to /users/ and what I needed is the protected version /rest/users. So next to my UserController I created UserResource.java which looks like so:
import com.acb.app.model.User;
import com.acb.app.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
#RestController
#RequestMapping("/rest")
public class UserResource {
#Autowired
private UserService userService;
#RequestMapping("/users")
public List<User> findAllUsers() {
return userService.getAllUsers();
}
}
And this makes use of the JwtFilter and allows me to protect my routes. Hopefully this will be helpful to someone else.
I am new to Camel and I am facing an issue while sending files to webservice via camel http.
I have a rest web service which consumes Multipart form data type content and accepts input as part of form data.
When I send file and form parameter via camel it gives me the following error at camel console:
Stacktrace
---------------------------------------------------------------------------------------------------------------------------------------
org.apache.camel.component.http.HttpOperationFailedException: HTTP operation failed invoking http://localhost:8080/JAX_RS_Application/resource/restwb/upload with statusCode: 415
at org.apache.camel.component.http.HttpProducer.populateHttpOperationFailedException(HttpProducer.java:230)
at org.apache.camel.component.http.HttpProducer.process(HttpProducer.java:156)
at org.apache.camel.util.AsyncProcessorConverterHelper$ProcessorToAsyncProcessorBridge.process(AsyncProcessorConverterHelper.java:61)
at org.apache.camel.processor.SendProcessor.process(SendProcessor.java:129)
at org.apache.camel.management.InstrumentationProcessor.process(InstrumentationProcessor.java:77)
at org.apache.camel.processor.RedeliveryErrorHandler.process(RedeliveryErrorHandler.java:448)
at org.apache.camel.processor.CamelInternalProcessor.process(CamelInternalProcessor.java:191)
at org.apache.camel.processor.Pipeline.process(Pipeline.java:118)
at org.apache.camel.processor.Pipeline.process(Pipeline.java:80)
at org.apache.camel.processor.CamelInternalProcessor.process(CamelInternalProcessor.java:191)
at org.apache.camel.component.file.GenericFileConsumer.processExchange(GenericFileConsumer.java:435)
at org.apache.camel.component.file.GenericFileConsumer.processBatch(GenericFileConsumer.java:211)
at org.apache.camel.component.file.GenericFileConsumer.poll(GenericFileConsumer.java:175)
at org.apache.camel.impl.ScheduledPollConsumer.doRun(ScheduledPollConsumer.java:174)
at org.apache.camel.impl.ScheduledPollConsumer.run(ScheduledPollConsumer.java:101)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:304)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:178)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:745)
The error i get on the server side console is as follows:
SEVERE: MessageBodyReader not found for media type=application/octet-stream, typ
e=class org.glassfish.jersey.media.multipart.FormDataMultiPart, genericType=clas
s org.glassfish.jersey.media.multipart.FormDataMultiPart.
The code snippet of the Rest web-service created via jersey is as follows:
import java.io.IOException;
import java.io.InputStream;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import org.apache.commons.io.IOUtils;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
#Path("/restwb")
public class FileResource {
#POST
#Path("/upload")
#Consumes(MediaType.MULTIPART_FORM_DATA)
public String uploadFile(#FormDataParam("username") String username,#FormDataParam("password") String password,#FormDataParam("upload") InputStream is) {
String output ="Hi "+username+" your password is "+password;
output=output+IOUtils.LINE_SEPARATOR +IOUtils.LINE_SEPARATOR;
output=output+"Output :"+IOUtils.LINE_SEPARATOR+"------------------------------------------------------------------------------"+IOUtils.LINE_SEPARATOR;
try {
output=output+IOUtils.toString(is)+IOUtils.LINE_SEPARATOR+IOUtils.LINE_SEPARATOR;
output=output+"==================================================================================================="+IOUtils.LINE_SEPARATOR+IOUtils.LINE_SEPARATOR;
System.out.println("Output :"+output);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return output;
}
}
And my Camel config is as follows:
import org.apache.camel.*;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.spi.Synchronization;
import org.apache.camel.spi.UnitOfWork;
import org.apache.http.HttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.james.mime4j.message.Multipart;
import org.apache.log4j.Logger;
import java.io.File;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
/**
* Created by Manish.Pillai on 7/16/2015.
*/
public class LoggingMain {
private static final Logger logger =Logger.getLogger(LoggingMain.class);
public static void main(String[] args) throws Exception{
CamelContext camelContext =new DefaultCamelContext();
try {
camelContext.addRoutes(new RouteBuilder() {
#Override
public void configure() throws Exception {
from("file:C:\\temp?delay=5000&move=processed&moveFailed=error&antExclude=**/processed/**,**/error/**")
.process(new Processor() {
public void process(Exchange exchange) throws Exception {
exchange.getContext().getTypeConverterRegistry().addTypeConverter(HttpEntity.class,InputStream.class,new InputStreamToHttpEntityConvertor());
exchange.getOut().setBody(exchange.getIn().getBody(),HttpEntity.class);
}
})
.to("http://localhost:8080/JAX_RS_Application/resource/restwb/upload");
}
});
camelContext.getRestConfiguration();
camelContext.start();
Thread.sleep(5000);
camelContext.stop();
} catch (Exception e) {
logger.error(e.getMessage());
}
}
static class InputStreamToHttpEntityConvertor implements TypeConverter {
public boolean allowNull() {
return false;
}
public <T> T convertTo(Class<T> type, Object value) throws TypeConversionException {
Exchange exchange=(Exchange)value;
StringBody username = new StringBody("username", ContentType.MULTIPART_FORM_DATA);
StringBody password = new StringBody("password", ContentType.MULTIPART_FORM_DATA);
MultipartEntityBuilder multipartEntityBuilder=MultipartEntityBuilder.create();
multipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
multipartEntityBuilder.addPart("upload", new FileBody(exchange.getIn().getBody(File.class), ContentType.MULTIPART_FORM_DATA, (String) exchange.getIn().getHeader(Exchange.FILE_NAME)));
multipartEntityBuilder.addPart("username",username);
multipartEntityBuilder.addPart("password",password);
return (T)multipartEntityBuilder.build();
}
public <T> T convertTo(Class<T> aClass, Exchange exchange, Object o) throws TypeConversionException {
return convertTo(aClass,o);
}
public <T> T mandatoryConvertTo(Class<T> type, Object value) throws TypeConversionException, NoTypeConversionAvailableException {
return convertTo(type,value);
}
public <T> T mandatoryConvertTo(Class<T> type, Exchange exchange, Object value) throws TypeConversionException, NoTypeConversionAvailableException {
return convertTo(type,value);
}
public <T> T tryConvertTo(Class<T> type, Object value) {
return convertTo(type,value);
}
public <T> T tryConvertTo(Class<T> type, Exchange exchange, Object value) {
return convertTo(type,value);
}
}
}
Any leads would be helpful.
Well, there are several things that can be improved in your code.
First, since you are using a MultipartEntityBuilder, that means you're using Apache's HttpClient version 4.3+, so for best compatibility you should use Camel's HTTP4 component.
Third, in an example as small as this, you don't really need to use the converter, you can do something like this:
public class LoggingMain {
private static final Logger logger = Logger.getLogger(LoggingMain.class);
public static void main(String[] args) throws Exception {
CamelContext camelContext = new DefaultCamelContext();
try {
camelContext.addRoutes(new RouteBuilder() {
#Override
public void configure() throws Exception {
from("file:C:\\temp?delay=5000&move=processed&moveFailed=error&antExclude=**/processed/**,**/error/**")
.process(new Processor() {
public void process(Exchange exchange) throws Exception {
StringBody username = new StringBody("username", ContentType.MULTIPART_FORM_DATA);
StringBody password = new StringBody("password", ContentType.MULTIPART_FORM_DATA);
MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
multipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
multipartEntityBuilder.addPart("username", username);
multipartEntityBuilder.addPart("password", password);
String filename = (String) exchange.getIn().getHeader(Exchange.FILE_NAME);
File file = exchange.getIn().getBody(File.class);
multipartEntityBuilder.addPart("upload", new FileBody(file, ContentType.MULTIPART_FORM_DATA, filename));
exchange.getIn().setBody(multipartEntityBuilder.build());
}
})
.to("http4://localhost:8080/JAX_RS_Application/resource/restwb/upload");
}
});
camelContext.getRestConfiguration();
camelContext.start();
Thread.sleep(5000);
camelContext.stop();
} catch (Exception e) {
logger.error(e.getMessage());
}
}
}
I hope this helps!
We were testing a REST webservice developed in jersey through postman rest client. It is a POST method and is annotated with #RolesAllowed. The full annotation the method is as follows:
#POST
#Path("/configuration")
#RolesAllowed("admin")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
When I requested this http://baseurl/configuration with the expected HTTP body content, I got 403 response(it is expected since it is allowed only for admin as it seems).
My doubt is how to access this service with the specified role via rest client.
So it seems like you set up the RolesAllowedDynamicFeature, but you have no authentication happening to set up the user and roles. What the RolesAllowedDynamicFeature does is lookup the SecurityContext, and calls the SecurityContext.isUserInRole(<"admin">) to see if the user in the SecurityContext has the role.
I imagine you don't know how the SecurityContext is set. There are a couple of ways. The first is through the servlet authentication mechanism. You can see more at Securing Web Applications from the Java EE tutorial.
Basically you need to set up a security realm or security domain on the server. Every server has it's own specific way of setting it up. You can see an example here or how it would be done with Tomcat.
Basically the realm/domain contains the users allowed to access the web app. Those users have associated roles. When the servlet container does the authentication, whether it be Basic authentication or Form authentication, it looks up the user from the credentials, and if the user is authenticated, the user and its roles are associated with the request. Jersey gathers this information and puts it into the SecurityContext for the request.
If this seems a bit complicated, an easier way to just forget the servlet container authentication and just create a Jersey filter, where you set the SecurityContext yourself. You can see an example here. You can use whatever authentication scheme you want. The important part is setting the SecurityContext with the user information, wherever you get it from, maybe a service that accesses a data store.
See Also:
securing rest services in Jersey
UPDATE
Here is a complete example of the second option using the filter. The test is run by Jersey Test Framework. You can run the test as is
import java.io.IOException;
import java.nio.charset.Charset;
import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Priority;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Priorities;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.DatatypeConverter;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.internal.util.Base64;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.glassfish.jersey.test.JerseyTest;
import static junit.framework.Assert.*;
import org.junit.Test;
public class BasicAuthenticationTest extends JerseyTest {
#Provider
#Priority(Priorities.AUTHENTICATION)
public static class BasicAuthFilter implements ContainerRequestFilter {
private static final Logger LOGGER = Logger.getLogger(BasicAuthFilter.class.getName());
#Inject
private UserStore userStore;
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String authentication = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authentication == null) {
throw new AuthenticationException("Authentication credentials are required");
}
if (!authentication.startsWith("Basic ")) {
return;
}
authentication = authentication.substring("Basic ".length());
String[] values = new String(DatatypeConverter.parseBase64Binary(authentication),
Charset.forName("ASCII")).split(":");
if (values.length < 2) {
throw new WebApplicationException(400);
}
String username = values[0];
String password = values[1];
LOGGER.log(Level.INFO, "{0} - {1}", new Object[]{username, password});
User user = userStore.getUser(username);
if (user == null) {
throw new AuthenticationException("Authentication credentials are required");
}
if (!user.password.equals(password)) {
throw new AuthenticationException("Authentication credentials are required");
}
requestContext.setSecurityContext(new MySecurityContext(user));
}
}
static class MySecurityContext implements SecurityContext {
private final User user;
public MySecurityContext(User user) {
this.user = user;
}
#Override
public Principal getUserPrincipal() {
return new Principal() {
#Override
public String getName() {
return user.username;
}
};
}
#Override
public boolean isUserInRole(String role) {
return role.equals(user.role);
}
#Override
public boolean isSecure() { return true; }
#Override
public String getAuthenticationScheme() {
return "Basic";
}
}
static class AuthenticationException extends WebApplicationException {
public AuthenticationException(String message) {
super(Response
.status(Status.UNAUTHORIZED)
.header("WWW-Authenticate", "Basic realm=\"" + "Dummy Realm" + "\"")
.type("text/plain")
.entity(message)
.build());
}
}
class User {
public final String username;
public final String role;
public final String password;
public User(String username, String password, String role) {
this.username = username;
this.password = password;
this.role = role;
}
}
class UserStore {
public final Map<String, User> users = new ConcurrentHashMap<>();
public UserStore() {
users.put("peeskillet", new User("peeskillet", "secret", "USER"));
users.put("stackoverflow", new User("stackoverflow", "superSecret", "ADMIN"));
}
public User getUser(String username) {
return users.get(username);
}
}
private static final String USER_RESPONSE = "Secured User Stuff";
private static final String ADMIN_RESPONSE = "Secured Admin Stuff";
private static final String USER_ADMIN_STUFF = "Secured User Admin Stuff";
#Path("secured")
public static class SecuredResource {
#GET
#Path("userSecured")
#RolesAllowed("USER")
public String getUser() {
return USER_RESPONSE;
}
#GET
#Path("adminSecured")
#RolesAllowed("ADMIN")
public String getAdmin() {
return ADMIN_RESPONSE;
}
#GET
#Path("userAdminSecured")
#RolesAllowed({"USER", "ADMIN"})
public String getUserAdmin() {
return USER_ADMIN_STUFF;
}
}
#Override
public ResourceConfig configure() {
return new ResourceConfig(SecuredResource.class)
.register(BasicAuthFilter.class)
.register(RolesAllowedDynamicFeature.class)
.register(new AbstractBinder(){
#Override
protected void configure() {
bind(new UserStore()).to(UserStore.class);
}
});
}
static String getBasicAuthHeader(String username, String password) {
return "Basic " + Base64.encodeAsString(username + ":" + password);
}
#Test
public void should_return_403_with_unauthorized_user() {
Response response = target("secured/userSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("stackoverflow", "superSecret"))
.get();
assertEquals(403, response.getStatus());
}
#Test
public void should_return_200_response_with_authorized_user() {
Response response = target("secured/userSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("peeskillet", "secret"))
.get();
assertEquals(200, response.getStatus());
assertEquals(USER_RESPONSE, response.readEntity(String.class));
}
#Test
public void should_return_403_with_unauthorized_admin() {
Response response = target("secured/adminSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("peeskillet", "secret"))
.get();
assertEquals(403, response.getStatus());
}
#Test
public void should_return_200_response_with_authorized_admin() {
Response response = target("secured/adminSecured")
.request()
.header(HttpHeaders.AUTHORIZATION,
getBasicAuthHeader("stackoverflow", "superSecret"))
.get();
assertEquals(200, response.getStatus());
assertEquals(ADMIN_RESPONSE, response.readEntity(String.class));
}
}
Here is the only dependency needed to run the test
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>${jersey2.version}</version>
<scope>test</scope>
</dependency>