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
缺少tty
和touch
,也装上:
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("\"", """); quotesHashMap.put("\'", "'"); quotesHashMap.put("`", "`"); angleBracketsHashMap.put("<", "<"); angleBracketsHashMap.put(">", ">"); javaScriptHashMap.put( "document(.*)\\.(.*)cookie", "document.cookie"); javaScriptHashMap.put("eval(\\s*)\\(", "eval("); javaScriptHashMap.put("setTimeout(\\s*)\\(", "setTimeout$1("); javaScriptHashMap.put("setInterval(\\s*)\\(", "setInterval$1("); javaScriptHashMap.put("execScript(\\s*)\\(", "exexScript$1("); javaScriptHashMap.put("(?i)javascript(?-i):", "javascript:"); 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("\"", """); quotesHashMap.put("\'", "'"); quotesHashMap.put("`", "`"); angleBracketsHashMap.put("<", "<"); angleBracketsHashMap.put(">", ">"); javaScriptHashMap.put( "document(.*)\\.(.*)cookie", "document.cookie"); javaScriptHashMap.put("eval(\\s*)\\(", "eval("); javaScriptHashMap.put("setTimeout(\\s*)\\(", "setTimeout$1("); javaScriptHashMap.put("setInterval(\\s*)\\(", "setInterval$1("); javaScriptHashMap.put("execScript(\\s*)\\(", "exexScript$1("); javaScriptHashMap.put("(?i)javascript(?-i):", "javascript:"); } // --------------------------------------------------- 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
申请商业认证
主要步骤:
- 生成服务器端的密钥对,存储到密钥库中。
- 根据密钥对产生谁签名请求(CSR,Certificate Signing Request)。
- 把CSR发给供应商,购买到商业服务器授权证书(CA)。
- 收到供应商发来的CA认证和新的签名认证。
- 把CA认证导入Java的Cacerts密钥库中。
- 把签名服务器证书导入已经存储服务器密钥对的相同密钥库中。
生成密钥:
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的注释,然后指定keystoreFile
和keystorePass
属性:
<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连接器,clientAuth
为true
,并配置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分别代表服务器和客户端。
- jettytest.server.keystore
- 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:根证书密码
注意:
- 设置clientAuth属性为True时,需要手动导入客户端证书才能访问。
- 要访问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>