こんにちは。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();
}
}
}
コメント