Jade Dungeon

Tomcat安全防护

基本的HTTP Basic验证

的Tomcat的tomcat-users.xml是配置用户信息:

<tomcat-users>  
	<role rolename="test100"/>
	<user username="test123" password="test123" roles="test100"/>
</tomcat-users>

具体应用的web.xml中配置用户的访问范围:

<security-constraint>
	<web-resource-collection>
		<web-resource-name>protected Resource</web-resource-name>
		<url-pattern>/BasicVerify/*</url-pattern>
	</web-resource-collection>
	<auth-constraint>
		<role-name>test100</role-name>
	</auth-constraint>
</security-constraint>

<login-config>
	<auth-method>BASIC</auth-method>
	<realm-name>Default</realm-name>
</login-config>

网络访问限制

限制网络访问,比如在前端有代理的情况下(Apache httpd、Nginx), 只有代理可以访问HTTP连接与AJP连接:

# allow ws-host to connect to tomcat
iptables -A INPUT -p tcp --dport 8080 --source ws-host -d 10.0.0.2 -j ACCEPT
iptables -A INPUT -p tcp --dport 8009 --source ws-host -d 10.0.0.2 -j ACCEPT
iptables -A INPUT -p tcp --dport 8443 --source ws-host -d 10.0.0.2 -j ACCEPT

# denie all other host to connect to tomcat
iptables -A INPUT -p tcp --dport 8080 -d 10.0.0.2 -j DROP
iptables -A INPUT -p tcp --dport 8009 -d 10.0.0.2 -j DROP
iptables -A INPUT -p tcp --dport 8443 -d 10.0.0.2 -j DROP

使用SecurityManager

SecurityManager可以限制Java程序代码,从而接受或拒绝本地文件访问、特定的网络连接 、停机JVM等操作。

配置安全策略的配置文件为:

$CATALINA_HOME/conf/catalina.policy

其格式为Java安全策略文件的标准格式,当JVM启动参数带有-security选项时生效。

Java安全策略文件指定一组许可权限,每个许可都被授予特定的codebase或Java类,例:

// comment ...
grant codeBase LIST {
	premission PERM;
	premission PERM;
	...
}

比如指定JAVA_HOME下所有的类拥有一切权限:

grant codeBase "file:${java.home}/lib/-" {
	permission java.security.AllPermission;
}

注意,使用-而不是*来选择所有的类。

常用的权限策略,更详细的在Java文档的security > permissions部分:

权限的名称 解释
java.io.FilePermission Controls read/write/execute access to files and directories.
java.lang.RuntimePermission Allows access to System/Runtime functions like exit() and exec(). Use with care!
java.lang.reflect.ReflectPermission Allows classes to look up methods/fields in other classes, instantiate them, etc.
java.net.NetPermission Controls use of multicast network connections (rare).
java.net.SocketPermission Allows access to network sockets.
java.security.AllPermission Grants all permissions. Be careful!
java.security.SecurityPermission Controls access to Security methods. Be careful!
java.util.PropertyPermission Configures access to Java properties like java.home. Be careful!
java.security.UnresolvedPermission This is a placeholder permission for other permission types that will be loaded at runtime. See the JDK’s documentation for more detailed information on how this works.
java.io.SerializablePermission Allows code to write objects as a stream of bytes.
java.sql.SQLPermission Allows logging all SQL database communications.
java.util.logging.LoggingPermission Grants permission to a codebase to be able to change java.util.logging log settings.
javax.net.ssl.SSLPermission Enables a codebase to relax some restrictions on SSL communications.
javax.security.auth.AuthPermission This permission is able to relax many permissions that would otherwise restrict logins, Subjects, and Principals.
javax.security.auth.PrivateCredentialPermission Protects access to private Credentials objects belonging to a particular Subject.
javax.security.auth.kerberos.DelegationPermission Restricts the usage of the Kerberos delegation model.
javax.security.auth.kerberos.ServicePermission Protects Kerberos services and the credentials necessary to access those services.
org.apache.naming.JndiPermission Allows read access to files listed in JNDI.

演示本地文件操作权限

在Servlet类中操作文件:

FileOutputStram os = new FileOutputStram(new File("/opt/tomcat/webapps/ROOT", 
			"test.txt"));
os.write("test...\n".getBytes());
os.close();

这时启动带SecurityManager的Tomcat是会报错的,因为默认的权限文件里没有文件操作权限:

$CATALINA_HOME/bin/catalina.sh start -security

另一种启动方式给出了更加详细的配置:

JAVA_OPS="-Djava.security.manager -Djava.security.policy=$CATALINA_HOME/conf/catalina.policy"
export JAVA_OPS
$CATALINA_HOME/bin/catalina.sh start

要增加文件操作权限,就要给ROOT Web应用增加文件访问权限,在catalina.policy 末尾添加:

grant codeBase "file:${catalina.home}/webapps/ROOT/-" {
	permission java.io.FilePermission "${catalina.home}/webapp/ROOT/test.txt", "read,write,delete";
}

这样就可以得到对应文件的权限了,如果是得到访问所有文件的权限,可以用<<ALL FILES>>

SecurityManager故障排查

如果catalina.policy指定的权限没有按预期地操作,可以增加更多日志:

-Djava.security.debug=all

然后在所有的日志里查找是否有denied的安全防护调试内容。 任何安全防护的失效都会留下stack trace,以及指向ProtectionDomain失效的指针。

Tomcat chroot Jail

利用Unix系统的chroot机制,把一个目录作为执行命令的根目录, 从而实现该命令执行环境与操作系统的隔离。

设置Chroot Jail

前提:

  • 拥有root权限。
  • 在真实系统的/etc/init.d中安装了启动脚本。

准备一个目录作为虚拟的root目录:

mkdir /opt/chroot
cd /opt/chroot

mkdir -p lib lib64 etc tmp dev usr
chmod 755 etc dev usr
chmod 1777 tmp
cp -a /etc/hosts etc/hosts

mkdir -p usr/java
cp -a /usr/java/jdk1.6.0 usr/java

ldd命令检查需要哪些库才能让java运行:

ldd /usr/lib/jvm/java-1.7.0-openjdk-amd64/bin/java 
	linux-vdso.so.1 =>  (0x00007ffff1b78000)
	libjli.so => /usr/lib/jvm/java-1.7.0-openjdk-amd64/bin/../lib/amd64/jli/libjli.so (0x00007fd4d156c000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd4d11a7000)
	libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fd4d0fa3000)
	libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fd4d0d85000)
	libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007fd4d0b6c000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fd4d177a000)

再把这些库复制到对应的目录中去:

cp -p /lib64/libpthread.so.0 lib64/
cp -p /lib64/libdl.so.2 lib64/
cp -p /lib64/libc.so.6 lib64/
cp -p /lib64/ld-linux.so.2 lib64/

尝试执行Java,如果还有缺少的库,再复制过来:

cp -p /lib64/libm.so.6 lib64/
cp -p /lib64/libnsl.so.1 lib64/

为了让Chroot Jail更像真正的操作系统,在dev下创建一些新的设备目录:

cd /opt/chroot
mkdir -p /opt/chroot/dev/pts
cd /dev
./MAKEDEV -d /opt/chroot/dev null random urandom zero loop* log console
cp MAKEDEV /opt/chroot/dev
cp -a /dev/shm /opt/chroot/dev/

安装/proc文件系统:

mkdir -p /opt/chroot/proc
mount -t proc proc /opt/chroot/proc

配置域名解析:

cp -a /etc/hosts /etc/resolv.conf /etc/nsswitch.conf /opt/chroot/etc/
cp -p /lib64/libresolv.so.2 lib64/
cp -p /lib64/libnss_dns.so.2 lib64/
cp -p /lib64/libnss_files.so.2 lib64/

bash也要有:

cd /opt/chroot
mkdir -p bin
cp /bin/bash bin/
ln -s /bin/bash bin/sh
cd lib64
cp -p /lib64/libtermcap.so.2 .
cp -p /lib64/libdl.so.2 .
cp -p /lib64/libc.so.6 .
cp -p /lib64/ld-linux-x86-64.so.2 .

尝试运行JVM:

cd /opt/chroot
chroot /opt/chroot /usr/java/jdk1.6.0/bin/java -version
java version "1.6.0"
Java(TM) SE Runtime Environment (build 1.6.0-b105)
Java HotSpot(TM) 64-Bit Server VM (build 1.6.0-b105, mixed mode, sharing)

如果跑不起来,用trace查一下:

strace chroot /opt/chroot /usr/java/jdk1.6.0/bin/java -version

等JVM可以运行了,那Tomcat应该也可以运行了,安装Tomcat:

mkdir -p opt
chmod 755 opt
cd opt
cp ~jasonb/apache-tomcat-6.0.14.tar.gz .
gunzip apache-tomcat-6.0.14.tar.gz
tar xvf apache-tomcat-6.0.14.tar
mv apache-tomcat-6.0.14 tomcat

启动:

# chroot /opt/chroot /opt/tomcat/bin/catalina.sh start
/opt/tomcat/bin/catalina.sh: line 49: uname: command not found
/opt/tomcat/bin/catalina.sh: line 69: dirname: command not found
Cannot find //bin/setclasspath.sh
This file is needed to run this program

缺省uname和dirname程序,复制过来:

cp /bin/uname bin/
mkdir -p usr/bin
cp /usr/bin/dirname usr/bin/

启动:

# chroot /opt/chroot /opt/tomcat/bin/catalina.sh start
/opt/tomcat/bin/catalina.sh: line 136: tty: command not found
Using CATALINA_BASE: /opt/tomcat
Using CATALINA_HOME: /opt/tomcat
Using CATALINA_TMPDIR: /opt/tomcat/temp
Using JRE_HOME: /usr/java/jdk1.6.0
/opt/tomcat/bin/catalina.sh: line 240: touch: command not found

缺少ttytouch,也装上:

cp -p /lib64/librt.so.1 lib64/
cp /usr/bin/tty usr/bin/
cp /bin/touch bin/

现在可以成功启动,不报错了。但还是需要有启动脚本。

在真实系统(注意是真实系统不是chroot中)的/etc/init.d下放启动脚本 tc-chroot

#!/bin/sh
#
# Linux init script for the chrooted Apache Tomcat servlet container.
#
# chkconfig: 2345 96 14
# description: The Apache Tomcat servlet container.
# processname: tc-chroot
# config: /opt/chroot/tomcat/conf/tomcat-env.sh
#
# $Id$
#
# Author: Jason Brittain <jason.brittain@gmail.com>
 
APP_ENV="/opt/tomcat/conf/tomcat-env.sh"

# Source the app config file, if it exists.
[ -r "$APP_ENV" ] && . "${APP_ENV}"

# The path to the Tomcat start/stop script.
TOMCAT_SCRIPT=$CATALINA_HOME/bin/catalina.sh

# The name of this program.
PROG="$0"

# Resolve links - $0 may be a soft link.
while [ -h "$PROG" ]; do
	ls=`ls -ld "$PROG"`
	link=`expr "$ls" : '.*-> \(.*\)$'`
	if expr "$link" : '.*/.*' > /dev/null; then
		PROG="$link"
	else
		PROG=`dirname "$PROG"`/"$link"
	fi
