TechHotoke’s diary

日々の学びについて記事としてまとめてます。

Javaでログイン機能を実装してみた。

f:id:TechHotoke:20211216134635p:plain

目的

Springなどのフレームワークを使用してログイン機能を実装する機会が多く、中身がよく分かっていなかったので学習の一環として自力で実装すること

前提

  • JDKのインストールが完了していること
  • Tomcatのインストールが完了していること
  • Eclipseのインストール・セットアップが完了していること
  • JavaCSS、HTML、JSP/Servletの基本的な知識があること

環境

実装

プロジェクト作成~ログイン遷移

まずプロジェクトを作成し、それっぽいログイン画面を作成します。

f:id:TechHotoke:20211121222309p:plain

  • ログイン画面

f:id:TechHotoke:20211121222011p:plain

動的webプロジェクトを作成し、index.jspファイルを作成します。

index.jsp

<%@ page language="java" contentType="text/html;
charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>ログイン画面</title>
<link rel="stylesheet" type="text/css" href="css/login.css">
<link
    href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css"
    rel="stylesheet"
    integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1"
    crossorigin="anonymous">
</head>
<body>
    <div class="container">
        <div class="card card-container">
            <img id="profile-img" class="profile-img-card"
                src="//ssl.gstatic.com/accounts/ui/avatar_2x.png" />
            <p id="profile-name" class="profile-name-card"></p>
            <form class="form-signin" method="post" action="LoginServlet">
                <span id="reauth-email" class="reauth-email"></span> 
                <input
                    type="email" id="inputEmail" class="form-control"
                    placeholder="Email address" name="user-id" required autofocus> 
                <input
                    type="password" id="inputPassword" class="form-control"
                    placeholder="Password" name="password" required>
                <button class="btn btn-lg btn-primary btn-block btn-signin"
                    type="submit" value="signin">ログイン</button>
            </form>
            <!-- /form -->
            <a href="#" class="forgot-password"> パスワードを忘れた方はこちら </a>
        </div>
        <!-- /card-container -->
    </div>
    <!-- /container -->
    <script
        src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW"
        crossorigin="anonymous"></script>
</body>
</html>

続いて、Login処理を行うLoginServletクラスを作成します。

MVC構成で作成するので、controllerパッケージを作成し、その直下に作成します。

LoginServlet

package controller;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class LoginServlet
 */
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
       
    /**
     * @see HttpServlet#HttpServlet()
     */
    public LoginServlet() {
        super();
    }

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //JSPから、ユーザーIDとパスワードの取得
        String user_id = request.getParameter("user-id");
        String password = request.getParameter("password");

        //ログ
        System.out.println("ユーザーID:" + user_id);
        System.out.println("パスワード:" + password);
        
        //ログイン完了ページへ転送
        request.getRequestDispatcher("/WEB-INF/top.jsp")
        .forward(request, response);

        return;
    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

}
  • doGetメソッドに記載。

  • ユーザーID、パスワードのinputタグ内のname属性値が取得されるのでgetParameterの引数にname属性の値を記述。

  • ログイン後遷移するtop.jspにforwardする。

次にtop.jspを作成します。

top.jsp

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>ログインに成功しました。</h1>
</body>
</html>
  • top.jspはWEB-INF配下に作成します。(WEB-INF配下には直接アクセスすることが出来ず、ログイン処理が必要なファイルはこちらに配置して隠蔽するのが良いので)

遷移が正常に行われるか確認します。

f:id:TechHotoke:20211122063241p:plain

コンソール

f:id:TechHotoke:20211122063339p:plain

データ連携〜ログインロジックの作成

それでは続いてデータの連携を行なっていきます。

今回、DB接続は行わずデータのやり取りはSessionを利用します。

まずはデータを格納するBeanクラスを作成します。

Javaソースの直下にdomainパッケージを作成し、UserBeanクラスを作成します。

UserBean

package domain;

import java.io.Serializable;

public class UserBean implements Serializable {

    private static final long serialVersionUID = 1L;
    
    private String user_id;
    private String user_name;
    private String password;
    private String phonenumber;
    private String address;
    private String role; // 権限
    private int status;// OK:0,未登録:1,パスワード不一致:2

