ホームに戻る
 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のような危険な関数は使用用途を十分に考える。
◎ 外部コマンド実行攻撃
外部コマンドの引数にリクエストを混ぜる方法。
◎ ファイルアップロード攻撃
実行ファイルなど不正なファイルをアップロードする方法。

inserted by FC2 system