Week 4
Create a new servlet called "Login". Add this code.
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/login")
public class Login extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setAttribute("pageTitle", "Login");
req.getRequestDispatcher("WEB-INF/login.jsp").forward(req, resp);
}
}
Update the Login link in the top-nav.jspf file.
<a href="${appURL}/login" class="btn btn-outline-dark me-2">Login</a>
Create a new JSP called login.jsp.
Add this code from the Bootstrap Signin Template.
I updated type="email" to type="text" so we can write Java to validate the input.
I updated the paragraph at the end to include a signup link
<main class="form-signin w-100 m-auto">
<form>
<h1 class="h3 mb-3 fw-normal">Please sign in</h1>
<div class="form-floating">
<input type="text" class="form-control" id="floatingInput" placeholder="name@example.com">
<label for="floatingInput">Email address</label>
</div>
<div class="form-floating">
<input type="password" class="form-control" id="floatingPassword" placeholder="Password">
<label for="floatingPassword">Password</label>
</div>
<div class="form-check text-start my-3">
<input class="form-check-input" type="checkbox" value="remember-me" id="flexCheckDefault">
<label class="form-check-label" for="flexCheckDefault">
Remember me for 30 days
</label>
</div>
<button class="btn btn-primary w-100 py-2" type="submit">Sign in</button>
<p class="mt-5 mb-3 text-body-secondary">Don't have an account? <a href="${appURL}/signup">Sign-up</a></p>
</form>
</main>
Create a new CSS file at webapp/styles/login.css
.form-signin {
max-width: 330px;
padding: 1rem;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
In top.jspf, add a <c:if> tag to apply this stylesheet to the Login page.
<c:if test="${pageTitle eq 'Login'}"><link rel="stylesheet" href="${appURL}/styles/login.css"></c:if>
Add form method and action attributes.
<form method="post" action="${appURL}/login">
Add name and id attributes to all form inputs that match the ids. Add a value attribute to the checkbox.
id="email" name="email"
id="password" name="password"
id="rememberMe" name="rememberMe" value="true"
Create a doPost method. Add code similar to the Signup servlet. Note there is only one password field. The agreeToTerms checkbox is now a rememberMe checkbox.
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String email = req.getParameter("email");
String password = req.getParameter("password");
String[] rememberMe = req.getParameterValues("rememberMe");
req.setAttribute("email", email);
req.setAttribute("password", password);
req.setAttribute("rememberMe", (rememberMe != null && rememberMe[0].equals("true")) ? "true" : "");
req.setAttribute("pageTitle", "Login");
req.getRequestDispatcher("WEB-INF/login.jsp").forward(req, resp);
}
Add value attributes to the email and password form inputs. Add an expression to the checkbox to check it.
value="${email}"
value="${password}"
${rememberMe eq "true" ? "checked" : ""}
The last expression could also be written like this:
<c:if test="${rememberMe == 'true'}">checked</c:if>
Add a c:if tag before the form tag to display any error message.
<c:if test="${not empty loginFail}">
<div class="alert alert-warning mb-2" role="alert">${loginFail}</div>
</c:if>
In the servlet, get the User object from the database and validate that the email and password match.
If they match, create an HttpSession, set the user as a session attribute, set a flash message session attribute greeting the user, and redirect the user to any page, like the home page.
User user = null;
try {
user = UserDAO.get(email);
} catch (RuntimeException e) {
req.setAttribute("loginFail", "An error occurred."); // Use e.getMessage() to see the SQLException
}
if (user == null) {
// No user found that matches the email
req.setAttribute("loginFail", "No user found with that email address. <a href=\"signup\">Sign-up</a>"); // For security, it might be better to just say "No user found".
} else {
boolean passwordMatches = false;
try {
passwordMatches = BCrypt.checkpw(password, String.valueOf(user.getPassword()));
} catch (Exception e) {
req.setAttribute("loginFail", "An error occurred."); // Use e.getMessage() to see the NoSuchAlgorithmException or InvalidKeySpecException
}
// No user found that matches the password
if (!passwordMatches) {
req.setAttribute("loginFail", "The password you entered is incorrect."); // For security, it might be better to just say "No user found".
} else {
// Successful login
user.setPassword(null); // Remove the password before setting the User object as a session attribute
HttpSession session = req.getSession(); // Get existing HttSession object
session.invalidate(); // Remove any existing session attributes
session = req.getSession(); // Create new HttpSession
session.setAttribute("activeUser", user);
session.setAttribute("flashMessageSuccess", String.format("Welcome back%s!", (user.getFirstName() != null && !user.getFirstName().equals("") ? " " + user.getFirstName() : "")));
resp.sendRedirect(resp.encodeRedirectURL(req.getContextPath() + "/")); // Redirects to the home page
return;
}
}
The setMaxInactiveInterval() method can be used with the session object to increase the session timeout.
This method sets the value, in seconds, that the session will remain open between client requests.
For a 30-day duration, the interval must be 2592000. (30 days * 24 hrs/day * 60 min/hr * 60 secs/min)
It is important to set this value before any data is stored in the session.
This feature will work on Azure, not localhost, because of how often the server needs to restart.
HttpSession session = req.getSession(); // Get existing HttSession object
session.invalidate(); // Remove any existing session attributes
session = req.getSession(); // Create new HttpSession
if(rememberMe != null && !rememberMe[0].equals("true")){
session.setMaxInactiveInterval(2592000); // Set session timeout to 30 days (in seconds)
}
session.setAttribute("activeUser", user);
String firstName = user.getFirstName() != null ? " " + user.getFirstName() : "";
session.setAttribute("flashMessageSuccess", String.format("Welcome back%s!", firstName));
Write JavaScript code to remember a user's email address the next time they try to sign in.
When the form is submitted, if "Remember Me" is checked, the email is stored in localStorage
. If not, the email is removed.
When the page loads, it checks if an email is stored in localStorage
and fills in the email field if found. If an email is found in localStorage
, the "Remember Me" checkbox automatically checks.
document.addEventListener("DOMContentLoaded", function () {
const emailInput = document.getElementById("email");
const rememberMeCheckbox = document.getElementById("rememberMe");
// Load saved email if it exists
const savedEmail = localStorage.getItem("rememberedEmail");
if (savedEmail) {
emailInput.value = savedEmail;
rememberMeCheckbox.checked = true;
}
document.getElementById("loginForm").addEventListener("submit", function (event) {
if (rememberMeCheckbox.checked) {
localStorage.setItem("rememberedEmail", emailInput.value);
} else {
localStorage.removeItem("rememberedEmail");
}
});
});
In top.jspf, add a <c:if> tag to apply this JS to the Login page.
<c:if test="${pageTitle eq 'Login'}"><script src="${appURL}/scripts/remember-me.js"></script></c:if>
Create a new servlet called "Signout". Add this code.
No return statement is needed after sendRedirect because that is the last line.
Update the Sign out link in the top-nav.jspf file.
<a href="${appURL}/signout" class="btn btn-outline-secondary">Sign out</a>
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/signout")
public class Signout extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession(); // Get existing session
session.invalidate(); // Remove all existing session attributes
session = req.getSession(); // Create new session
session.setAttribute("flashMessageWarning", "You are logged out. See you next time!");
resp.sendRedirect(resp.encodeRedirectURL(req.getContextPath() + "/")); // Redirects to the home page
}
}
HttpSession session = req.getSession();
User userFromSession = (User)session.getAttribute("activeUser");
if(userFromSession == null || !userFromSession.getStatus().equalsIgnoreCase("active") || !userFromSession.getPrivileges().equalsIgnoreCase("admin")) {
session.setAttribute("flashMessageWarning", "You must be logged in to view this content");
resp.sendRedirect("login");
return;
} else {
// Code that already exists in the doGet method.
}
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="${appURL}">Home</a>
</li>
<c:if test="${not empty sessionScope.activeUser && sessionScope.activeUser.privileges eq 'admin' && sessionScope.activeUser.status eq 'active'}">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown" aria-expanded="false">Admin</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="${appURL}/users">Users</a></li>
</ul>
</li>
</c:if>
</ul>
if(!user.getStatus().equalsIgnoreCase("active")) {
req.setAttribute("loginFail", "Your account is locked or inactive. Please reset your password.");
req.setAttribute("pageTitle", "Login");
req.getRequestDispatcher("WEB-INF/login.jsp").forward(req, resp);
return;
}