done

PROG="`basename $PROG`"

case "$1" in
	start)
		echo -n "Starting $PROG: "

		# Mount /proc.
		mkdir -p /opt/chroot/proc
		mount -t proc proc /opt/chroot/proc &>/dev/null
		
		chroot /opt/chroot /bin/bash -c "set -a; . $APP_ENV; \
			$TOMCAT_SCRIPT start" &>/dev/null

		let RETVAL=$?
		if [ $RETVAL -eq 0 ]; then
			echo "[ OK ]"
		else
			echo "[ FAILED ]"
		fi
		;;
	stop)
		echo -n "Stopping $PROG: "

		chroot /opt/chroot /bin/bash -c "set -a; . $APP_ENV; \
			$TOMCAT_SCRIPT stop" &>/dev/null

		let RETVAL=$?
		if [ $RETVAL -eq 0 ]; then
			# Give Tomcat some time to properly stop all webapps.
			sleep 3

			# Unmount /proc.
			umount /opt/chroot/proc &>/dev/null
			echo "[ OK ]"
		else
			echo "[ FAILED ]"
		fi
		;;
	*)
		echo "Usage: tc-chroot {start|stop}"
		exit 1
esac

安装启动脚本:

# cp tc-chroot /etc/rc.d/init.d/
# chmod 755 /etc/rc.d/init.d/tc-chroot

执行:

# /etc/rc.d/init.d/tc-chroot start

或:

# service tc-chroot start

Chroot Jail中使用非Root用户

在BSD操作系统中,chroot支持开关,这里就不讨论了。

(略)

外部攻击

XSS

(略)

HTML注入

(略)

SQL注入

(略)

过滤Value过滤用户输入


/*
 * $Revision: 96 $
 * $Date: 2007-02-11 21:49:34 -0800 (Sun, 11 Feb 2007) $
 *
 * Copyright (c) 2007 O'Reilly Media.  All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.oreilly.tomcat.valve;

import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletException;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.util.ParameterMap;
import org.apache.catalina.valves.RequestFilterValve;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;


/**
 * Filters out bad user input from HTTP requests to avoid malicious
 * attacks including Cross Site Scripting (XSS), SQL Injection, and
 * HTML Injection vulnerabilities, among others.
 *
 * @author Jason Brittain
 */
public class BadInputValve extends RequestFilterValve {

    // --------------------------------------------- Static Variables

    /**
     * The Log instance to log with.
     */
    private static Log log = LogFactory.getLog(BadInputValve.class);

    /**
     * Descriptive information about this implementation.
     */
    protected static String info =
        "com.oreilly.tomcat.valve.BadInputValve/2.0";

    /**
     * An empty String array to re-use as a type indicator for toArray().
     */
    private static final String[] STRING_ARRAY = new String[0];

    // ------------------------------------------- Instance Variables

    /**
     * The flag that determines whether or not to escape quotes that are
     * part of the request.
     */
    protected boolean escapeQuotes = false;

    /**
     * The flag that determines whether or not to escape angle brackets
     * that are part of the request.
     */
    protected boolean escapeAngleBrackets = false;

    /**
     * The flag that determines whether or not to escape JavaScript
     * function and object names that are part of the request.
     */
    protected boolean escapeJavaScript = false;

    /**
     * A substitution mapping (regular expression to match, replacement)
     * that is used to replace single quotes (') and double quotes (")
     * with escaped equivalents that can't be used for malicious purposes.
     */
    protected HashMap<String, String> quotesHashMap =
        new HashMap<String, String>();

    /**
     * A substitution mapping (regular expression to match, replacement)
     * that is used to replace angle brackets (<>) with escaped
     * equivalents that can't be used for malicious purposes.
     */
    protected HashMap<String, String> angleBracketsHashMap =
        new HashMap<String, String>();

    /**
     * A substitution mapping (regular expression to match, replacement)
     * that is used to replace potentially dangerous JavaScript function
     * calls with escaped equivalents that can't be used for malicious
     * purposes.
     */
    protected HashMap<String, String> javaScriptHashMap =
        new HashMap<String, String>();
    
    /**
     * A Map of regular expressions used to filter the parameters.  The key
     * is the regular expression String to search for, and the value is the
     * regular expression String used to modify the parameter if the search
     * String is found.
     */
    protected HashMap<String, String> parameterEscapes =
        new HashMap<String, String>();

    // ------------------------------------------------- Constructors

    /**
     * Construct a new instance of this class with default property values.
     */
    public BadInputValve() {

        super();
        
        // Populate the regex escape maps.
        quotesHashMap.put("\"", "&quot;");
        quotesHashMap.put("\'", "&#39;");
        quotesHashMap.put("`", "&#96;");
        angleBracketsHashMap.put("<", "&lt;");
        angleBracketsHashMap.put(">", "&gt;");
        javaScriptHashMap.put(
            "document(.*)\\.(.*)cookie", "document&#46;&#99;ookie");
        javaScriptHashMap.put("eval(\\s*)\\(", "eval&#40;");
        javaScriptHashMap.put("setTimeout(\\s*)\\(", "setTimeout$1&#40;");
        javaScriptHashMap.put("setInterval(\\s*)\\(", "setInterval$1&#40;");
        javaScriptHashMap.put("execScript(\\s*)\\(", "exexScript$1&#40;");
        javaScriptHashMap.put("(?i)javascript(?-i):", "javascript&#58;");

        log.info("BadInputValve instantiated.");

    }

    // --------------------------------------------------- Properties

