I have 2 spring web apps that provide 2 separate set of services. Web App 1 has Spring Security implemented using a user-based authentication.
Now, Web App 2 needs
I was in the same situation. Here there is my solution.
Server - spring security config
<sec:http>
<sec:intercept-url pattern="/**" access="ROLE_USER" method="POST"/>
<sec:intercept-url pattern="/**" filters="none" method="GET"/>
<sec:http-basic />
</sec:http>
<sec:authentication-manager alias="authenticationManager">
<sec:authentication-provider>
<sec:user-service>
<sec:user name="${rest.username}" password="${rest.password}" authorities="ROLE_USER"/>
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
Client side RestTemplate config
<bean id="httpClient" class="org.apache.commons.httpclient.HttpClient">
<constructor-arg ref="httpClientParams"/>
<property name="state" ref="httpState"/>
</bean>
<bean id="httpState" class="CustomHttpState">
<property name="credentials" ref="credentials"/>
</bean>
<bean id="credentials" class="org.apache.commons.httpclient.UsernamePasswordCredentials">
<constructor-arg value="${rest.username}"/>
<constructor-arg value="${rest.password}"/>
</bean>
<bean id="httpClientFactory" class="org.springframework.http.client.CommonsClientHttpRequestFactory">
<constructor-arg ref="httpClient"/>
</bean>
<bean class="org.springframework.web.client.RestTemplate">
<constructor-arg ref="httpClientFactory"/>
</bean>
Custom HttpState implementation
/**
* Custom implementation of {@link HttpState} with credentials property.
*
* @author banterCZ
*/
public class CustomHttpState extends HttpState {
/**
* Set credentials property.
*
* @param credentials
* @see #setCredentials(org.apache.commons.httpclient.auth.AuthScope, org.apache.commons.httpclient.Credentials)
*/
public void setCredentials(final Credentials credentials) {
super.setCredentials(AuthScope.ANY, credentials);
}
}
Maven dependency
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
Here is a solution that works very well with Spring 3.1 and Apache HttpComponents 4.1 I created based various answers on this site and reading the spring RestTempalte source code. I am sharing in hopes of saving others time, I think spring should just have some code like this built in but it does not.
RestClient client = new RestClient();
client.setApplicationPath("someApp");
String url = client.login("theuser", "123456");
UserPortfolio portfolio = client.template().getForObject(client.apiUrl("portfolio"),
UserPortfolio.class);
Below is the Factory class which setups up the HttpComponents context to be the same on every request with the RestTemplate.
public class StatefullHttpComponentsClientHttpRequestFactory extends
HttpComponentsClientHttpRequestFactory
{
private final HttpContext httpContext;
public StatefullHttpComponentsClientHttpRequestFactory(HttpClient httpClient, HttpContext httpContext)
{
super(httpClient);
this.httpContext = httpContext;
}
@Override
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri)
{
return this.httpContext;
}
}
Below is Statefull Rest template that you can use to remember cookies, once you log in with it will remember the JSESSIONID and sent it on subsequent requests.
public class StatefullRestTemplate extends RestTemplate
{
private final HttpClient httpClient;
private final CookieStore cookieStore;
private final HttpContext httpContext;
private final StatefullHttpComponentsClientHttpRequestFactory statefullHttpComponentsClientHttpRequestFactory;
public StatefullRestTemplate()
{
super();
httpClient = new DefaultHttpClient();
cookieStore = new BasicCookieStore();
httpContext = new BasicHttpContext();
httpContext.setAttribute(ClientContext.COOKIE_STORE, getCookieStore());
statefullHttpComponentsClientHttpRequestFactory = new StatefullHttpComponentsClientHttpRequestFactory(httpClient, httpContext);
super.setRequestFactory(statefullHttpComponentsClientHttpRequestFactory);
}
public HttpClient getHttpClient()
{
return httpClient;
}
public CookieStore getCookieStore()
{
return cookieStore;
}
public HttpContext getHttpContext()
{
return httpContext;
}
public StatefullHttpComponentsClientHttpRequestFactory getStatefulHttpClientRequestFactory()
{
return statefullHttpComponentsClientHttpRequestFactory;
}
}
Here is a class to represent a rest client so that you can call into an app secured with spring security.
public class RestClient
{
private String host = "localhost";
private String port = "8080";
private String applicationPath;
private String apiPath = "api";
private String loginPath = "j_spring_security_check";
private String logoutPath = "logout";
private final String usernameInputFieldName = "j_username";
private final String passwordInputFieldName = "j_password";
private final StatefullRestTemplate template = new StatefullRestTemplate();
/**
* This method logs into a service by doing an standard http using the configuration in this class.
*
* @param username
* the username to log into the application with
* @param password
* the password to log into the application with
*
* @return the url that the login redirects to
*/
public String login(String username, String password)
{
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.add(usernameInputFieldName, username);
form.add(passwordInputFieldName, password);
URI location = this.template.postForLocation(loginUrl(), form);
return location.toString();
}
/**
* Logout by doing an http get on the logout url
*
* @return result of the get as ResponseEntity
*/
public ResponseEntity<String> logout()
{
return this.template.getForEntity(logoutUrl(), String.class);
}
public String applicationUrl(String relativePath)
{
return applicationUrl() + "/" + checkNotNull(relativePath);
}
public String apiUrl(String relativePath)
{
return applicationUrl(apiPath + "/" + checkNotNull(relativePath));
}
public StatefullRestTemplate template()
{
return template;
}
public String serverUrl()
{
return "http://" + host + ":" + port;
}
public String applicationUrl()
{
return serverUrl() + "/" + nullToEmpty(applicationPath);
}
public String loginUrl()
{
return applicationUrl(loginPath);
}
public String logoutUrl()
{
return applicationUrl(logoutPath);
}
public String apiUrl()
{
return applicationUrl(apiPath);
}
public void setLogoutPath(String logoutPath)
{
this.logoutPath = logoutPath;
}
public String getHost()
{
return host;
}
public void setHost(String host)
{
this.host = host;
}
public String getPort()
{
return port;
}
public void setPort(String port)
{
this.port = port;
}
public String getApplicationPath()
{
return applicationPath;
}
public void setApplicationPath(String contextPath)
{
this.applicationPath = contextPath;
}
public String getApiPath()
{
return apiPath;
}
public void setApiPath(String apiPath)
{
this.apiPath = apiPath;
}
public String getLoginPath()
{
return loginPath;
}
public void setLoginPath(String loginPath)
{
this.loginPath = loginPath;
}
public String getLogoutPath()
{
return logoutPath;
}
@Override
public String toString()
{
StringBuilder builder = new StringBuilder();
builder.append("RestClient [\n serverUrl()=");
builder.append(serverUrl());
builder.append(", \n applicationUrl()=");
builder.append(applicationUrl());
builder.append(", \n loginUrl()=");
builder.append(loginUrl());
builder.append(", \n logoutUrl()=");
builder.append(logoutUrl());
builder.append(", \n apiUrl()=");
builder.append(apiUrl());
builder.append("\n]");
return builder.toString();
}
}
The RestTemplate is very basic and limited; there doesn't seem to be an easy way to do this. The best way is probably to implement digest of basic auth in Web App 1. Then use Apache HttpClient directly to access the rest services from Web App 2.
That being said, for testing I was able to work around this with a big hack. Basically you use the RestTemplate to submit the login (j_spring_security_check), parse out the jsessionid from the request headers, then submit the rest request. Here's the code, but I doubt it's the best solution for production ready code.
public final class RESTTest {
public static void main(String[] args) {
RestTemplate rest = new RestTemplate();
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslsession) {
return true;
}
});
// setting up a trust store with JCA is a whole other issue
// this assumes you can only log in via SSL
// you could turn that off, but not on a production site!
System.setProperty("javax.net.ssl.trustStore", "/path/to/cacerts");
System.setProperty("javax.net.ssl.trustStorePassword", "somepassword");
String jsessionid = rest.execute("https://localhost:8443/j_spring_security_check", HttpMethod.POST,
new RequestCallback() {
@Override
public void doWithRequest(ClientHttpRequest request) throws IOException {
request.getBody().write("j_username=user&j_password=user".getBytes());
}
}, new ResponseExtractor<String>() {
@Override
public String extractData(ClientHttpResponse response) throws IOException {
List<String> cookies = response.getHeaders().get("Cookie");
// assuming only one cookie with jsessionid as the only value
if (cookies == null) {
cookies = response.getHeaders().get("Set-Cookie");
}
String cookie = cookies.get(cookies.size() - 1);
int start = cookie.indexOf('=');
int end = cookie.indexOf(';');
return cookie.substring(start + 1, end);
}
});
rest.put("http://localhost:8080/rest/program.json;jsessionid=" + jsessionid, new DAO("REST Test").asJSON());
}
}
Note for this to work, you need to create a trust store in JCA so the SSL connection can actually be made. I assume you don't want to have Spring Security's login be over plain HTTP for a production site since that would be a massive security hole.
The following will authenticate and return the session cookie:
String sessionCookie= restTemplate.execute(uri, HttpMethod.POST, request -> {
request.getBody().write(("j_username=USER_NAME&j_password=PASSWORD").getBytes());
}, response -> {
AbstractClientHttpResponse r = (AbstractClientHttpResponse) response;
HttpHeaders headers = r.getHeaders();
return headers.get("Set-Cookie").get(0);
});
There's a simple way to do this in case you are someone who's looking for a simple call and not a API consumer.
HttpClient client = new HttpClient();
client.getParams().setAuthenticationPreemptive(true);
Credentials defaultcreds = new UsernamePasswordCredentials("username", "password");
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new CommonsClientHttpRequestFactory(client));
client.getState().setCredentials(AuthScope.ANY, defaultcreds);
This is very similar to ams's approach, except I've completely encapsulated the concern of maintaining the session cookie in the StatefulClientHttpRequestFactory. Also by decorating an existing ClientHttpRequestFactory with this behaviour, it can be used with any underlying ClientHttpRequestFactory and isn't bound to a specific implementation.
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import static java.lang.String.format;
/**
* Decorates a ClientHttpRequestFactory to maintain sessions (cookies)
* to web servers.
*/
public class StatefulClientHttpRequestFactory implements ClientHttpRequestFactory {
protected final Log logger = LogFactory.getLog(this.getClass());
private final ClientHttpRequestFactory requestFactory;
private final Map<String, String> hostToCookie = new HashMap<>();
public StatefulClientHttpRequestFactory(ClientHttpRequestFactory requestFactory){
this.requestFactory = requestFactory;
}
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod);
final String host = request.getURI().getHost();
String cookie = getCookie(host);
if(cookie != null){
logger.debug(format("Setting request Cookie header to [%s]", cookie));
request.getHeaders().set("Cookie", cookie);
}
//decorate the request with a callback to process 'Set-Cookie' when executed
return new CallbackClientHttpRequest(request, response -> {
List<String> responseCookie = response.getHeaders().get("Set-Cookie");
if(responseCookie != null){
setCookie(host, responseCookie.stream().collect(Collectors.joining("; ")));
}
return response;
});
}
private synchronized String getCookie(String host){
String cookie = hostToCookie.get(host);
return cookie;
}
private synchronized void setCookie(String host, String cookie){
hostToCookie.put(host, cookie);
}
private static class CallbackClientHttpRequest implements ClientHttpRequest{
private final ClientHttpRequest request;
private final Function<ClientHttpResponse, ClientHttpResponse> filter;
public CallbackClientHttpRequest(ClientHttpRequest request, Function<ClientHttpResponse, ClientHttpResponse> filter){
this.request = request;
this.filter = filter;
}
@Override
public ClientHttpResponse execute() throws IOException {
ClientHttpResponse response = request.execute();
return filter.apply(response);
}
@Override
public OutputStream getBody() throws IOException {
return request.getBody();
}
@Override
public HttpMethod getMethod() {
return request.getMethod();
}
@Override
public URI getURI() {
return request.getURI();
}
@Override
public HttpHeaders getHeaders() {
return request.getHeaders();
}
}
}