The article Modern Java EE describe Java EE at the high level and the article Java EE tricks describe many details with a focus on web frontend. Both are focusing on the Java EE standard and not on specific servers.
This article focuses on how to configure and setup Tomcat showing the server specific features.
Java EE is a rather well defined standard that allows code to be written server indepdendent. But even well abstracted Java EE features need to be configured in a specific context.
The main directories of Tomcat are:
Directory | Content |
---|---|
$CATALINA_HOME/bin | Various scripts for startup, shutdown etc. |
$CATALINA_HOME/conf | Configuration files |
$CATALINA_HOME/lib | All jar files used by Tomcat itself (note: these are also available for web applications, but web applications jar files are not available for Tomcat itself) |
$CATALINA_HOME/logs | Log files |
$CATALINA_HOME/temp | Temporary files |
$CATALINA_HOME/wepapps | The web applications |
$CATALINA_HOME/work | See JSP compilation |
The main config files covering the entire server are:
Config file | Content |
---|---|
$CATALINA_HOME/conf/server.xml | The main configuation file - it defines connectors (HTTP, HTTPS, AJP) |
$CATALINA_HOME/conf/web.xml | Defaults for web applications WEB-INF/web.xml - it enables JSP support, define MIME types and define welcome files |
$CATALINA_HOME/conf/logging.properties | Control logging |
There are 3 possible locations to define application specific configuration:
For production servers use the second. For development servers use the third. Never use the first.
JSP pages are not interpreted. JSP pages are compiled. In fact JSP pages are tripple compiled:
One interesting aspect is that it is possible to see thee Java code generated by the jspc compiler.
$CATALINA_HOME/webapps/foobar/abc.jsp get compiled to $CATALINA_HOME/work/Catalina/localhost/foobar/org/apache/jsp/abc_jsp.java and even though the generated Java code is not that readable for humans then sometimes it can help troubleshoot weird error messages.
Establish a conection to the database before usage and close it again after usage can add significant overhead to database operations.
How much overhead depends on the database. Embedded databased has very little overhead. MySQL has low overhead. But some other database servers have huge overhead.
And when the transport to a non-embedded database is changed from plain socket to SSL socket, then the connection overhead explode due to the SSL handshake.
The solution is to use a database connection pool.
A database connection pool create N connections to the database and keep them open permanently. When applications need a connection then instead of creating a new connection it just borrows am existing connection from the pool and return it after usage.
Many performance problems have been solved by simply switching to using a database connection pool.
.NET ADO.NET providers comes with builtin connection pool, but Java JDBC drivers does not.
Luckily Java EE servers including Tomcat comes with a connection pool that just need to be enabled.
Configuring a connection pool is rather simple. Just supply the normal database connection information and specify pool size.
Example with MySQL pool size 30-100:
<Context>
...
<Resource name="jdbc/MySqlTest"
type="javax.sql.DataSource"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost/Test"
username="root"
password=""
maxIdle="30"
maxTotal="100"/>
...
</Context>
Code snippet without connection pool:
...
import java.sql.Connection;
import java.sql.DriverManager;
...
public class JDBC {
...
public int test() {
...
Class.forName("com.mysql.jdbc.Driver"); // not necessary in Java 6 and newer
Connection con = DriverManager.getConnection("jdbc:mysql://localhost/Test", "root", "");
... // using connection
con.close(); // should be done with try with resources in Java 7 and newer
...
}
...
}
Code snippet with connection pool:
...
import java.sql.Connection;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
...
public class DS {
private DataSource ds;
public DS() throws NamingException {
Context initctx = new InitialContext();
Context envctx = (Context)initctx.lookup("java:/comp/env");
ds = (DataSource)envctx.lookup("jdbc/MySqlTest");
}
...
public int test() {
...
Connection con = ds.getConnection();
... // using connection
con.close(); // should be done with try with resources in Java 7 and newer
...
}
...
}
In many cases where JSF or CDI are present the above code can be made a little more elegant with injection:
...
import java.sql.Connection;
import javax.annotation.ManagedBean;
import javax.annotation.Resource;
import javax.sql.DataSource;
...
@ManagedBean // only needed if not a servlet and not an EJB
public class DS {
@Resource(name="jdbc/MySqlTest")
private DataSource ds;
...
public int test() {
...
Connection con = ds.getConnection();
... // using connection
con.close(); // should be done with try with resources in Java 7 and newer
...
}
...
}
Usually database access is done via JPA or another ORM.
The article Modern Java EE has an example on how to use data source with JPA here (scroll down a few pages to find persistence.xml example).
Java EE has a standard for how to protect parts or the entire web application. Protect means require login and user to be in a specific role.
But the actual authentication mechanism must be defined in the server.
Tomcat supports several such mechanisms called realms:
We will look at MemoryRealm that is good for developer test and the two database realms that can be used for production usage.
Realms are always defined in Context config section (see above for where it can be located).
Example with MemoryRealm:
<Context>
...
<Realm className="org.apache.catalina.realm.MemoryRealm"
pathname="webapps/test/WEB-INF/tomcat-users.xml"/>
...
</Context>
It assumes a WEB-INF/tomcat-users.xml with:
<tomcat-users>
<role rolename="admin"/>
<role rolename="user"/>
<user username="arne" password="secret" roles="admin,user"/>
</tomcat-users>
Do not use MemoryReal for production usage. Having passwords in clear text in a file is a security risk. And there is no supported way of updating users programmatically.
Example with JDBCRealm:
<Context>
...
<Realm className="org.apache.catalina.realm.JDBCRealm"
driverName="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost/Test"
connectionName="root"
connectionPassword=""
userTable="webusers"
userRoleTable="webroles"
userNameCol="usr"
userCredCol="pwd"
roleNameCol="role">
<CredentialHandler className="org.apache.catalina.realm.MessageDigestCredentialHandler"
algorithm="sha-512"
iterations="1000"
saltLength="32"/>
</Realm>
...
</Context>
Example with DataSourceRealm:
<Context>
...
<Realm className="org.apache.catalina.realm.DataSourceRealm"
dataSourceName="jdbc/MySqlTest"
userTable="webusers"
userRoleTable="webroles"
userNameCol="usr"
userCredCol="pwd"
roleNameCol="role">
<CredentialHandler className="org.apache.catalina.realm.MessageDigestCredentialHandler"
algorithm="sha-512"
iterations="1000"
saltLength="32"/>
</Realm>
...
</Context>
Both assume a database setup like the following:
CREATE TABLE webusers (
usr VARCHAR(20) NOT NULL,
pwd VARCHAR(255) NOT NULL,
PRIMARY KEY(usr)
);
CREATE TABLE webroles (
usr VARCHAR(20) NOT NULL,
role VARCHAR(20) NOT NULL,
PRIMARY KEY(usr,role)
);
INSERT INTO webusers VALUES('arne', '4356f3c7e5e4591f3aa6a984cf1e48a7ab00af6412588b0305b8d4c493b9a501$1000$bc17f265f840b1c10c0f80e29617debc41f0aeca01f50b763204266e7ec7fbf4119987fb88e4b7be193007e58f4d219af9dc942a218503f6aa360eee67c97f83');
INSERT INTO webroles VALUES('arne', 'user');
INSERT INTO webroles VALUES('arne', 'admin');
So what is the story about the weird password?
It is very bad security to store a real password in the database so it can be retrieved. Instead one should store a hash of the password.
To help against dictionary attacks the hash should be of a random unique salt combined with the password.
The above weird looking string contains salt$iteration$hash.
And the hash is 1000 iterations using SHA-512 algorithm.
Such strings can be set administrative by using the digest tool in $CATALINA_HOME/bin dir:
C:\Apache\apache-tomcat-8.5.12\bin>digest -a sha-512 -i 1000 -s 32 secret secret:4356f3c7e5e4591f3aa6a984cf1e48a7ab00af6412588b0305b8d4c493b9a501$1000$bc17f265f840b1c10c0f80e29617debc41f0aeca01f50b763204266e7ec7fbf4119987fb88e4b7be193007e58f4d219af9dc942a218503f6aa360eee67c97f83
Or generated by Java code to be used from a user creation module within the web application:
package test;
import java.security.NoSuchAlgorithmException;
import org.apache.catalina.realm.MessageDigestCredentialHandler;
public class PwdGen {
public static String gen(String algorithm, int iterations, int saltlen, String pwd) throws NoSuchAlgorithmException {
MessageDigestCredentialHandler md = new MessageDigestCredentialHandler();
md.setAlgorithm(algorithm);
md.setIterations(iterations);
md.setSaltLength(saltlen);
return md.mutate(pwd);
}
public static String gen(String pwd) throws NoSuchAlgorithmException {
return gen("sha-512", 1000, 32, pwd);
}
}
If none of the predefined realms fit into a specific context, then it is possible to write a custom realm.
Here is a simple demo:
<Realm className="test.MyRealm" extra="test"/>
MyRealm.java:
package test;
import java.security.Principal;
import java.util.Arrays;
import org.apache.catalina.realm.GenericPrincipal;
import org.apache.catalina.realm.RealmBase;
public class MyRealm extends RealmBase {
private String extra;
public String getExtra() {
return extra;
}
public void setExtra(String extra) {
this.extra = extra;
}
@Override
protected String getName() {
return "MyRealm";
}
@Override
protected String getPassword(String username) {
GenericPrincipal p = (GenericPrincipal)getPrincipal(username);
if(p != null) {
return p.getPassword();
} else {
return null;
}
}
@Override
protected Principal getPrincipal(String username) {
if(username.equals("arne")) {
return new GenericPrincipal("arne", "secret", Arrays.asList("user", "admin", extra));
} else {
return null;
}
}
}
Note that the jar file with the code need to be in $CATALINA_HOME/lib dir.
To prevent password guessing accounts can be locked out for a number of seconds after a number of failed attempts.
This is done with the LockoutRealm, which is a nested realm that goes around the real realm.
Example fragment:
<Realm className="org.apache.catalina.realm.LockOutRealm" failureCount="3" lockOutTime="180">
<Realm .../>
</Realm>
This section assumes Tomcat 8.5 or newer. Older versions are different - see documentation for those.
Getting started with a self signed certificate is easy:
Create keystore with certficate example:
C:\Apache\apache-tomcat-8.5.12\conf>keytool -genkey -alias tomcat -keyalg RSA -keystore .\test.jks Enter keystore password: Re-enter new password: What is your first and last name? [Unknown]: Arne Vajhoej What is the name of your organizational unit? [Unknown]: Article writing What is the name of your organization? [Unknown]: Personal What is the name of your City or Locality? [Unknown]: Coventry What is the name of your State or Province? [Unknown]: RI What is the two-letter country code for this unit? [Unknown]: US Is CN=Arne Vajhoej, OU=Article writing, O=Personal, L=Coventry, ST=RI, C=US corr ect? [no]: yes Enter key password for <tomcat> (RETURN if same as keystore password):
server.xml fragment:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="conf/test.jks" certificateKeystorePassword="bigsecret"/>
</SSLHostConfig>
</Connector>
And now URL's https://localhost:8443/test/whatever work.
Well - they do not really work. All modern browsers will produce severe warnings about the self signed certficate.
To get a real certficate you need to:
I have never tried that process, so I can not give any insights.
To generate CSR you can use:
keytool -genkey -alias tomcat -keyalg RSA -keystore .\real.jks keytool -certreq -keyalg RSA -alias tomcat -file certreq.csr -keystore .\real.jks
To import real certficate you can use:
keytool -import -alias root -keystore .\real.jks -trustcacerts -file <chain certificate> keytool -import -alias tomcat -keystore .\real.jks -file <real certificate>
(and change server.xml to use real.jks instead of test.jks)
Tomcat supports HTTP 2.0 but only for HTTPS.
To enable it use the following server.xml fragment:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled="true">
<UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol"/>
<SSLHostConfig>
<Certificate certificateKeystoreFile="conf/test.jks" certificateKeystorePassword="bigsecret"/>
</SSLHostConfig>
</Connector>
Tomcat provides a neat way to control cache-control by allowing to set max-age for diffent content types via a filter.
Example WEB-INF/web.xml fragment:
<filter>
<filter-name>ExpiresFilter</filter-name>
<filter-class>org.apache.catalina.filters.ExpiresFilter</filter-class>
<init-param>
<param-name>ExpiresByType image</param-name>
<param-value>access plus 10 minutes</param-value>
</init-param>
<init-param>
<param-name>ExpiresByType text/css</param-name>
<param-value>access plus 10 minutes</param-value>
</init-param>
<init-param>
<param-name>ExpiresByType application/javascript</param-name>
<param-value>access plus 10 minutes</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>ExpiresFilter</filter-name>
<url-pattern>*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
Sometimes it is preferred to put Apache httpd in front of Tomcat.
Possible reasons being:
The extra security is achieved by having an extra firewall.
From:
To:
The two firewalls create a DMZ where the Apache httpd reside and provide an extra layer of security. /
And mod_security can be used to block many forms of attacks.
(there may also be a firewall between Tomcat and database, but that is out of scope for this article)
Note that Apache httpd uses a special protocol AJP (Apache Jserv Protocol) to communicate with Tomcat.
There are 2 supported ways to translate from HTTP(S) to AJP:
mod_jk is the traditional module and comes with a lot of configurability. But setup can be a bit tricky for beginners.
mod_proxy_ajp is only available in newer Apache httpd and has much less configurability. But it is very easy to setup.
httpd.conf fragment:
# load module
LoadModule jk_module "C:/DivNative/32bit/mod_jk-1.2.42/mod_jk.so"
# define workers
JkWorkersFile conf/workers.properties
# define logging
JkLogFile logs/mod_jk.log
JkLogLevel info
# forward in all virtual hosts
JkMountCopy All
# forward requests matching /test/*
JkMount /test/* testworker
workers.properties:
worker.list=testworker,jkstatus
#
worker.testworker.type=ajp13
worker.testworker.host=localhost
worker.testworker.port=8009
worker.testworker.connection_pool_size=100
#
worker.jkstatus.type=status
httpd.conf fragments:
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_ajp_module modules/mod_proxy_ajp.so
...
ProxyPass "/test" "ajp://localhost:8009/test"
Version | Date | Description |
---|---|---|
1.0 | September 7th 2017 | Initial version |
1.1 | October 14th 2017 | Add description of cache filter and HTTP 2.0 support |
See list of all articles here
Please send comments to Arne Vajhøj