        // setter/getterは省略

}

次に、Daoを作成します。 Javaソース直下にDaoパッケージを作成し、UserDaoクラスを作成します。

それらしいデータをBeanクラスにセットします。

※住所などの個人情報はジェネレータを使用しています。

package dao;

import domain.UserBean;

public class UserDao {
    /**
     * ユーザーIDからUserBeanを取得
     * 
     * @param user_id ユーザーID
     * @return UserBean ユーザーBean
     */
    public UserBean getUser(String user_id) {
        UserBean userBean = new UserBean();
        
        switch (user_id) {
        case "001":// user_id=001の場合
            userBean.setUser_id(user_id);
            userBean.setUser_name("前田");
            userBean.setPassword("pass");
            userBean.setPhonenumber("090-0000-1111");
            userBean.setAddress("佐賀県杵島郡大町町福母3-2-2 パークハイツ福母 9F");
            userBean.setRole("member");
            break;
        case "002":// user_id=002の場合
            userBean.setUser_id(user_id);
            userBean.setUser_name("鈴木");
            userBean.setPassword("pass2");
            userBean.setPhonenumber("090-5561-1174 ");
            userBean.setAddress("三重県四日市市前田町9-1-9");
            userBean.setRole("member");
            break;
        case "100":// user_id=100の場合
            userBean.setUser_id(user_id);
            userBean.setUser_name("佐藤");
            userBean.setPassword("pass3");
            userBean.setPhonenumber("070-2968-0125 ");
            userBean.setAddress("香川県綾歌郡綾川町萱原8-3-3");
            userBean.setRole("admin");
            break;
        default:// ユーザーIDが存在しない場合
            userBean.setUser_id(user_id);
            userBean.setStatus(1);
        }

        return userBean;
    }

}

続いて、ログインロジックを実装します。

package model;

import dao.UserDao;
import domain.UserBean;

public class LoginService {
    /**
     * @param user_id  ユーザーID
     * @param password パスワード
     * @return UserBean
     */
    public UserBean checkLogin(String user_id, String password) {
        UserDao userDao = new UserDao();
        UserBean userBean = new UserBean();

        // UserBeanの取得
        userBean = userDao.getUser(user_id);

        // ID存在チェック
        // status: 0:OK, 1:不在, 2:NG
        if (userBean.getStatus() == 1) {
            return userBean;
        } else {
            // パスワードのチェック
            if (userBean.getPassword().equals(password)) {
                userBean.setStatus(0);
            } else {
                userBean.setStatus(2); 
            }
            return userBean;
        }
    }
}

そして、LoginServletに下記のようにチェックロジックを追加します。

     LoginService loginService = new LoginService();

        // UserBeanの取得
        UserBean userBean = loginService.checkLogin(user_id, password);

        // ログイン失敗の場合は、失敗ページへ
        if (userBean.getStatus() != 0) {
            // 失敗ページの転送
            request.getRequestDispatcher("/WEB-INF/login-failed.jsp").forward(request, response);

            return;
        }

さらに、ログイン失敗後の遷移画面も作っていきます。

LoginServletのパスにlogin-failed.jspという名称を入れているのでそこに合わせたファイル名のJSPを作成します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1 style="color: red;">Sign in failed</h1>
</body>
</html>

設定したパスワード・ユーザーIDでログインしたときは成功画面へ、それ以外のパスワード・ユーザーIDでログインしたときは失敗画面へ遷移すればOKです。

SessionにUserBeanのデータを格納し画面に表示する

続いて、Userの情報を画面に表示させていきます。

まずは、LoginServletに以下を追記します。

    //UserBeanをセッションにセット
        HttpSession session = request.getSession(true);
        session.setAttribute("userBean", userBean);

これで、userBeanオブジェクトとuserBeanパラメータが紐づきました。

続いて画面上にデータを表示させていきます。 今回はJSTLを使用します。

※EL式を利用する場合はXSSクロスサイトスクリプティング)の脆弱性に注意してください。

こちらのサイトからzipファイルをダウンロードし、WEB-INF配下のlibフォルダ直下に配置します。

