SSL enabled RESTful services are quite easier to develop and test using Jersey, Grizzly and RestTemplate.
Jersey (resource development)
Grizzly Web Server (resource configuration and deployment)
Spring 3 RestTemplate backed by Commons HTTP Client (resource access)
In a moment, you will notice how all these nicely fit the bill. Let us start with the POM for the maven fans.
<
dependencies
>
<
dependency
>
<
groupId
>org.springframework</
groupId
>
<
artifactId
>org.springframework.web</
artifactId
>
<
version
>3.0.0.M4</
version
>
</
dependency
>
<
dependency
>
<
groupId
>commons-httpclient</
groupId
>
<
artifactId
>commons-httpclient</
artifactId
>
<
version
>3.1</
version
>
</
dependency
>
<
dependency
>
<
groupId
>com.sun.jersey</
groupId
>
<
artifactId
>jersey-server</
artifactId
>
<
version
>1.1.2-ea</
version
>
</
dependency
>
<
dependency
>
<
groupId
>com.sun.grizzly</
groupId
>
<
artifactId
>grizzly-servlet-webserver</
artifactId
>
<
version
>1.9.18a</
version
>
</
dependency
>
<
dependency
>
<
groupId
>org.apache.log4j</
groupId
>
<
artifactId
>com.springsource.org.apache.log4j</
artifactId
>
<
version
>1.2.15</
version
>
</
dependency
>
</
dependencies
>
|
Configuring Log4j is very useful as you could see the commons Client debug messages and http wire headers which are quite useful for debugging in case if you were lost in translation.
Putting together all these pieces working did not take much of my time. I did not have to do anything fancy here as I just reused most of the sample code from the Jersey HTTPS sample and Commons HTTP Client SSL sample.
Lets dive into the Spring Config, which does most of the wiring of HTTP Client and RestTemplate.
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
?>
xsi:schemaLocation
=
"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
>
<
bean
id
=
"sslClient"
class
=
"spring3.restclient.RestSSLClient"
>
<
constructor-arg
ref
=
"restTemplate"
/>
<
constructor-arg
ref
=
"credentials"
/>
</
bean
>
<
bean
id
=
"httpClientParams"
class
=
"org.apache.commons.httpclient.params.HttpClientParams"
>
<
property
name
=
"authenticationPreemptive"
value
=
"true"
/>
<
property
name
=
"connectionManagerClass"
value
=
"org.apache.commons.httpclient.MultiThreadedHttpConnectionManager"
/>
</
bean
>
<
bean
id
=
"httpClient"
class
=
"org.apache.commons.httpclient.HttpClient"
>
<
constructor-arg
ref
=
"httpClientParams"
/>
</
bean
>
<
bean
id
=
"credentials"
class
=
"org.apache.commons.httpclient.UsernamePasswordCredentials"
>
<
constructor-arg
value
=
"admin"
/>
<
constructor-arg
value
=
"adminadmin"
/>
</
bean
>
<
bean
id
=
"httpClientFactory"
class
=
"org.springframework.http.client.CommonsClientHttpRequestFactory"
>
<
constructor-arg
ref
=
"httpClient"
/>
</
bean
>
<
bean
id
=
"restTemplate"
class
=
"org.springframework.web.client.RestTemplate"
>
<
constructor-arg
ref
=
"httpClientFactory"
/>
<
property
name
=
"messageConverters"
>
<
list
>
<
bean
class
=
"org.springframework.http.converter.StringHttpMessageConverter"
/>
</
list
>
</
property
>
</
bean
>
</
beans
>
|
The below code configures a Grizzly Server with SSL support for server side certificates, Basic Auth filter and Jersey resource servlet.
import
com.sun.grizzly.SSLConfig;
import
com.sun.grizzly.http.embed.GrizzlyWebServer;
import
com.sun.grizzly.http.servlet.ServletAdapter;
import
com.sun.jersey.api.container.filter.RolesAllowedResourceFilterFactory;
import
com.sun.jersey.api.core.ResourceConfig;
import
com.sun.jersey.spi.container.servlet.ServletContainer;
import
com.sun.jersey.samples.https_grizzly.auth.SecurityFilter;
import
javax.ws.rs.core.UriBuilder;
import
java.io.IOException;
import
java.net.URI;
public
class
GrizzlyServer {
private
static
GrizzlyWebServer webServer;
public
static
final
URI BASE_URI = getBaseURI();
private
static
URI getBaseURI() {
}
private
static
int
getPort(
int
defaultPort) {
String port = System.getenv(
"JERSEY_HTTP_PORT"
);
if
(
null
!= port) {
try
{
return
Integer.parseInt(port);
}
catch
(NumberFormatException e) {
}
}
return
defaultPort;
}
protected
static
void
startServer() {
webServer =
new
GrizzlyWebServer(getPort(
4463
),
"."
,
true
);
// add Jersey resource servlet
ServletAdapter jerseyAdapter =
new
ServletAdapter();
jerseyAdapter.addInitParameter(
"com.sun.jersey.config.property.packages"
,
"server.https.auth;server.https.resource"
);
jerseyAdapter.setContextPath(
"/"
);
jerseyAdapter.setServletInstance(
new
ServletContainer());
// add security filter (which handles http basic authentication)
jerseyAdapter.addInitParameter(ResourceConfig.PROPERTY_CONTAINER_REQUEST_FILTERS, SecurityFilter.
class
.getName());
// add authorization filter
jerseyAdapter.addInitParameter(ResourceConfig.PROPERTY_RESOURCE_FILTER_FACTORIES, RolesAllowedResourceFilterFactory.
class
.getName());
webServer.addGrizzlyAdapter(jerseyAdapter,
new
String[]{
"/"
});
// Grizzly ssl configuration
SSLConfig sslConfig =
new
SSLConfig();
sslConfig.setNeedClientAuth(
true
);
// don't work - known grizzly bug, will be fixed in 2.0.0
// set up security context
String keystore_server = Thread.currentThread().getContextClassLoader().getResource(
"keystore_server"
).getFile();
String truststore_server = Thread.currentThread().getContextClassLoader().getResource(
"truststore_server"
).getFile();
sslConfig.setKeyStoreFile(keystore_server);
// contains server keypair
sslConfig.setKeyStorePass(
"secret"
);
sslConfig.setTrustStoreFile(truststore_server);
// contains client certificate
sslConfig.setTrustStorePass(
"secret"
);
webServer.setSSLConfig(sslConfig);
// turn server side client certificate authentication on
// ((SSLSelectorThread) webServer.getSelectorThread()).setNeedClientAuth(true);
try
{
// start Grizzly embedded server //
System.out.println(String.format(
"Jersey app started with WADL at %sapplication.wadl"
, BASE_URI));
webServer.start();
}
catch
(Exception ex) {
System.out.println(ex.getMessage());
}
}
protected
static
void
stopServer() {
webServer.stop();
}
public
static
void
main(String[] args)
throws
InterruptedException, IOException {
startServer();
System.out.println(
"Hit return to stop..."
);
System.in.read();
stopServer();
}
}
|
Here’s slightly modified version of the sample Jersey Security filter which would handle the HTTP basic authentication on the server. The auth helper classes (AuthenticationExceptionMapper, AuthenticationException) are found here.
package
com.sun.jersey.samples.https_grizzly.auth;
import
com.sun.jersey.api.container.MappableContainerException;
import
com.sun.jersey.core.util.Base64;
import
com.sun.jersey.spi.container.ContainerRequest;
import
com.sun.jersey.spi.container.ContainerRequestFilter;
import
javax.ws.rs.WebApplicationException;
import
javax.ws.rs.core.Context;
import
javax.ws.rs.core.SecurityContext;
import
javax.ws.rs.core.UriInfo;
import
java.security.Principal;
public
class
SecurityFilter
implements
ContainerRequestFilter {
@Context
UriInfo uriInfo;
private
static
final
String REALM =
"HTTPS Example authentication"
;
public
ContainerRequest filter(ContainerRequest request) {
User user = authenticate(request);
request.setSecurityContext(
new
Authorizer(user));
return
request;
}
private
User authenticate(ContainerRequest request) {
// Extract authentication credentials
String authentication = request.getHeaderValue(ContainerRequest.AUTHORIZATION);
if
(authentication ==
null
) {
throw
new
MappableContainerException
(
new
AuthenticationException(
"Authentication credentials are required"
, REALM));
}
if
(!authentication.startsWith(
"Basic "
)) {
return
null
;
// additional checks should be done here
// "Only HTTP Basic authentication is supported"
}
authentication = authentication.substring(
"Basic "
.length());
String[] values =
new
String(Base64.base64Decode(authentication)).split(
":"
);
if
(values.length <
2
) {
throw
new
WebApplicationException(
400
);
// "Invalid syntax for username and password"
}
String username = values[
0
];
String password = values[
1
];
if
((username ==
null
) || (password ==
null
)) {
throw
new
WebApplicationException(
400
);
// "Missing username or password"
}
// Validate the extracted credentials
User user =
null
;
if
(username.equals(
"john"
) && password.equals(
"secret"
)) {
user =
new
User(
"john"
,
"user"
);
System.out.println(
"USER 'John Doe' AUTHENTICATED"
);
}
else
if
(username.equals(
"jane"
) && password.equals(
"secret"
)) {
user =
new
User(
"jane"
,
"user"
);
System.out.println(
"USER 'Jane Doe' AUTHENTICATED"
);
}
else
if
(username.equals(
"admin"
) && password.equals(
"adminadmin"
)) {
user =
new
User(
"admin"
,
"admin"
);
System.out.println(
"ADMIN AUTHENTICATED"
);
}
else
{
System.out.println(
"USER NOT AUTHENTICATED"
);
throw
new
MappableContainerException(
new
AuthenticationException(
"Invalid username or password\r\n"
, REALM));
}
return
user;
}
public
class
Authorizer
implements
SecurityContext {
private
User user;
private
Principal principal;
public
Authorizer(
final
User user) {
this
.user = user;
this
.principal =
new
Principal() {
public
String getName() {
return
user.username;
}
};
}
public
Principal getUserPrincipal() {
return
this
.principal;
}
public
boolean
isUserInRole(String role) {
return
(role.equals(user.role));
}
public
boolean
isSecure() {
return
"https"
.equals(uriInfo.getRequestUri().getScheme());
}
public
String getAuthenticationScheme() {
return
SecurityContext.BASIC_AUTH;
}
}
public
class
User {
public
String username;
public
String role;
public
User(String username, String role) {
this
.username = username;
this
.role = role;
}
}
}
|
The resource class is very simple. The resource methods are access controlled using the JSR-250 annotation @RolesAllowed. The methods are self-explanatory and they are just coded for illustration, not a fool-proof implementation. In this sample, the Grizzly server would perform server-side certificate authentication and HTTP Basic authentication, in addition to basic authorization checks.
import
com.sun.jersey.core.util.Base64;
import
javax.annotation.security.RolesAllowed;
import
javax.ws.rs.GET;
import
javax.ws.rs.Path;
import
javax.ws.rs.PathParam;
import
javax.ws.rs.core.Context;
import
javax.ws.rs.core.HttpHeaders;
import
javax.ws.rs.core.MediaType;
import
javax.ws.rs.core.Response;
@Path
(
"/"
)
public
class
HttpsResource {
@GET
@RolesAllowed
({
"admin"
})
@Path
(
"/locate/{username}"
)
public
Response getUserLocation(
@Context
HttpHeaders headers,
@PathParam
(
"username"
) String username) {
// you can get username from HttpHeaders
System.out.println(
"Service: GET / User Location for : "
+ username +
" requested by "
+ getUser(headers));
return
Response.ok(
"Billings, Montana"
).type(MediaType.TEXT_PLAIN).build();
}
@GET
@RolesAllowed
({
"admin"
,
"user"
})
public
Response getUserPin(
@Context
HttpHeaders headers) {
// you can get username from HttpHeaders
System.out.println(
"Service: GET / User Pin for: "
+ getUser(headers));
return
Response.ok(
"1234"
).type(MediaType.TEXT_PLAIN).build();
}
private
String getUser(HttpHeaders headers) {
String auth = headers.getRequestHeader(
"authorization"
).get(
0
);
auth = auth.substring(
"Basic "
.length());
String[] values =
new
String(Base64.base64Decode(auth)).split(
":"
);
String username = values[
0
];
String password = values[
1
];
return
username;
}
}
|
The following steps guide to create sample client and server certificates using the JDK keytool utility. The self-signed certificates are used for demonstration purposes only. In reality, this would be performed by a Certificate Authority (for ex: Verisign).
-
generate client and server keys:
keytool -genkey -keystore keystore_client -alias clientKey -dname “CN=www.aruld.info, OU=R&D, O=Vasun Technologies, L=Billings, ST=Montana, C=US”
keytool -genkey -keystore keystore_server -alias serverKey -dname “CN=www.aruld.info, OU=R&D, O=Vasun Technologies, L=Billings, ST=Montana, C=US”
-
generate client and server certificates:
keytool -export -alias clientKey -rfc -keystore keystore_client > client.cert
keytool -export -alias serverKey -rfc -keystore keystore_server > server.cert
-
import certificates to corresponding truststores:
keytool -import -alias clientCert -file client.cert -keystore truststore_server
keytool -import -alias serverCert -file server.cert -keystore truststore_client
SSL helper classes (AuthSSLProtocolSocketFactory, AuthSSLX509TrustManager, AuthSSLInitializationError) for the client-side are used from the Commons Client SSL contrib samples.
RestTemplate is injected into the RestSSLClient which uses the Commons Client APIs to set the credentials and configures the keystore and truststore on the client-side.
import
org.apache.commons.httpclient.Credentials;
import
org.apache.commons.httpclient.HttpClient;
import
org.apache.commons.httpclient.UsernamePasswordCredentials;
import
org.apache.commons.httpclient.auth.AuthScope;
import
org.apache.commons.httpclient.contrib.ssl.AuthSSLProtocolSocketFactory;
import
org.apache.commons.httpclient.protocol.Protocol;
import
org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
import
org.springframework.http.client.CommonsClientHttpRequestFactory;
import
org.springframework.web.client.RestTemplate;
import
java.net.MalformedURLException;
import
java.net.URL;
import
java.net.URISyntaxException;
import
java.util.Map;
import
java.util.HashMap;
public
class
RestSSLClient {
private
final
RestTemplate restTemplate;
private
final
HttpClient client;
private
Credentials credentials;
private
static
final
int
HTTPS_PORT =
4463
;
private
static
final
String HTTPS =
"https"
;
private
static
final
String HTTPS_HOST =
"localhost"
;
public
RestSSLClient(RestTemplate restTemplate, Credentials credentials) {
this
.restTemplate = restTemplate;
this
.credentials = credentials;
CommonsClientHttpRequestFactory factory = (CommonsClientHttpRequestFactory) restTemplate.getRequestFactory();
this
.client = factory.getHttpClient();
client.getState().setCredentials(AuthScope.ANY, credentials);
try
{
URL keystore_client = Thread.currentThread().getContextClassLoader().getResource(
"keystore_client"
).toURI().toURL();
URL truststore_client = Thread.currentThread().getContextClassLoader().getResource(
"truststore_client"
).toURI().toURL();
ProtocolSocketFactory protocolSocketFactory =
new
AuthSSLProtocolSocketFactory(keystore_client,
"secret"
,
truststore_client,
"secret"
);
Protocol authhttps =
new
Protocol(HTTPS, protocolSocketFactory, HTTPS_PORT);
Protocol.registerProtocol(HTTPS, authhttps);
client.getHostConfiguration().setHost(HTTPS_HOST, HTTPS_PORT, authhttps);
}
catch
(URISyntaxException e) {
e.printStackTrace();
}
catch
(MalformedURLException e) {
e.printStackTrace();
}
}
public
void
setCredentials(String user, String pass) {
this
.credentials =
new
UsernamePasswordCredentials(user, pass);
client.getState().setCredentials(AuthScope.ANY, credentials);
}
public
String get() {
return
restTemplate.getForObject(HTTPS_GET, String.
class
);
}
public
String getLocation(String user) {
Map<String, String> vars =
new
HashMap<String, String>();
vars.put(
"username"
, user);
return
restTemplate.getForObject(HTTPS_GET_LOCATION, String.
class
, vars);
}
}
|
The test code which invokes the SSL configured resource is shown below.
import
org.springframework.context.ApplicationContext;
import
org.springframework.context.support.ClassPathXmlApplicationContext;
public
class
Spring3RestSSLClient {
public
static
void
main(String[] args) {
ApplicationContext applicationContext =
new
ClassPathXmlApplicationContext(
"applicationContext-ssl.xml"
);
RestSSLClient client = applicationContext.getBean(
"sslClient"
, RestSSLClient.
class
);
System.out.println(
"John's Location : "
+ client.getLocation(
"john"
));
System.out.println(
"Jane's Location : "
+ client.getLocation(
"jane"
));
client.setCredentials(
"john"
,
"secret"
);
System.out.println(
"John Doe's Pin : "
+ client.get());
client.setCredentials(
"jane"
,
"secret"
);
System.out.println(
"Jane Doe's Pin : "
+ client.get());
}
}
|
WADL for this resource can be accessed from https://localhost:4463/application.wadl. You could access the URL from a browser as the server side client certificate authentication is disabled in the GrizzlyServer. (Uncommenting line # 70 would enable server side client cert auth, but this would force the browser to use the generated client keys). Test it yourself, you would be presented with a basic auth dialog (valid user/pass/role: admin/adminadmin/admin, john/secret/user, jane/secret/user) and you could access the resource methods with specified roles. I have tested with Firefox and Chrome. Enjoy!