    /**
     * Gets the flag which determines whether this Valve will escape
     * any quotes (both double and single quotes) that are part of the
     * request, before the request is performed.
     */
    public boolean getEscapeQuotes() {

        return escapeQuotes;

    }

    /**
     * Sets the flag which determines whether this Valve will escape
     * any quotes (both double and single quotes) that are part of the
     * request, before the request is performed.
     *
     * @param escapeQuotes
     */
    public void setEscapeQuotes(boolean escapeQuotes) {

        this.escapeQuotes = escapeQuotes;
        if (escapeQuotes) {
            // Escape all quotes.
            parameterEscapes.putAll(quotesHashMap);
        }

    }

    /**
     * Gets the flag which determines whether this Valve will escape
     * any angle brackets that are part of the request, before the
     * request is performed.
     */
    public boolean getEscapeAngleBrackets() {

        return escapeAngleBrackets;

    }

    /**
     * Sets the flag which determines whether this Valve will escape
     * any angle brackets that are part of the request, before the
     * request is performed.
     *
     * @param escapeAngleBrackets
     */
    public void setEscapeAngleBrackets(boolean escapeAngleBrackets) {

        this.escapeAngleBrackets = escapeAngleBrackets;
        if (escapeAngleBrackets) {
            // Escape all angle brackets.
            parameterEscapes.putAll(angleBracketsHashMap);
        }

    }

    /**
     * Gets the flag which determines whether this Valve will escape
     * any potentially dangerous references to JavaScript functions
     * and objects that are part of the request, before the request is
     * performed.
     */
    public boolean getEscapeJavaScript() {

        return escapeJavaScript;

    }

    /**
     * Sets the flag which determines whether this Valve will escape
     * any potentially dangerous references to JavaScript functions
     * and objects that are part of the request, before the request is
     * performed.
     *
     * @param escapeJavaScript
     */
    public void setEscapeJavaScript(boolean escapeJavaScript) {

        this.escapeJavaScript = escapeJavaScript;
        if (escapeJavaScript) {
            // Escape potentially dangerous JavaScript method calls.
            parameterEscapes.putAll(javaScriptHashMap);
        }

    }

    /**
     * Return descriptive information about this Valve implementation.
     */
    public String getInfo() {

        return info;

    }

    // ----------------------------------------------- Public Methods

    /**
     * Sanitizes request parameters before bad user input gets into the
     * web application.
     *
     * @param request The servlet request to be processed
     * @param response The servlet response to be created
     *
     * @exception IOException if an input/output error occurs
     * @exception ServletException if a servlet error occurs
     */
    @Override
    public void invoke(Request request, Response response)
        throws IOException, ServletException {

        // Skip filtering for non-HTTP requests and responses.
        if (!(request instanceof HttpServletRequest) ||
            !(response instanceof HttpServletResponse)) {
            getNext().invoke(request, response);
            return;
        }

        // Only let requests through based on the allows and denies.
        if (processAllowsAndDenies(request, response)) {

            // Filter the input for potentially dangerous JavaScript
            // code so that bad user input is cleaned out of the request
            // by the time Tomcat begins to perform the request.
            filterParameters(request);

            // Perform the request.
            getNext().invoke(request, response);
        }

    }

    /**
     * Uses the functionality of the (abstract) RequestFilterValve to
     * stop requests that contain forbidden string patterns in parameter
     * names and parameter values.
     *
     * @param request The servlet request to be processed
     * @param response The servlet response to be created
     *
     * @exception IOException if an input/output error occurs
     * @exception ServletException if a servlet error occurs
     *
     * @return false if the request is forbidden, true otherwise.
     */
    public boolean processAllowsAndDenies(Request request, Response response)
        throws IOException, ServletException {

        ParameterMap paramMap =
            (ParameterMap) ((HttpServletRequest) request).getParameterMap();
        // Loop through the list of parameters.
        Iterator y = paramMap.keySet().iterator();
        while (y.hasNext()) {
            String name = (String) y.next();
            String[] values = ((HttpServletRequest)
                request).getParameterValues(name);

            // See if the name contains a forbidden pattern.
            if (!checkAllowsAndDenies(name, response)) {
                return false;
            }

            // Check the parameter's values for the pattern.
            if (values != null) {
                for (int i = 0; i < values.length; i++) {
                    String value = values[i];
                    if (!checkAllowsAndDenies(value, response)) {
                        return false;
                    }
                }
            }
        }

        // No parameter caused a deny.  The request should continue.
        return true;

    }

    /**
     * Perform the filtering that has been configured for this Valve,
     * matching against the specified request property. If the request
     * is allowed to proceed, this method returns true.  Otherwise,
     * this method sends a Forbidden error response page, and returns
     * false.
     *
     * <br><br>
     *
     * This method borrows heavily from RequestFilterValve.process(),
     * only this method has a boolean return type and doesn't call
     * getNext().invoke(request, response).
     *
     * @param property The request property on which to filter
     * @param response The servlet response to be processed
     *
     * @exception IOException if an input/output error occurs
     * @exception ServletException if a servlet error occurs
     *
     * @return true if the request is still allowed to proceed.
     */
    public boolean checkAllowsAndDenies(String property, Response response)
        throws IOException, ServletException {

        // If there were no denies and no allows, process the request.
        if (denies.length == 0 && allows.length == 0) {
            return true;
        }

        // Check the deny patterns, if any
        for (int i = 0; i < denies.length; i++) {
            Matcher m = denies[i].matcher(property);
            if (m.find()) {
                ServletResponse sres = response.getResponse();
                if (sres instanceof HttpServletResponse) {
                    HttpServletResponse hres = (HttpServletResponse) sres;
                    hres.sendError(HttpServletResponse.SC_FORBIDDEN);
                    return false;
                }
            }
        }

        // Check the allow patterns, if any
        for (int i = 0; i < allows.length; i++) {
            Matcher m = allows[i].matcher(property);
            if (m.find()) {
                return true;
            }
        }

        // Allow if denies specified but not allows
        if (denies.length > 0 && allows.length == 0) {
            return true;
        }

        // Otherwise, deny the request.
        ServletResponse sres = response.getResponse();
        if (sres instanceof HttpServletResponse) {
            HttpServletResponse hres = (HttpServletResponse) sres;
            hres.sendError(HttpServletResponse.SC_FORBIDDEN);
        }
        return false;

    }

    /**
     * Filters all existing parameters for potentially dangerous content,
     * and escapes any if they are found.
     *
     * @param request The Request that contains the parameters.
     */
    public void filterParameters(Request request) {

        ParameterMap paramMap =
            (ParameterMap) ((HttpServletRequest) request).getParameterMap();
        // Unlock the parameters map so we can modify the parameters.
        paramMap.setLocked(false);

        // Loop through each of the substitution patterns.
        Iterator escapesIterator = parameterEscapes.keySet().iterator();
        while (escapesIterator.hasNext()) {
            String patternString = (String) escapesIterator.next();
            Pattern pattern = Pattern.compile(patternString);

            // Loop through the list of parameters.
            @SuppressWarnings("unchecked")
            String[] paramNames =
                (String[]) paramMap.keySet().toArray(STRING_ARRAY);
            for (int i = 0; i < paramNames.length; i++) {
                String name = paramNames[i];
                String[] values = ((HttpServletRequest)
                    request).getParameterValues(name);
                // See if the name contains the pattern.
                boolean nameMatch;
                Matcher matcher = pattern.matcher(name);
                nameMatch = matcher.find();
                if (nameMatch) {
                    // The parameter's name matched a pattern, so we
                    // fix it by modifying the name, adding the parameter
                    // back as the new name, and removing the old one.
                    String newName = matcher.replaceAll(
                        (String) parameterEscapes.get(patternString));
                    request.addParameter(newName, values);
                    paramMap.remove(name);
                    log.warn("Parameter name " + name +
                        " matched pattern \"" + patternString +
                        "\".  Remote addr: " +
                        ((HttpServletRequest) request).getRemoteAddr());
                }
                // Check the parameter's values for the pattern.
                if (values != null) {
                    for (int j = 0; j < values.length; j++) {
                        String value = values[j];
                        boolean valueMatch;
                        matcher = pattern.matcher(value);
                        valueMatch = matcher.find();
                        if (valueMatch) {
                            // The value matched, so we modify the value
                            // and then set it back into the array.
                            String newValue;
                            newValue = matcher.replaceAll((String)
                                parameterEscapes.get(patternString));
                            values[j] = newValue;
                            log.warn("Parameter \"" + name +
                                "\"'s value \"" + value +
                                "\" matched pattern \"" +
                                patternString + "\".  Remote addr: " +
                                ((HttpServletRequest)
                                    request).getRemoteAddr());
                        }
                    }
                }
            }
        }
        // Make sure the parameters map is locked again when we're done.
        paramMap.setLocked(true);

    }

