【1.4 自作Ecsite連載】MVCモデルでログイン処理を記述する

zuka

こんにちは。zuka(@beginaid)です。

この記事は,Ecsiteを自作するシリーズになります。今回はログイン処理を記述します。

その他のシリーズ記事は以下の目次をご覧ください。

目次

完成品デモ

ログインが成功した場合

パスワードが間違えていた場合

直接トップページにアクセスした場合

全体フロー

この記事では,以下のようなログイン処理のフローを目指します。

流れ図

説明

基本的に,直接jspファイルにはアクセスできないような設計にしています。そのため,全てのjspファイルはWEB-INFディレクトリ下に配置しています。WEB-INF下に配置するとforwardは動作しますが,redirectが通らなくなります。そこで,jspファイルにredirectしたい場合は一回何らかのサーブレットを経由するようにします。今回でいえばIndexサーブレットがindex.jspへのリダイレクトを橋渡しする役割を果たします。

そもそも,なぜログイン処理でリダイレクトが必要なのかというと,URLを変えるためです。フォワードのみでログイン処理を記述してしまうと,ログインしていても,していなくてもブラウザには同じURLが表示されますから,ユーザは混乱してしまいます。そこで,ログイン後のURLを明示的に変えるためにリダイレクト処理を加えます。なお,リダイレクト後のURLを小文字構成にするために,橋渡しをするサーブレットの@WebServletアノテーションは”/index”のように小文字で指定します。

今回のログイン処理はMVCモデルに則っています。計算処理などを担当するModelはJavaのクラス,ページ表示を担当するViewはjsp,橋渡し役をするControllerはサーブレットによって実現させています。さらに,Modelの中でも特にデータベースとのやり取りを行うクラスをDAO(Data Access Object)と呼びます。今回はユーザ情報を参照するUser.DAOを作成しています。

具体的には,以下のようになっています。

  • Model
    • LoginLogic
    • UserDAO
  • View
    • login.jsp
    • index.jsp
  • Controller
    • Welcome
    • Login
    • Index

さらに,User情報を表すUserクラス,ログイン情報を表すLoginクラスを定義します。これらは本来,単純なコンストラクタとgetterからなるValue Object(VO)と呼ばれるクラスですが,今回はsetterも定義してModelと同一視することにします。

実装

以下では実装を確認していきます。Model,View,Controllerに分けてお伝えしていきます。

Model

まずは,情報を保持するクラスであるUserとLoginです。

package com.cod_aid.model;

public class User {
	String userId;
	String userName;
	String email;
	String password;
	String role;
	boolean isDelte;

	public User() {
	}

	public User(String userId, String userName, String password, String email, String role, boolean isDelte) {
		this.userId = userId;
		this.userName = userName;
		this.password = password;
		this.email = email;
		this.role = role;
		this.isDelte = isDelte;
	}

	public String getUserId() {
		return userId;
	}

	public void setUserId(String userId) {
		this.userId = userId;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getRole() {
		return role;
	}

	public void setRole(String role) {
		this.role = role;
	}

	public boolean isDelte() {
		return isDelte;
	}

	public void setDelte(boolean isDelte) {
		this.isDelte = isDelte;
	}
}
package com.cod_aid.model;

public class Login {
	private String email;
	private String password;

	public Login() {
	}

	public Login(String email, String password) {
		this.email = email;
		this.password = password;
	}

	public String getEmail() {
		return email;
	}

	public String getPassword() {
		return password;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public void setPassword(String password) {
		this.password = password;
	}

}

続いて,データベースとのやりとりを記述するUserDAOです。UserDAOはデータベースとのコネクションを確立するBaseDAOを継承するという形で設計します。

package com.cod_aid.dao;

import java.sql.Connection;
import java.sql.SQLException;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

abstract public class BaseDAO {
	public Connection connect() throws SQLException, NamingException {
		String localName = "java:comp/env/jdbc/ecsite";
		Context context = new InitialContext();
		DataSource ds = (DataSource) context.lookup(localName);
		Connection con = ds.getConnection();
		return con;
	}

