ホームに戻る
Tomcat9の覚え書き
0、はじめに
Raspberry PIでTomcat9を動かします。
Tomcatは「Webコンテナ」という分類になるそうです。
Webアクセスに対してJavaServletとの中継をする役割があります。
Apacheと連携する方法もあるそうですが以下では取り扱いません。
動作例として、
Webブラウザからの入力に対して、
TomcatがServletを呼び出して動的に出力を生成します。
複数アクセスに対するスレッド処理はTomcatが行うようです。
Webサービスを作成する利点は、
クライアントがブラウザベースであるため、
インストールの手間が無いこと、環境依存が少ないこと。
1、インストール
tomcat9のインストールは以下。
apt-get install tomcat9
jdkのインストールは以下。
apt-get install openjdk-8-jdk
「Error occurred during initialization of VM」
というメッセージが出たら以下を試す。
(11のところは10ないし9にしてみる。)
apt-get remove openjdk-11-jre-headless
以下で動作を確認。
「active」と出ていれば成功。
systemctl status tomcat9
ブラウザから192.172.1.1:8080で「It works!」が表示される。
2、ディレクトリ
「It works!」の画面を確認すると、
例えば、今回の環境では、
CATALINA_HOME は /usr/share/tomcat9 であること、
CATALINA_BASE は /var/lib/tomcat9 であることがわかる。
CATALINA_BASE のほうにwebappsというディレクトリがあり、
次のようにディレクトリを作成する。
/var/lib/tomcat9/webapps/HOGE/WEB-INF/classes
ここにHelloWorld.javaなどのコードを置く。
CATALINA_HOME にはServletのライブラリが含まれる。
次のようにコンパイルを行う。
javac -classpath /usr/share/tomcat9/lib/servlet-api.jar HelloWorld.java
/lib/tomcat9/webapps/HOGE/以降のディレクトリは普通にアクセス可能。
例えば、/var/lib/tomcat9/webapps/HOGE/index.htmlとファイルを置くと、
http://192.172.1.1:8080/HOGE/index.htmlでアクセスできる。
ただし、/var/lib/tomcat9/webapps/HOGE/WEB-INF/以降はアクセスできない。
3、出力のみのサンプルコード
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class HelloWorld extends HttpServlet {
private static final long serialVersionUID = 1L;
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
{
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>HelloWorld</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>HelloWorld</h1>");
out.println("</body>");
out.println("</html>");
}
}
4、web.xml
/var/lib/tomcat9/webapps/HOGE/WEB-INF に web.xmlを置きます。
これで 192.172.1.1:8080/HOGE/servlet/hello にアクセスすると、
HelloWorldと表示されるようになります。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0"
metadata-complete="true">
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>HelloWorld</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>
hello
</servlet-name>
<url-pattern>
/servlet/hello
</url-pattern>
</servlet-mapping>
</web-app>
5、classの自動更新設定
classファイルは更新されても自動でリロードされません。
よって、度々再起動するのですが、開発時は面倒です。
以下の
/var/lib/tomcat9/conf/server.xml
の<HOST>〜</HOST>内に以下を追加。
<Context path="/HOGE" docBase="/var/lib/tomcat9/webapps/HOGE" debug="0"
reloadable="true"/>
設定後はいちど再起動を行います。
これでclassファイルが更新されるたびにリロードされます。
ただし、これは定期的に更新を確認しにいくため余分な負荷がかかります。
開発が終わり運用する場合にはfalseに変更すべきです。
6、入力(GET)
以下でGETで入力を送れます。
http://x.x.x.x:8080/abc/servlet/hello?name=World
/var/lib/tomcat9/webapps/abc/WEB-INF/classes/HelloWorld.class
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class HelloWorld extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
{
response.setContentType("text/html");
PrintWriter out = response.getWriter();
String name = request.getParameter("name");
out.println(String.format("Hello,%s!",name));
}
}
POSTに関してはdoPostに送られます。
7、ファイルの入出力
tomcatを起動しているユーザ名はtomcat、グループ名はtomcatです。
ディレクトリないしファイルに読み込み、書き込みの権限を与えます。
まずは次のディレクトリを作成します。
mkdir /var/lib/tomcat9/webapps/counter/file
ユーザ名とグループ名を変更します。
chown tomcat:tomcat /var/lib/tomcat9/webapps/counter/file
権限は読み書きのみなら 100、ファイルの新規作成をするなら 300にします。
以下のサンプルコードは新規作成をするので 300にする必要あり。
chmod 300 /var/lib/tomcat9/webapps/counter/file
他ユーザに編集させたい場合はさらにグループ権限をつけると良いでしょう。
サンプルサーブレットのページを閲覧すると新規でファイルを作成します。
/var/lib/tomcat9/webapps/counter/file/count.txt
ファイルの権限は新規作成ファイルの設定に従います。
新規作成ファイルの権限についてはumaskで調整します。
ユーザ名はtomcat、グループ名はtomcatになります。
権限の確認は以下のコマンドで、
ls -la /var/lib/tomcat9/webapps/counter/file/count.txt
以下は訪問数をカウントするサーブレットのサンプルコードです。
tomcatは複数アクセスでの整合性を保障しないので注意が必要です。
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class HelloWorld extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
{
int count;
String fname ="/var/lib/tomcat9/webapps/counter/file/count.txt";
File file = new File(fname);
if(file.exists()){
BufferedReader br = new BufferedReader(new FileReader(file));
String line = br.readLine();
count = Integer.parseInt(line);
count++;
br.close();
}
else{
count = 1;
}
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)));
pw.println(count);
pw.close();
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println(String.format("COUNT:%s",count));
}
}
8、Basic認証
Basic認証は名前とパスワードで認証を行います。
認証に暗号は使用されません。
ブラウザを閉じると認証期間が終了します。
他に、Digest認証はMD5で暗号化されますが、
暗号化は認証のみで通信自体は暗号化されません。
Form認証は暗号化されません。
セッション管理なのでブラウザを閉じても認証は続きます。
通信自体を暗号化する認証にはSSLを使うのが普通でしょう。
今回はBasic認証をやります。
/var/lib/tomcat9/webapps/abc/WEB-INF/web.xmlに以下を追加。
roleに「basic」を設定してある。
<security-constraint>
<web-resource-collection>
<web-resource-name>User Basic Auth</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>basic</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>User Basic Auth</realm-name>
</login-config>
<security-role>
<role-name>basic</role-name>
</security-role>
/var/lib/tomcat9/conf/tomcat-users.xmlに以下を追加。
他に、SQLでも管理できますがここではやりません。
<role rolename="basic"/>
<user username="hogehoge" password="pw" roles="basic"/>
サーブレットからユーザ名は以下のように拾えます。
String user =(request.getRemoteUser()).toString();
9、JSP
JPSはHTMLにJavaのコードを埋め込む方法です。
servletにHTMLを大量に埋め込む必要性がなくなります。
servletでは処理を、JSPでは表示をと役割分担するのが普通です。
JSPは/var/lib/tomcat9/webapps/HOGE/index.jspのようにHOGE以降に配置します。
JSPであるかどうかは拡張子で判断されます。
以下はindex.jspの一例です。
<%-- commet --%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="java.util.Date,java.text.SimpleDateFormat" %>
<%
String name = "abc";
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
String today = sdf.format(date);
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<% for(int i = 0; i < 10; i++){ %>
My name is <%= name %> <%= i %><br>
<% } %>
Today is <%= today %>
</body>
</html>
10、servletからJSPへのフォワード、リダイレクト
servletからJSPを呼び出すことができます。
フォワードは同一サーバー内でURLを変えずに呼び出しが可能です。
以下のサンプルは/HOGE/servlet/helloからforward.jspを呼び出しています。
このときに/WEB-INF/以降のjspであっても結果が表示可能です。
フォワードの場合JSPを表示してもURLは/HOGE/servlet/helloのままです。
リダイレクトは他サーバーでURLを変更して再呼び出しを行います。
下の例ではコメントアウトしてあります。
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class HelloWorld extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
{
String abc = "abc1";
request.setAttribute("abc",abc);
// forward
RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/jsp/forward.jsp");
dispatcher.forward(request,response);
// redirect
// response.sendRedirect("http://*.*.*.*/HOGE/servlet/goodby");
}
}
forward.jspは以下のようです。
<%
String abc = (String)request.getAttribute("abc");
%>
<html>
<body>
<%= abc %>
</body>
</html>
11、servletとJSPのデータの受け渡し
servletからJSPに値を送る際にスコープを使います。
スコープは値の送受信というよりは値を保管する場所を確保するイメージです。
リクエストスコープ、セッションスコープ、アプリケーションスコープがあります。
リクエストスコープは1つのリクエストのみ有効で次のリクエストでは使えません。
セッションスコープは個々のユーザのセッション単位で有効です。
アプリケーションスコープはservletの起動中に全てのユーザで共有します。
いずれの場合もservletを再起動すると値は消えてしまうので、
再起動後も値を保持するのであればファイル保存やデータベースを使用します。
// リクエストスコープ
request.setAttribute("abc",abc);
request.getAttribute("abc");
// セッションスコープ
HttpSession session = request.getSession();
session.setAttribute("abc",abc);
request.getSession().getAttribute("abc");
// アプリケーションスコープ
ServletContext application = this.getServletContext();
application.setAttribute("abc",abc);
request.getServletContext().getAttribute("abc");
スコープからの値の削除も可能です。
// リクエストスコープから値を削除
request.removeAttribute("abc");
同時書きこみなど順番は保障されないため、
同期に関しては自前で書くかデータベースに頼ります。
12、SQLと連携する
SQLサーバーとしてMySQLから派生したMariaDBを使います。
インストールは以下のよう。
apt-get install mariadb-server
JavaとSQLの連携にはJDBCドライバーというものが必要です。
MariaDBのサイトからmariadb-java-client-*.*.*.jarをダウンロード。
SQLサーバーが外部で起動している場合はアクセス権を与える必要があります。
/etc/mysql/my.cnfがMariaDBの設定になります。
次の設定ですべてのIPアドレスからアクセスできます。
bind-address = 0.0.0.0
servletとSQLサーバーが同一であれば127.0.0.1で問題ありません。
以下のサンプルでは"root"というユーザでパスワードは"password"とします。
"*.*.*.*"はSQLサーバーのIPアドレスです。
ポート番号は3306ですが、書かなくてもディフォルトで使用されます。
"testdb"というデータベースを作成します。
"test"というテーブルを作成して"id"に整数を保管できるようにします。
保管した整数を列挙して表示します。
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.sql.*;
public class HelloWorld extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
{
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<body>");
Connection connect = null;
try{
Class.forName("org.mariadb.jdbc.Driver");
connect = DriverManager.getConnection("jdbc:mariadb://*.*.*.*/testdb","root","password");
String sql = "select id from test";
PreparedStatement pStmt = connect.prepareStatement(sql);
ResultSet rs = pStmt.executeQuery();
while(rs.next()){
int id = rs.getInt("id");
out.println(id);
}
}
catch(ClassNotFoundException e){
out.println(e);
}
catch(SQLException e){
out.println(e);
}
finally{
if(connect != null){
try{
connect.close();
}
catch(SQLException e){
out.println(e);
}
}
}
out.println("</body>");
out.println("</html>");
}
}
排他処理についてはGET_LOCKなどを用いて実現可能です。
13、サーバー機能の実装例
例えば、ログイン画面を作る場合を考えます。
ログインに際してはログインのみ管理するservletを作ります。
GETが発生したらJSPへフォワードしてログイン画面を出します。
ログイン画面ではユーザ名、パスワードを入力してPOSTさせます。
パスワードが間違っていればパスワードが違うことを表示するJSPへフォワード。
パスワードが正しければ、次の処理を行うservletにリダイレクトします。
フォワードだとURLが変わらず、リダイレクトは変わることを利用して、
次の状態へ以降することを管理できます。
このように処理ごとに複数のservletに分けると簡単です。
複数のservlet内にSQLを書くとデータベースの仕様変更の対応が困難です。
データベースの処理はクラスでまとめて一元化すると良いでしょう。
また、表示機能に関してはJSPに任せることでページのデザインが容易です。
アクションタグでヘッダー、フッターの管理をするのも良いでしょう。
以下は、ログインだけを行うservletの実装例です。
http://*.*.*.*:8080/abc/servlet/login でログイン画面を表示します。
MariaDBサーバーでユーザ名は"root"、パスワードは"password"です。
データベースは"testdb"で"account"テーブルにid、passを持っています。
ログインが成功するとセッションスコープにlogin_idを記録して、
http://*.*.*.*:8080/abc/servlet/start にリダイレクトします。
本来であればエラー画面なども用意すべきですが簡単のため省略しました。
このサンプルでは次の画面は作成しませんので 404 Not Found が出れば成功です。
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.sql.*;
public class HelloLogin extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
{
RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/jsp/loginpage.jsp");
dispatcher.forward(request,response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
{
String login_id = null;
request.setCharacterEncoding("UTF-8");
String id = request.getParameter("id");
String pass = request.getParameter("pass");
Connection connect = null;
try{
Class.forName("org.mariadb.jdbc.Driver");
connect = DriverManager.getConnection("jdbc:mariadb://127.0.0.1/testdb","root","password");
String sql = "select id,pass from account where id = ? and pass = ?";
PreparedStatement pStmt = connect.prepareStatement(sql);
pStmt.setString(1, id);
pStmt.setString(2, pass);
ResultSet rs = pStmt.executeQuery();
if(rs.next()){
login_id = rs.getString("id");
}
}
catch(ClassNotFoundException e){}
catch(SQLException e){}
finally{
if(connect != null){
try{
connect.close();
}
catch(SQLException e){}
}
}
if(login_id != null){
HttpSession session = request.getSession();
session.setAttribute("login_id", login_id);
response.sendRedirect("/abc/servlet/start");
}
else{
RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/jsp/loginpage.jsp");
dispatcher.forward(request,response);
}
}
}
JSPは /WEB-INF/jsp/loginpage.jps として配置します。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<form action="/abc/servlet/login" method="post">
id:<input type="text" name="id"><br>
pass:<input type="text" name="pass"><br>
<input type="submit" value="login">
</form>
</body>
</html>
14、webサービスのセキュリティ
webサービスで気を付けるべき点についてまとめました。
サーバーそのもののセキュリティについてはここでは考えていません。
webサービスのセキュリティでよくあるのは文字列への埋め込みです。
入力文字列やGETでの入力などタグの無効化などで対策します。
SQL文の書き換えにも十分に注意が必要です。
複雑な入力は弾いて、単純化することがリスクの低下につながりそうです。
◎ XSS
リンクを踏ませてGETでスクリプトを埋め込む。
対策は<>&"' の無効化など。
◎ Script Insertion
投稿そのものにスクリプトを埋め込む
◎ SQL Injection
SQL文に任意の文字を埋め込み処理を変える。
"'の無効化など。
以下の例のpwdはSQL Injectionの典型的なものであるが、
プレースホルダである?を用いて無効化している。
tyy{
String username = "abc"
String pwd = "' or 'a'='a";
String sql = "select * from db where username=? and password=?";
PreparedStatement pStmt = connect.prepareStatement(sql);
pStmt.setString(1, username);
pStmt.setString(2, pwd);
ResultSet rs = stmt.executeQuery();
if(!rs.next()){
throw new SecurityException("username or password incorrect");
}
}
◎ ヌルバイト攻撃
ヌルバイトを用いた拡張子チェックなどの回避。
◎ ディレクトリトラバーサル
../../を使った攻撃。
◎ 変数汚染攻撃
変数を直接書き換える方法。
◎ メール改竄攻撃
メールヘッダを改竄して任意のアドレスにメールを送る方法。
◎ HTTPレスポンス分割攻撃
改行を用いてリクエストを分割する方法。
対策は改行の禁止など。
他にセッションIDの管理にも注意が必要でしょう。
通常セッションIDはwebサーバーが作成しクッキーに保存されますが、
個人的にIDを作成したりGET入力にIDを表示したりするとリスクが上がります。
◎ CSRF
あるサイトの権限を持つ人に他サイトから権限を行使させる方法。
対策はワンタイムチケット法など。
◎ セッションハイジャック
クッキーに保存されたセッションIDを奪う方法。
他に、サーバー内のファイルやコマンドに関する攻撃が考えられるかと思います。
◎ インクルード攻撃
サイト外のファイルを読み込ませる、もしくはサイト内の期待しないファイルを読み込ませる方法。
対策は許可ファイルリストを作成するなど。
◎ eval利用攻撃
evalやexecのような危険な関数は使用用途を十分に考える。
◎ 外部コマンド実行攻撃
外部コマンドの引数にリクエストを混ぜる方法。
◎ ファイルアップロード攻撃
実行ファイルなど不正なファイルをアップロードする方法。