    /**
     * Return a text representation of this object.
     */
    @Override
    public String toString() {
        return "BadInputValve";
    }
}

主要属性:

clasName 必须是com.oreilly.tomcat.values.BadInputValue
escapeQuotes 转义引号,默认为false
escapeAngleBrackets 转义<>, 默认为false
escapejavaScript 转义JS函数与对象引用,默认true
allow 以逗号分隔的表达式,列出允许的请求。默认为空表示none
deny 以逗号分隔的表达式,列出拒绝的请求。

把类放在$CATALINA_HOME/lib中,并在配置Context中添加<Value>元素:

<Context path="" docBase="ROOT">
	<Valve className="com.oreilly.tomcat.valve.BadInputValve"
		deny="\x00,\x04,\x08,\x0a,\x0d"
		escapeQuotes="true"
		escapeAngleBrackets="true"
		escapeJavaScript="true"/>
</Context>

使用Filter过滤用户输入

/*
 * $Revision: 99 $
 * $Date: 2007-02-13 22:26:42 -0800 (Tue, 13 Feb 2007) $
 *
 * Copyright (c) 2007 O'Reilly Media.  All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.oreilly.tomcat.filter;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


/**
 * Filters out bad user input from HTTP requests to avoid malicious
 * attacks including Cross Site Scripting (XSS), SQL Injection, and
 * HTML Injection vulnerabilities, among others.
 *
 * @author Jason Brittain
 */
public class BadInputFilter implements Filter {

    // --------------------------------------------- Static Variables

    /**
     * Descriptive information about this implementation.
     */
    protected static String info =
        "com.oreilly.tomcat.filter.BadInputFilter/2.0";

    /**
     * An empty String array to re-use as a type indicator for toArray().
     */
    private static final String[] STRING_ARRAY = new String[0];

    // ------------------------------------------- Instance Variables

    /**
     * The flag that determines whether or not to escape quotes that are
     * part of the request.
     */
    protected boolean escapeQuotes = false;

    /**
     * The flag that determines whether or not to escape angle brackets
     * that are part of the request.
     */
    protected boolean escapeAngleBrackets = false;

    /**
     * The flag that determines whether or not to escape JavaScript
     * function and object names that are part of the request.
     */
    protected boolean escapeJavaScript = false;

    /**
     * A substitution mapping (regular expression to match, replacement)
     * that is used to replace single quotes (') and double quotes (")
     * with escaped equivalents that can't be used for malicious purposes.
     */
    protected HashMap<String, String> quotesHashMap =
        new HashMap<String, String>();

    /**
     * A substitution mapping (regular expression to match, replacement)
     * that is used to replace angle brackets (<>) with escaped
     * equivalents that can't be used for malicious purposes.
     */
    protected HashMap<String, String> angleBracketsHashMap =
        new HashMap<String, String>();

    /**
     * A substitution mapping (regular expression to match, replacement)
     * that is used to replace potentially dangerous JavaScript function
     * calls with escaped equivalents that can't be used for malicious
     * purposes.
     */
    protected HashMap<String, String> javaScriptHashMap =
        new HashMap<String, String>();

    /**
     * The comma-delimited set of <code>allow</code> expressions.
     */
    protected String allow = null;

    /**
     * The set of <code>allow</code> regular expressions we will evaluate.
     */
    protected Pattern allows[] = new Pattern[0];

    /**
     * The set of <code>deny</code> regular expressions we will evaluate.
     */
    protected Pattern denies[] = new Pattern[0];

    /**
     * The comma-delimited set of <code>deny</code> expressions.
     */
    protected String deny = null;

    /**
     * A Map of regular expressions used to filter the parameters.  The key
     * is the regular expression String to search for, and the value is the
     * regular expression String used to modify the parameter if the search
     * String is found.
     */
    protected HashMap<String, String> parameterEscapes =
        new HashMap<String, String>();

    /**
     * The ServletContext under which this Filter runs.  Used for logging.
     */
    protected ServletContext servletContext;

    /**
     * On Tomcat, the parameterMap must be unlocked, modified, then
     * unlocked.  But, the class that has the method to do that is part
     * of Tomcat, not part of the servlet API, so that class shouldn't
     * be visible to webapps, although it is, by default, on Tomcat 6.0.
     * This Filter uses reflection to invoke it, if it's there.
     */
    protected Method setLockedMethod;

    // ------------------------------------------------- Constructors

    /**
     * Construct a new instance of this class with default property values.
     */
    public BadInputFilter() {

        // Populate the regex escape maps.
        quotesHashMap.put("\"", "&quot;");
        quotesHashMap.put("\'", "&#39;");
        quotesHashMap.put("`", "&#96;");
        angleBracketsHashMap.put("<", "&lt;");
        angleBracketsHashMap.put(">", "&gt;");
        javaScriptHashMap.put(
            "document(.*)\\.(.*)cookie", "document&#46;&#99;ookie");
        javaScriptHashMap.put("eval(\\s*)\\(", "eval&#40;");
        javaScriptHashMap.put("setTimeout(\\s*)\\(", "setTimeout$1&#40;");
        javaScriptHashMap.put("setInterval(\\s*)\\(", "setInterval$1&#40;");
        javaScriptHashMap.put("execScript(\\s*)\\(", "exexScript$1&#40;");
        javaScriptHashMap.put("(?i)javascript(?-i):", "javascript&#58;");

    }

    // --------------------------------------------------- Properties

    /**
     * Gets the flag which determines whether this Filter will escape
     * any quotes (both double and single quotes) that are part of the
     * request, before the request is performed.
     */
    public boolean getEscapeQuotes() {

        return escapeQuotes;

    }

    /**
     * Sets the flag which determines whether this Filter will escape
     * any quotes (both double and single quotes) that are part of the
     * request, before the request is performed.
     *
     * @param escapeQuotes
     */
    public void setEscapeQuotes(boolean escapeQuotes) {

        this.escapeQuotes = escapeQuotes;
        if (escapeQuotes) {
            // Escape all quotes.
            parameterEscapes.putAll(quotesHashMap);
        }

    }

    /**
     * Gets the flag which determines whether this Filter will escape
     * any angle brackets that are part of the request, before the
     * request is performed.
     */
    public boolean getEscapeAngleBrackets() {

        return escapeAngleBrackets;

    }

    /**
     * Sets the flag which determines whether this Filter will escape
     * any angle brackets that are part of the request, before the
     * request is performed.
     *
     * @param escapeAngleBrackets
     */
    public void setEscapeAngleBrackets(boolean escapeAngleBrackets) {

        this.escapeAngleBrackets = escapeAngleBrackets;
        if (escapeAngleBrackets) {
            // Escape all angle brackets.
            parameterEscapes.putAll(angleBracketsHashMap);
        }

    }

    /**
     * Gets the flag which determines whether this Filter will escape
     * any potentially dangerous references to JavaScript functions
     * and objects that are part of the request, before the request is
     * performed.
     */
    public boolean getEscapeJavaScript() {

        return escapeJavaScript;

    }

    /**
     * Sets the flag which determines whether this Filter will escape
     * any potentially dangerous references to JavaScript functions
     * and objects that are part of the request, before the request is
     * performed.
     *
     * @param escapeJavaScript
     */
    public void setEscapeJavaScript(boolean escapeJavaScript) {

        this.escapeJavaScript = escapeJavaScript;
        if (escapeJavaScript) {
            // Escape potentially dangerous JavaScript method calls.
            parameterEscapes.putAll(javaScriptHashMap);
        }

    }
    
    /**
     * Return a comma-delimited set of the <code>allow</code> expressions
     * configured for this Filter, if any; otherwise, return <code>null</code>.
     */
    public String getAllow() {

        return (this.allow);

    }

    /**
     * Set the comma-delimited set of the <code>allow</code> expressions
     * configured for this Filter, if any.
     *
     * @param allow The new set of allow expressions
     */
    public void setAllow(String allow) {

        this.allow = allow;
        allows = precalculate(allow);
        servletContext.log("BadInputFilter: allow = " + deny);

    }