	public void disconnect(Connection con) throws SQLException {
		if (con != null) {
			con.close();
		}
	}
}
package com.cod_aid.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.naming.NamingException;

import com.cod_aid.model.Login;
import com.cod_aid.model.User;

public class UserDAO extends BaseDAO {
	private final String SQL_GET_USER = "SELECT user_id, user_name, password, email, role, is_delete FROM users WHERE email = ? AND password = ?";

	public User findByLogin(Connection con, Login login) throws NamingException, SQLException {
		PreparedStatement ps = con.prepareStatement(SQL_GET_USER);
		ps.setString(1, login.getEmail());
		ps.setString(2, login.getPassword());

		ResultSet rs = ps.executeQuery();

		User user = null;
		if (rs.next()) {
			String userId = rs.getString("user_id");
			String userName = rs.getString("user_name");
			String password = rs.getString("password");
			String email = rs.getString("email");
			String role = rs.getString("role");
			boolean isDelete = rs.getBoolean("is_delete");
			user = new User(userId, userName, password, email, role, isDelete);
		}
		return user;
	}
}

なお,データベース自体は以下の記事で作成しています。

続いて,controllerから指令を受けてDAOを呼び出すLoginLogicです。冒頭で示した全体のフローではexecuteメソッドの概要は記述していませんでしたが,ログイン画面で入力されたメールアドレスとパスワードの組み合わせがデータベースから見つかればtrue,見つからなければfalseを返すようなメソッドです。

package com.cod_aid.model;

import java.sql.Connection;
import java.sql.SQLException;

import javax.naming.NamingException;

import com.cod_aid.dao.UserDAO;

public class LoginLogic {
	public boolean execute(Login login) throws NamingException, SQLException {
		UserDAO dao = new UserDAO();
		Connection con = dao.connect();
		User user = dao.findByLogin(con, login);
		return user != null;
	}

	public User findUser(Login login) throws NamingException, SQLException {
		UserDAO dao = new UserDAO();
		Connection con = dao.connect();
		User user = dao.findByLogin(con, login);
		return user;
	}
}

View

まずは,ログインページを表示するindex.jspですが,以下の記事で詳しく説明しています。

続いて,ログイン後のトップページですが,以下の記事で詳しく説明しています。

Controller

まずは,ブラウザからのGETリクエストを処理するWelcomeサーブレットです。

package com.cod_aid.controller;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/Welcome")
public class Welcome extends HttpServlet {
	private static final long serialVersionUID = 1L;

	public Welcome() {
		super();
	}

	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/login.jsp");
		dispatcher.forward(request, response);
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doGet(request, response);
	}

}

続いて,データベースとの参照を命令するLoginServletです。不正アクセスの場合はエラーメッセージを格納してWelcomeサーブレットにリダイレクトしています。

package com.cod_aid.controller;

import java.io.IOException;
import java.sql.SQLException;

import javax.naming.NamingException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.cod_aid.model.Login;
import com.cod_aid.model.LoginLogic;
import com.cod_aid.model.User;

@WebServlet("/Login")
public class Login extends HttpServlet {
	private static final long serialVersionUID = 1L;

	public LoginServlet() {
		super();
	}

	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		HttpSession session = request.getSession();
		User user = (User) session.getAttribute("user");
		if (user == null) {
			session.setAttribute("errorIllegalAccess", "errorIllegalAccess");
			response.sendRedirect("Welcome");
		} else {
			RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/index.jsp");
			dispatcher.forward(request, response);
		}
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		String email = request.getParameter("inputEmail");
		String password = request.getParameter("inputPassword");
		String remember = request.getParameter("remember");

		Login login = new Login(email, password);
		LoginLogic bo = new LoginLogic();
		User user = new User();

		boolean result = false;
		try {
			user = bo.findUser(login);
			result = bo.execute(login);
		} catch (NamingException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		HttpSession session = request.getSession();

		if (result) {
			session.setAttribute("user", user);
			if (remember != null) {
				session.setAttribute("email", user.getEmail());
				session.setAttribute("password", user.getPassword());
			}
			response.sendRedirect("index");
		} else {
			session.setAttribute("errorNotFoundUser", "errorNotFoundUser");
			response.sendRedirect("Welcome");
		}
	}
}

最後に,トップページへのリダイレクト処理の橋渡しを行うIndexサーブレットです。@WebServletアノテーションはindexと設定しておくことでURLらしくなります。こちらも,不正アクセスの場合はエラーメッセージを格納してWelcomeサーブレットにリダイレクトしています。

package com.cod_aid.controller;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/index")
public class Index extends HttpServlet {
	private static final long serialVersionUID = 1L;