f:id:TechHotoke:20211123001216p:plain

タグリブを使用する場合は、使用するJSPファイルに宣言を追加してください。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

トップ画面に以下の記述を追加し、ログインした際にユーザー情報が反映されていることを確認します。

          <div>
        ユーザーID:
        <c:out value="${userBean.user_id}" />
    </div>
        <div>
        ユーザー名:
        <c:out value="${userBean.user_name}" />
    </div>

f:id:TechHotoke:20211123002656p:plain

login-failed.jspにも似たような形でデータを表示させます。

こちらはステータスに応じてメッセージを分岐させています。

<body>
    <h1 style="color: red;">Sign in failed</h1>
    <!-- 判定フラグ -->
    <c:if test="${userBean.status == 1}" var="flg" />

    <%--IDが存在しない時(ステータス1の場合) --%>
    <c:if test="${flg}">
        <p>
            ユーザーID: <strong><c:out value="${userBean.user_id}" /></strong>
            は存在しません。<br />
        </p>
    </c:if>

    <%-- パスワード間違いの時(ステータス2の場合) --%>
    <c:if test="${!flg}">
        <p>
            <strong>パスワードが間違っています。</strong><br /> 
        </p>
    </c:if>

    <a href="javascript:history.back();">戻る</a>

</body>

これでSessionのデータを画面に表示させる実装が完了しました。


[余談] JSTLを使用することで悪意のあるスクリプトが埋め込まれた時にタグを反映させないc:outなどを使用することで対策が可能です。 EL式ではタグがそのまま反映されてしまうため、XSS攻撃に対して脆弱性があると言われています。


フィルターの実装

フィルターは共通処理を行う仕組み。 JSPサーブレットを呼び出す前後で⾃動的に呼び出されるので認証・認可の実装などに用いられます。

それではこれまでの流れに沿ってtop.jspから会員のみ閲覧可能なalbum.jspを作成していきます。 (jspファイルなどの実装工程は冗長なので省略。GitHubのソースをご参照ください)

変更点としては三つで

  • top.jspからalbum.jspに遷移するリンクを作成したこと

  • album.jspを作成したこと

  • AlbumServletを作成したこと

です。

それではフィルターの実装を行なっていきます。

f:id:TechHotoke:20211123011128p:plain

画像の項目を選択し、LoginFilterクラスを作成。

以下のようにFilterの設定を行います。urlPatternsのパスにフィルターの対象とするServletを記述します。

/* フィルターの設定 */
@WebFilter(filterName = "LoginFilter", urlPatterns = { "/AlbumServlet" })
public class LoginFilter implements Filter {
//省略
}

次にdoFilterメソッドに処理を記述します。

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // HttpServletに変換
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        // セッションから情報取得
        HttpSession session = req.getSession();
        UserBean userBean = (UserBean) session.getAttribute("userBean");

        // 未ログインの場合、ログイン画面にリダイレクト
        if (session == null || userBean == null) {
            System.out.println("未ログイン");
            res.sendRedirect("index.jsp");
            return;
        }

        // pass the request along the filter chain
        chain.doFilter(request, response);
    }

ここまでで、AlbumSevletに直接アクセスしようとするとログイン画面にリダイレクトされて、コンソールに「未ログイン」の文字が表示されればOKです。

ログアウト処理の実装

Sessionが残っている場合、AlbumServletにログイン処理を行わなくともアクセス出来てしまう状態なのでSessionを破棄するように実装していきます。

LogoutServletを作成し、以下のように処理を記述します。

 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
          //セッション取得
        HttpSession session = request.getSession();

        //セッション破棄
        session.invalidate();

        //ページ転送
        request.getRequestDispatcher("index.jsp")
        .forward(request, response);
    }

top.jspにログアウト用のリンクを作成し、

  <p class="logout">
         <a href="LogoutServlet">Logout</a>
     </p>

ログイン→ログアウトリンクを押下→AlbumServletに直接アクセス

の手順を踏んだ時にログイン画面に遷移されればOKです。


以上長くなりましたがこれで完了です。気が向いた頃に認可処理も実装してみようと思います。 ありがとうございました。

参考

github.com