    /**
     * Return a comma-delimited set of the <code>deny</code> expressions
     * configured for this Filter, if any; otherwise, return
     * <code>null</code>.
     */
    public String getDeny() {

        return (this.deny);

    }

    /**
     * Set the comma-delimited set of the <code>deny</code> expressions
     * configured for this Filter, if any.
     *
     * @param deny The new set of deny expressions
     */
    public void setDeny(String deny) {

        this.deny = deny;
        denies = precalculate(deny);
        servletContext.log("BadInputFilter: deny = " + deny);

    }

    // ----------------------------------------------- Public Methods

    /**
     * {@inheritDoc}
     */
    public void init(FilterConfig filterConfig) throws ServletException {

        servletContext = filterConfig.getServletContext();
        
        // Parse the Filter's init parameters.
        setAllow(filterConfig.getInitParameter("allow"));
        setDeny(filterConfig.getInitParameter("deny"));
        String initParam = filterConfig.getInitParameter("escapeQuotes");
        if (initParam != null) {
            boolean flag = Boolean.parseBoolean(initParam);
            setEscapeQuotes(flag);
        }
        initParam = filterConfig.getInitParameter("escapeAngleBrackets");
        if (initParam != null) {
            boolean flag = Boolean.parseBoolean(initParam);
            setEscapeAngleBrackets(flag);
        }
        initParam = filterConfig.getInitParameter("escapeJavaScript");
        if (initParam != null) {
            boolean flag = Boolean.parseBoolean(initParam);
            setEscapeJavaScript(flag);
        }

        servletContext.log(toString() + " initialized.");

    }
    
    /**
     * Sanitizes request parameters before bad user input gets into the
     * web application.
     *
     * @param request The servlet request to be processed
     * @param response The servlet response to be created
     *
     * @exception IOException if an input/output error occurs
     * @exception ServletException if a servlet error occurs
     */
    public void doFilter(ServletRequest request, ServletResponse response,
                             FilterChain filterChain)
        throws IOException, ServletException {

        // Skip filtering for non-HTTP requests and responses.
        if (!(request instanceof HttpServletRequest) ||
            !(response instanceof HttpServletResponse)) {
            filterChain.doFilter(request, response);
            return;
        }

        // Only let requests through based on the allows and denies.
        if (processAllowsAndDenies(request, response)) {

            // Filter the input for potentially dangerous JavaScript
            // code so that bad user input is cleaned out of the request
            // by the time Tomcat begins to perform the request.
            filterParameters(request);

            // Perform the request.
            filterChain.doFilter(request, response);
        }
        
    }

    /**
     * Stops requests that contain forbidden string patterns in parameter
     * names and parameter values.
     *
     * @param request The servlet request to be processed
     * @param response The servlet response to be created
     *
     * @exception IOException if an input/output error occurs
     * @exception ServletException if a servlet error occurs
     *
     * @return false if the request is forbidden, true otherwise.
     */
    public boolean processAllowsAndDenies(ServletRequest request,
                                          ServletResponse response)
        throws IOException, ServletException {

        Map paramMap = request.getParameterMap();
        // Loop through the list of parameters.
        Iterator y = paramMap.keySet().iterator();
        while (y.hasNext()) {
            String name = (String) y.next();
            String[] values = request.getParameterValues(name);

            // See if the name contains a forbidden pattern.
            if (!checkAllowsAndDenies(name, response)) {
                return false;
            }

            // Check the parameter's values for the pattern.
            if (values != null) {
                for (int i = 0; i < values.length; i++) {
                    String value = values[i];
                    if (!checkAllowsAndDenies(value, response)) {
                        return false;
                    }
                }
            }
        }

        // No parameter caused a deny.  The request should continue.
        return true;
        
    }

    /**
     * Perform the filtering that has been configured for this Filter,
     * matching against the specified request property. If the request
     * is allowed to proceed, this method returns true.  Otherwise,
     * this method sends a Forbidden error response page, and returns
     * false.
     *
     * <br><br>
     *
     * This method borrows heavily from RequestFilterValve.process().
     *
     * @param property The request property on which to filter
     * @param response The servlet response to be processed
     *
     * @exception IOException if an input/output error occurs
     * @exception ServletException if a servlet error occurs
     *
     * @return true if the request is still allowed to proceed.
     */
    public boolean checkAllowsAndDenies(String property,
                                        ServletResponse response)
        throws IOException, ServletException {

        // If there were no denies and no allows, process the request.
        if (denies.length == 0 && allows.length == 0) {
            return true;
        }
        
        // Check the deny patterns, if any
        for (int i = 0; i < denies.length; i++) {
            Matcher m = denies[i].matcher(property);
            if (m.find()) {
                if (response instanceof HttpServletResponse) {
                    HttpServletResponse hres =
                        (HttpServletResponse) response;
                    hres.sendError(HttpServletResponse.SC_FORBIDDEN);
                    return false;
                }
            }
        }

        // Check the allow patterns, if any
        for (int i = 0; i < allows.length; i++) {
            Matcher m = allows[i].matcher(property);
            if (m.find()) {
                return true;
            }
        }

        // Allow if denies specified but not allows
        if (denies.length > 0 && allows.length == 0) {
            return true;
        }
        
        // Otherwise, deny the request.
        if (response instanceof HttpServletResponse) {
            HttpServletResponse hres = (HttpServletResponse) response;
            hres.sendError(HttpServletResponse.SC_FORBIDDEN);
        }
        return false;
        
    }

    /**
     * Filters all existing parameters for potentially dangerous content,
     * and escapes any if they are found.
     *
     * @param request The ServletRequest that contains the parameters.
     */
    @SuppressWarnings("unchecked")
    public void filterParameters(ServletRequest request) {

        Map paramMap = ((HttpServletRequest) request).getParameterMap();
        // Try to unlock the parameters map so we can modify the parameters.
        try {
            if (setLockedMethod == null) {
                setLockedMethod = paramMap.getClass().getMethod(
                    "setLocked", new Class[] { Boolean.TYPE });
            }
            setLockedMethod.invoke(paramMap, new Object[] { Boolean.FALSE });
        } catch (Exception e) {
            // Unable to unlock the parameters, and if this occurs while
            // running on Tomcat, we cannot filter the parameters.
            servletContext.log("BadInputFilter: Cannot filter parameters!");
        }
        
        // Loop through each of the substitution patterns.
        Iterator escapesIterator = parameterEscapes.keySet().iterator();
        while (escapesIterator.hasNext()) {
            String patternString = (String) escapesIterator.next();
            Pattern pattern = Pattern.compile(patternString);

            // Loop through the list of parameters.
            @SuppressWarnings("unchecked")
            String[] paramNames =
                (String[]) paramMap.keySet().toArray(STRING_ARRAY);
            for (int i = 0; i < paramNames.length; i++) {
                String name = paramNames[i];
                String[] values = ((HttpServletRequest)
                    request).getParameterValues(name);
                // See if the name contains the pattern.
                boolean nameMatch;
                Matcher matcher = pattern.matcher(name);
                nameMatch = matcher.matches();
                if (nameMatch) {
                    // The parameter's name matched a pattern, so we
                    // fix it by modifying the name, adding the parameter
                    // back as the new name, and removing the old one.
                    String newName = matcher.replaceAll(
                        (String) parameterEscapes.get(patternString));
                    paramMap.remove(name);
                    paramMap.put(newName, values);
                    servletContext.log("Parameter name " + name +
                        " matched pattern \"" + patternString +
                        "\".  Remote addr: " +
                        ((HttpServletRequest) request).getRemoteAddr());
                }
                // Check the parameter's values for the pattern.
                if (values != null) {
                    for (int j = 0; j < values.length; j++) {
                        String value = values[j];
                        boolean valueMatch;
                        matcher = pattern.matcher(value);
                        valueMatch = matcher.find();
                        if (valueMatch) {
                            // The value matched, so we modify the value
                            // and then set it back into the array.
                            String newValue;
                            newValue = matcher.replaceAll((String)
                                parameterEscapes.get(patternString));
                            values[j] = newValue;
                            servletContext.log("Parameter \"" + name +
                                "\"'s value \"" + value +
                                "\" matched pattern \"" +
                                patternString + "\".  Remote addr: " +
                                ((HttpServletRequest)
                                request).getRemoteAddr());
                        }
                    }
                }
            }
        }

        // Try to lock the parameters map again when we're done.
        try {
            if (setLockedMethod == null) {
                setLockedMethod = paramMap.getClass().getMethod(
                    "setLocked", new Class[] { Boolean.TYPE });
            }
            setLockedMethod.invoke(paramMap, new Object[] { Boolean.TRUE });
        } catch (Exception e) {
            // We already logged about this, so do nothing here.
        }
        
    }