	public Index() {
		super();
	}

	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		HttpSession session = request.getSession();
		User user = (User) session.getAttribute("user");
		if (user == null) {
			session.setAttribute("errorIllegalAccess", "errorIllegalAccess");
			response.sendRedirect("Welcome");
		} else {
			RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/index.jsp");
			dispatcher.forward(request, response);
		}
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doGet(request, response);
	}
}

JUnit単体テスト

JUnitを用いてUserDAOの単体テストを行う場合,サーブレットコンテナ(tomcat)は起動しないため,データベースとのコネクション確立を行う方法が変わります。詳しくは以下で説明しています。

以下では,実装のみお伝えすることにします。

package com.cod_aid.dao;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;

import java.sql.Connection;
import java.sql.SQLException;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import com.cod_aid.model.Login;
import com.cod_aid.model.User;
import com.mysql.cj.jdbc.MysqlDataSource;

class UserDAOTest {
	private Login login1;
	private Login login2;
	private UserDAO dao = new UserDAO();

	@BeforeAll
	static void setUpContext() throws Exception {
		try {
			System.setProperty(Context.INITIAL_CONTEXT_FACTORY,
					"org.apache.naming.java.javaURLContextFactory");
			System.setProperty(Context.URL_PKG_PREFIXES,
					"org.apache.naming");
			InitialContext ic = new InitialContext();

			ic.createSubcontext("java:");
			ic.createSubcontext("java:comp");
			ic.createSubcontext("java:comp/env");
			ic.createSubcontext("java:comp/env/jdbc");

			MysqlDataSource ds = new MysqlDataSource();
			ds.setUser("ecsite");
			ds.setPassword("password");
			ds.setDatabaseName("ecsite");
			ds.setServerName("localhost");
			ds.setPortNumber(3306);

			ic.bind("java:comp/env/jdbc/ecsite", ds);
		} catch (NamingException ex) {
			ex.printStackTrace();
		}

	}

	@BeforeEach
	void setUp() throws Exception {
		login1 = new Login("customer1@abc.de.f", "password");
		login2 = new Login("customer1@abc.de.f", "pass");
	}

	@Test
	void testFindByLogin1() {
		// 見つかる場合のテスト
		User result1;
		try (Connection con1 = dao.connect()) {
			result1 = dao.findByLogin(con1, login1);
			assertThat(result1.getUserId(), is("C0000001"));
			assertThat(result1.getUserName(), is("customer1"));
			assertThat(result1.getPassword(), is("password"));
			assertThat(result1.getEmail(), is("customer1@abc.de.f"));
			assertThat(result1.getRole(), is("user"));
			assertThat(result1.isDelte(), is(false));
		} catch (NamingException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	@Test
	public void testFindByLogic2() {
		// 見つからない場合のテスト
		User result2;
		try (Connection con2 = dao.connect()) {
			result2 = dao.findByLogin(con2, login2);
			assertThat(result2, nullValue());
		} catch (NamingException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
}

LoginLogicのテストは簡単です。

package com.cod_aid.model;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;

import java.sql.SQLException;

import javax.naming.NamingException;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class LoginLogicTest {
	private Login login1;
	private Login login2;
	LoginLogic bo = new LoginLogic();

	@BeforeEach
	void setUp() throws Exception {
		login1 = new Login("customer1@abc.de.f", "password");
		login2 = new Login("customer1@abc.de.f", "pass");
	}

	@Test
	void testExecute1() {
		// 見つかる場合のテスト
		try {
			assertThat(bo.execute(login1), is(true));
		} catch (NamingException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	@Test
	void testExecute2() {
		// 見つからない場合のテスト
		try {
			assertThat(bo.execute(login2), is(false));
		} catch (NamingException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
}
よかったらシェアしてね!

コメント

コメントする

目次
閉じる