    /**
     * Return a text representation of this object.
     */
    @Override
    public String toString() {

        return "BadInputFilter";

    }

    /**
     * {@inheritDoc}
     */
    public void destroy() {
                
    }
        
    // -------------------------------------------- Protected Methods

    /**
     * Return an array of regular expression objects initialized from the
     * specified argument, which must be <code>null</code> or a
     * comma-delimited list of regular expression patterns.
     *
     * @param list The comma-separated list of patterns
     *
     * @exception IllegalArgumentException if one of the patterns has
     *  invalid syntax
     */
    protected Pattern[] precalculate(String list) {

        if (list == null)
            return (new Pattern[0]);
        list = list.trim();
        if (list.length() < 1)
            return (new Pattern[0]);
        list += ",";

        ArrayList<Pattern> reList = new ArrayList<Pattern>();
        while (list.length() > 0) {
            int comma = list.indexOf(',');
            if (comma < 0)
                break;
            String pattern = list.substring(0, comma).trim();
            try {
                reList.add(Pattern.compile(pattern));
            } catch (PatternSyntaxException e) {
                IllegalArgumentException iae = new IllegalArgumentException(
                    "Syntax error in request filter pattern" + pattern);
                iae.initCause(e);
                throw iae;
            }
            list = list.substring(comma + 1);
        }

        Pattern reArray[] = new Pattern[reList.size()];
        return ((Pattern[]) reList.toArray(reArray));

    }
}

在应用程序的web.xml中添加过滤器:

<filter>
	<filter-name>BadInputFilter</filter-name>
	<filter-class>com.oreilly.tomcat.filter.BadInputFilter</filter-class>
	<init-param>
	<param-name>deny</param-name>
	<param-value>\x00,\x04,\x08,\x0a,\x0d</param-value>
	</init-param>
	<init-param>
	<param-name>escapeQuotes</param-name>
	<param-value>true</param-value>
	</init-param>
	<init-param>
	<param-name>escapeAngleBrackets</param-name>
	<param-value>true</param-value>
	</init-param>
	<init-param>
	<param-name>escapeJavaScript</param-name>
	<param-value>true</param-value>
	</init-param>
</filter>

<filter-mapping>
	<filter-name>BadInputFilter</filter-name>
	<url-pattern>/input_test.jsp</url-pattern>
</filter-mapping>

SSL安全连接

生成自签名服务器认证

用JDK keytool生成密钥

keytool -genkeypair -alias tomcat -keyalg RSA -keysize 1024 \
	-validity 365 -keystore /opt/tomcat/conf/keystore
  • -alias:密钥的别名。
  • -keyalg:加密方式,这里是RSA。
  • -keysize:密钥长度。
  • -validity:有效期(天数)。
  • -keystore:生成的密钥文件。

然后程序会交互式地收集信息:

Enter keystore password:  
Re-enter new password: 
What is your first and last name?
  [Unknown]:  test-tomcat.jade-docker.net
What is the name of your organizational unit?
  [Unknown]:  Jade Dungeon Home Department
What is the name of your organization?
  [Unknown]:  Jade Dungeon
What is the name of your City or Locality?
  [Unknown]:  Shanghai
What is the name of your State or Province?
  [Unknown]:  Shanghai
What is the two-letter country code for this unit?
  [Unknown]:  CN
Is CN=test-tomcat.jade-docker.net, OU=Jade Dungeon Home Department, O=Jade Dungeon, L=Shanghai, ST=Shanghai, C=CN correct?
  [no]:  yes

Enter key password for <tomcat>
	(RETURN if same as keystore password):
  • 注意「What is your first and last name?」,这里输入的是CN信息。 如果用于网站密钥就一定要和域名一致,不然浏览器会报错。

最后给出的确认信息:

CN=test-tomcat.jade-docker.net, OU=Jade Dungeon Home Department, \
	 O=Jade Dungeon, L=Shanghai, ST=Shanghai, C=CN

可以直接用它在命令行生成密钥而不用交互形式。不用交互形式生成:

keytool -genkeypair -alias tomcat -keyalg RSA -keysize 1024 \
	-validity 365 -keystore /opt/tomcat/conf/keystore \
	-dname "CN=test-tomcat.jade-docker.net, OU=Jade Dungeon Home Department, \ 
	O=Jade Dungeon, L=Shanghai, ST=Shanghai, C=CN"

用OpenSSH生成密钥

如果是用APR连接器支持HTTPS连接,就一定要用OpenSSL生成的密钥。 这样生成的密钥和自签名认证都是独立文件,而不是在密钥库中:

openssl genrsa -out rsa-private-key.pem 1024
openssl req -new -x509 -nodes -sha1 -days 365 -key rsa-private-key.pem -out selfsigned-cert.pem

还是交互方式收集信息:

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:Shanghai
Locality Name (eg, city) []:Shanghai
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Jade Dungeon
Organizational Unit Name (eg, section) []:Jade Dungeon Home Department
Common Name (e.g. server FQDN or YOUR name) []:test-tomcat.jade-docker.net
Email Address []:jade@test-dungeon.net

申请商业认证

主要步骤:

  1. 生成服务器端的密钥对,存储到密钥库中。
  2. 根据密钥对产生谁签名请求(CSR,Certificate Signing Request)。
  3. 把CSR发给供应商,购买到商业服务器授权证书(CA)。
  4. 收到供应商发来的CA认证和新的签名认证。
  5. 把CA认证导入Java的Cacerts密钥库中。
  6. 把签名服务器证书导入已经存储服务器密钥对的相同密钥库中。

生成密钥:

cd $CATALINA_HOME/conf
keytool -genkeypair -alias tomcat -keyalg RSA -keystore keystore

创建SCR文件,发给CA供应商:

mkdir -p -m go= /etc/ssl/private
keytool -certreq -keyalg RSA -alias tomcat \
	-file /etc/ssl/private/www.example.com.csr

从CA供应商那里收到服务器签名证书。因为只有你自己有私钥,这里的CA是安全的。

把从CA那里拿到的证书导入到存储着服务器端密钥对的密钥库中,插入选择的CA名以及 包含CA证书的文件名:

keytool -importcert -file /etc/ssl/ca-cert.pem -alias ca_name \
	-keystore /opt/tomcat/conf/keystore -trustcacerts

把签名后的服务器证书导入到已经存储有密钥对的相同密钥库中:

keytool -importcert -file /etc/ssl/your-signed-server-cert-file.pem \
	-alias tomcat -keystore /opt/tomcat/conf/keystore -trustcacerts

这样就可以用新的商业化签名的服务器证书了。

如果用的是OpenSSL工具:

# cd /opt/tomcat/conf
# openssl req -nodes -newkey rsa:1024 -keyout rsa-private-key.pem \
-out tomcat-csr.pem
Generating a 1024 bit RSA private key
...++++++
...........++++++
writing new private key to 'rsa-private-key.pem'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [GB]:US
State or Province Name (full name) [Berkshire]:Washington
Locality Name (eg, city) [Newbury]:Tacoma
Organization Name (eg, company) [My Company Ltd]:Groovy Wigs Inc.
Organizational Unit Name (eg, section) []:Wig Design Department
Common Name (eg, your name or your server's hostname) []:localhost
Email Address []:webmaster@groovywigs.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

这样私钥存储在文件rsa-private-key中,而SCR存储在文件tomcat-csr.pem里。 把tomcat-csr.pem发给CA,然后收到签名证书。

如果使用APR连接器,则不需要所签名证书导到密钥库文件中,APR连接器要求把它放在 独立的PEM文件中。

在Tomcat中启用SSL连接器

server.xml文件中有一段注释掉的关于SSL的配置,以它为基础实现具体的SSL连接方式:

<!-- Define a SSL HTTP/1.1 Connector on port 8443
	This connector uses the JSSE configuration, when using APR, the
	connector should be using the OpenSSL style configuration
	described in the APR documentation -->
<!--
<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
	maxThreads="150" scheme="https" secure="true"
	clientAuth="false" sslProtocol="TLS" />
-->

基于JIO连接器的SSL配置

只要打开SSL的注释,然后指定keystoreFilekeystorePass属性:

<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
	maxThreads="150" scheme="https" secure="true"
	clientAuth="false" sslProtocol="TLS"
	keystoreFile="conf/keystore" keystorePass="secrit"/>

可以通过浏览器打开:https://localhost:8443/

基于APR连接器的SSL配置

APR连接器要调用OpenSSL的本地库,所以要把APR库文件libtcnative编译为支持SSL的形式 。

如果编译了自己的APR连接器,则必须用--with-ssl开关配置编译过程, 并支持OpenSSL的版本上编译APR连接器。

编辑server.xml,确定classname属性是AprLifecycleListener类的<Listener> 元素,其中的SSLEngine值为on

<Server port="8005" shutdown="SHUTDOWN">
	
	<!--APR library loader. Documentation at /docs/apr.html -->
	<Listener className="org.apache.catalina.core.AprLifecycleListener"
		SSLEngine="on" />

	...
	
</Server>

然后设置HTTPS连接器:

<Connector port="8443"
	protocol="org.apache.coyote.http11.Http11AprProtocol"
	maxThreads="150" scheme="https" secure="true"
	clientAuth="false" sslProtocol="TLS" SSLEnabled="true"
	SSLCertificateKeyFile="/opt/tomcat/conf/rsa-private-key.pem"
	SSLCertificateFile="/opt/tomcat/conf/self-signed-cert.pem"/>

基于NIO连接器的SSL配置

主要是协议属性:

<Connector port="8443"
	protocol="org.apache.coyote.http11.Http11NioProtocol"
	maxThreads="150" scheme="https" secure="true"
	clientAuth="false" sslProtocol="TLS" SSLEnabled="true"
	keystoreFile="conf/keystore" keystorePass="secrit"/>

客户端认证

客户端直接用X.509认证,而不用输入用户名密码。

mkdir -p -m go= /etc/ssl/private
mkdir -p -m go= /etc/ssl/private/client
# openssl req -new -newkey rsa:512 -nodes \
	-out /etc/ssl/private/ca.csr -keyout /etc/ssl/private/ca.key

Using configuration from /usr/share/ssl/openssl.cnf
Generating a 512 bit RSA private key
..++++++++++++
.++++++++++++
writing new private key to '/etc/ssl/private/ca.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:California
Locality Name (eg, city) []:Dublin
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Jason's Certification
Authority
Organizational Unit Name (eg, section) []:System Administration
Common Name (eg, your name or your server's hostname) []:Jason's CA
Email Address []:jason.brittain@gmail.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

创建CA自签名及信任的X.509数字证书:

# openssl x509 -trustout -signkey /etc/ssl/private/ca.key \
	-days 365 -req -in /etc/ssl/private/ca.csr -out /etc/ssl/ca.pem

Signature ok
subject=/C=US/ST=California/L=Dublin/O=Jason's Certification Authority/OU=System
Administration/CN=Jason's CA/Email=jason.brittain@gmail.com
Getting Private key

把CA证书导出为PKCS12格式的证书,创建Tomcat的信任库文件(truststore):

# openssl pkcs12 -export -chain -in /etc/ssl/ca.pem \
	-inkey /etc/ssl/private/ca.key \
	-out /opt/tomcat/conf/truststore.p12 -name jasonsca \
	-CAfile /etc/ssl/ca.pem -caname jasonsca

Enter Export Password:secrit
Verifying - Enter Export Password:secrit

truststore文件意味着存储的是发出客户端证书的CA,而不是客户端证书。

无出truststore的内容清单,验证是否已经正确创建:

# keytool -list -keystore /opt/tomcat/conf/truststore.p12 -storetype pkcs12

Enter keystore password:secrit

Keystore type: PKCS12
Keystore provider: SunJSSE

Your keystore contains 1 entry

jason, Sep 27, 2007, PrivateKeyEntry,
Certificate fingerprint (MD5): E4:35:FB:6A:3D:C0:E9:FA:0C:38:D9:9E:75:D3:9A:14

创建CA要使用的序列号文件。OpenSSL通常以02开关:

echo "02" > /etc/ssl/private/ca.srl

为客户端认证创建密钥和认证请求:

$ openssl req -new -newkey rsa:512 -nodes -out \
/etc/ssl/private/client/client1.req -keyout \
/etc/ssl/private/client/client1.key

Using configuration from /usr/share/ssl/openssl.cnf
Generating a 512 bit RSA private key
.................++++++++++++
.........++++++++++++
writing new private key to '/etc/ssl/private/client/client1.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:California
Locality Name (eg, city) []:Dublin
Organization Name (eg, company) [Internet Widgits Pty Ltd]:O'Reilly
Organizational Unit Name (eg, section) []:.
Common Name (eg, your name or your server's hostname) []:jasonb
Email Address []:jason.brittain@gmail.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

使用CA证书和密钥创建X.509客户端证书,并签名:

# openssl x509 -CA /etc/ssl/ca.pem -CAkey /etc/ssl/private/ca.key \
	-CAserial /etc/ssl/private/ca.srl -req \
	-in /etc/ssl/private/client/client1.req \
	-out /etc/ssl/private/client/client1.pem

Signature ok
subject=/C=US/ST=California/L=Dublin/O=O'Reilly/CN=jasonb
/Email=jason.brittain@gmail.com
Getting CA Private Key

根据X.509客户端证书产生的PKCS12客户端证书,可以把PKCS12格式证书的副本导入浏览器 :

# openssl pkcs12 -export -clcerts -chain \
	-in /etc/ssl/private/client/client1.pem \
	-inkey /etc/ssl/private/client/client1.key \
	-out /etc/ssl/private/client/client1.p12 \
	-name "Jason's Client Certificate"

Enter Export Password:clientpw
Verifying password - Enter Export Password:clientpw

可以列出Tomcat的密钥库:

# keytool -list

Enter keystore password: password

Keystore type: jks
Keystore provider: SUN

Your keystore contains 1 entry:

tomcat, Thu Sep 27 06:07:25 PST 2007, keyEntry,
Certificate fingerprint (MD5): B9:77:65:1C:3F:95:F1:DC:36:E3:F7:7C:B0:07:B2:8C

配置Tomcat的HTTPS连接器,clientAuthtrue,并配置truststore属性:

<Connector port="8443" protocol="HTTP/1.1"
	maxThreads="150" scheme="https" secure="true"
	clientAuth="false" 
	sslProtocol="TLS" SSLEnabled="true"
	keystoreFile="conf/keystore" keystorePass="secrit"
	truststoreFile="conf/truststore.p12" truststorePass="secrit"
	truststoreType="PKCS12"/>

OpenSSL中录入的客户端身价被作为Tomcat中的用户名。 如果要与Tomcat中的角色组合,在权限文件tomcat-users.xml中配置:

<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
	<role rolename="tomcat"/>
	<role rolename="role1"/>
	<role rolename="manager"/>
	<role rolename="admin"/>
	<user username="EMAILADDRESS=jason.brittain@gmail.com, CN=Jasons Client, 
		OU=Glue Dept., O=Groovy Wigs Inc., L=Dublin, ST=California, C=US" 
		password="null" roles="admin"/>
</tomcat-users>

注意:因为通过证书授权,所以密码为null

把应用程序配置为CLIENT-CERT授权,在web.xml中:

<web-app>
	<display-name>Welcome to Tomcat</display-name>
	<description>Welcome to Tomcat</description>
	
	<login-config>
		<auth-method>CLIENT-CERT</auth-method>
		<realm-name>Client Cert Users-only Area</realm-name>
	</login-config>

</web-app>

这里没有使用安全防护约束。除了Realm以外,只要当配置应用程序使用CLIENT-CERT 的时候,才有必要使用安全防护约束。

然后把证书client1.p12导入浏览器,注意把证书发给用户的方法,Email不是很安全, 推荐用scp等加密的方式。

以Firefox为例:Edit > Preferences > Advanced > Security > View Certificates

可以用以下命令测试客户端认证:

openssl s_client -connect localhost:8443 \
	-cert /etc/ssl/private/client/client1.pem \
	-key /etc/ssl/private/client/client1.key -tls1

使用浏览器与服务器双向的SSL认证连接

为了实现双向认证,我们要生成两个keystore分别代表服务器和客户端。

  1. jettytest.server.keystore
  2. jettytest.client.p12.keystore

服务器证书

生成服务器keystore:

$ keytool -genkey -keyalg RSA -keysize 1024 -alias jettytest.server -keystore jettytest.server.keystore                     
Enter keystore password:  p@ssw0rd
Re-enter new password: p@ssw0rd
What is your first and last name?
  [Unknown]:  Jade Shan
What is the name of your organizational unit?
  [Unknown]:  Study Lab
What is the name of your organization?
  [Unknown]:  Jade Dungeon
What is the name of your City or Locality?
  [Unknown]:  Shanghai
What is the name of your State or Province?
  [Unknown]:  Shanghai
What is the two-letter country code for this unit?
  [Unknown]:  CN
Is CN=Jade Shan, OU=Study Lab, O=Jade Dungeon, L=Shanghai, ST=Shanghai, C=CN correct?
  [no]:  yes

Enter key password for <jettytest.server>
        (RETURN if same as keystore password):  p@ssw0rd
Re-enter new password: p@ssw0rd

由于不能直接将PKCS12格式的证书库导入,必须先把客户端证书导出为一个单独的CER文件

$ keytool -export -alias jettytest.server -keystore jettytest.server.keystore -file jettytest.server.cer
Enter keystore password:  p@ssw0rd
Certificate stored in file <jettytest.server.cer>

客户端证书

为浏览器生成证书,以便让服务器来验证它。为了能将证书顺利导入至IE和Firefox,证书格式应该是PKCS12

生成客户端证书:

$ keytool -genkey -keyalg RSA -keysize 1024 -storetype PKCS12 -alias jettytest.client -keystore jettytest.client.p12.keystore
Enter keystore password:  p@ssw0rd
Re-enter new password: p@ssw0rd
What is your first and last name?
  [Unknown]:  Jade Shan
What is the name of your organizational unit?
  [Unknown]:  Study Lab
What is the name of your organization?
  [Unknown]:  Jade Dungeon
What is the name of your City or Locality?
  [Unknown]:  Shanghai
What is the name of your State or Province?
  [Unknown]:  Shanghai
What is the two-letter country code for this unit?
  [Unknown]:  CN
Is CN=Jade Shan, OU=Study Lab, O=Jade Dungeon, L=Shanghai, ST=Shanghai, C=CN correct?
  [no]:  yes

由于不能直接将PKCS12格式的证书库导入,必须先把客户端证书导出为一个单独的CER文件

$ keytool -export -storetype PKCS12 -alias jettytest.client -keystore jettytest.client.p12.keystore -file jettytest.client.p12.cer 
Enter keystore password:  p@ssw0rd
Certificate stored in file <jettytest.client.p12.cer>

让服务器信任客户端证书

将客户端的证书文件导入到服务器的证书库,添加为一个信任证书:

$ keytool -import -v -alias jettytest.client -keystore jettytest.server.keystore -file jettytest.client.p12.cer
Enter keystore password:  p@ssw0rd
Owner: CN=Jade Shan, OU=Study Lab, O=Jade Dungeon, L=Shanghai, ST=Shanghai, C=CN
Issuer: CN=Jade Shan, OU=Study Lab, O=Jade Dungeon, L=Shanghai, ST=Shanghai, C=CN
Serial number: 3d6efc1c
Valid from: Fri Dec 30 17:53:46 CST 2016 until: Thu Mar 30 17:53:46 CST 2017
Certificate fingerprints:
         MD5:  1D:07:EA:0C:9B:93:24:02:8B:A3:60:8A:70:0B:66:80
         SHA1: B1:85:FC:01:D6:D3:F1:B7:CF:49:E2:DC:78:59:61:98:32:65:8A:68
         SHA256: 70:CC:3F:51:CE:E1:B4:AB:92:4B:9A:B1:03:CE:34:0A:69:6A:B8:37:70:13:28:C8:71:8D:DE:E1:11:0E:4F:AB
         Signature algorithm name: SHA256withRSA
         Version: 3

Extensions:

#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 71 B1 11 B2 78 28 97 2A   4F 40 D0 2F 1D 74 79 BB  q...x(.*O@./.ty.
0010: 38 54 4E 9A                                        8TN.
]
]

Trust this certificate? [no]:  yes
Certificate was added to keystore
[Storing jettytest.server.keystore]

查看证书库,现在应该有两个证书了:

$ keytool -list -keystore jettytest.server.keystore
Enter keystore password:  p@ssw0rd

Keystore type: JKS
Keystore provider: SUN

Your keystore contains 2 entries

jettytest.server, Dec 30, 2016, PrivateKeyEntry,
Certificate fingerprint (SHA1): 5B:D3:6B:81:2D:01:C6:7E:55:88:1C:DF:1A:6F:E8:A7:C4:FC:53:76
jettytest.client, Dec 30, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): B1:85:FC:01:D6:D3:F1:B7:CF:49:E2:DC:78:59:61:98:32:65:8A:68

还可以通过添加参数-v查看更加详细的信息。

配置Tomcat服务器

<Connector port="8443"
	protocol="org.apache.coyote.http11.Http11NioProtocol" 
	SSLEnabled="true"
	maxThreads="150"
	scheme="https"
	secure="true"
	clientAuth="true"
	sslProtocol="TLS"
	keystoreFile="D:/keys/jettytest.server.keystore"
	keystorePass="p@ssw0rd"
	truststoreFile="D:/keys/jettytest.server.keystore"
	truststorePass="p@ssw0rd" />

属性说明:

  • clientAuth:设置是否双向验证,默认为false,设置为true代表双向验证
  • keystoreFile:服务器证书文件路径
  • keystorePass:服务器证书密码
  • truststoreFile:用来验证客户端证书的根证书,此例中就是服务器证书
  • truststorePass:根证书密码

注意:

  1. 设置clientAuth属性为True时,需要手动导入客户端证书才能访问。
  2. 要访问https请求 需要访问8443端口,访问http请求则访问Tomcat默认端口(你自己设置的端口,默认8080)即可。

总结:

经过以上操作,你使用HTTPS 端口为8443 进行访问的时候 就是经过SSL信息加密,不怕被截获了。 通话的双方,必须是都拥有证书的端,才能进行会话,换句话说,就是只有安装了咱证书的客户端,才能与服务器通信。

强制 https 访问

在 tomcat /conf/web.xml 中的</welcome-file-list>后面加上这

<!-- Authorization setting for SSL -->    
<login-config>    
	<auth-method>CLIENT-CERT</auth-method>    
	<realm-name>Client Cert Users-only Area</realm-name>    
</login-config>    
<!-- Authorization setting for SSL -->    
<security-constraint>
	<web-resource-collection>
		<web-resource-name>SSL</web-resource-name>
		<url-pattern>/*</url-pattern>
	</web-resource-collection>
	<user-data-constraint>
		<transport-guarantee>CONFIDENTIAL</transport-guarantee>
	</user-data-constraint>
</security-constraint>

完成以上步骤后,在浏览器中输入http的访问地址也会自动转换为https了。

客户端加入客户端证书

由于是双向SSL认证,客户端一定要带有在服务器注册过的证书才可以。 因此,必须把在服务器那里注册过的证书jettytest.client.p12.keystore 添加到浏览器的【受信任的根证书颁发机构】。

在浏览器的证书导入中,导入jettytest.client.p12.keystore证书文件。

tomcat 5.8

Tomcat放大招了,8.5版本来了一次大改革,完全颠覆之前tomcat部署https的方式,简直就是不按套路出牌,不讲究。 广大tomcat8.5+用户着急了,楼主只能使出洪荒之力来圆大家https的梦。

1、首先您得有一张jks格式证书,没错还是jks格式证书。怎么获取证书,我就不多说了。 2、编辑 conf/server.xml 实现配置https。 原始配置:
<!--
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true">
	<SSLHostConfig>
		<Certificate certificateKeystoreFile="conf/localhost-rsa.jks" type="RSA" />
	</SSLHostConfig>
</Connector>
-->

去掉原始配置文件的注释,添加两个参数certificateKeyAlias(配置证书别名沃通获取的jks文件别名为1)和certificateKeystorePassword(配置证书密码),实例文件如下:

<Connector port="443" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true">
	<SSLHostConfig>
		<Certificate certificateKeystoreFile="F:\Tomcat 8.0\conf\ims.cn1.jks" 
			certificateKeyAlias="1" certificateKeystorePassword="密码" 
			type="RSA" />
	</SSLHostConfig>
</Connector>