Week 3
In the Signup servlet, create a doPost method to get the parameters from the form submission.
Immediately set those values as attributes. Note the ternary operator being used to set the terms attribute to "agree" only if the checkbox was checked.
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String email = req.getParameter("email");
String password1 = req.getParameter("password1");
String password2 = req.getParameter("password2");
String[] terms = req.getParameterValues("terms");
req.setAttribute("email", email);
req.setAttribute("password1", password1);
req.setAttribute("password2", password2);
req.setAttribute("terms", (terms != null && terms[0].equals("agree")) ? "agree" : "");
req.setAttribute("pageTitle", "Sign up for an account");
req.getRequestDispatcher("WEB-INF/signup.jsp").forward(req, resp);
}
Use expressions in value attributes to get attributes from the servletvalue="${email}"
value="${password1}"
value="${password2}"
Use a c:if tag inside in the checkbox input to re-select it.<c:if test="${terms eq 'agree'}">checked</c:if>
Run Tomcat. When you enter values and press submit, the values should remain in the form.
Create a shared/Validators class with code we created in Java 2.
Add a method to validate a strong password.
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Validators {
public static boolean isStrongPassword(String password) {
// Password requires 8 characters with at least 3 out 4 (uppercase letter, lowercase letter, number, special character ~`!@#$%^&*()_-+={}[]|\:;"'<>,.?/
Pattern pattern = Pattern.compile("^(?:(?=.*\\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\\d)(?=.*[^A-Za-z0-9])(?=.*[a-z])|(?=.*[^A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z])|(?=.*\\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9]))(?!.*(.)\\1{2,})[A-Za-z0-9~`!\\@#\\$%\\^&*()_\\-+={}\\[\\]\\|\\\\:;\"'<>,.?\\/]{8,128}$");
Matcher matcher = pattern.matcher(password);
return matcher.matches();
}
public static boolean isANumber(String str) {
try {
Double.parseDouble(str);
return true;
} catch (NumberFormatException e) {
return false;
}
}
public static boolean isValidEmail(String email) {
String regex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(email);
return matcher.matches();
}
public static boolean isValidState(String state) {
state = state.toUpperCase();
String regex = "^(AL|AK|AZ|AR|CA|CO|CT|DE|DC|FL|GA|HI|ID|IL|IN|IA|KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|OH|OK|OR|PA|RI|SC|TN|TX|UT|VT|VA|WA|WV|WI|WY)$";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(state);
return matcher.matches();
}
public static boolean isValidZip(String zip) {
String regex = "^\\d{5}(-\\d{4})?$";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(zip);
return matcher.matches();
}
public static boolean isValidCountry(String country) {
country = country.toUpperCase();
String regex = "^(AF|AX|AL|DZ|AS|AD|AO|AI|AQ|AG|AR|AM|AW|AU|AT|AZ|BS|BH|BD|BB|BY|BE|BZ|BJ|BM|BT|BO|BQ|BA|BW|BV|BR|IO|BN|BG|BF|BI|KH|CM|CA|CV|KY|CF|TD|CL|CN|CX|CC|CO|KM|CG|CD|CK|CR|CI|HR|CU|CW|CY|CZ|DK|DJ|DM|DO|EC|EG|SV|GQ|ER|EE|ET|FK|FO|FJ|FI|FR|GF|PF|TF|GA|GM|GE|DE|GH|GI|GR|GL|GD|GP|GU|GT|GG|GN|GW|GY|HT|HM|VA|HN|HK|HU|IS|IN|ID|IR|IQ|IE|IM|IL|IT|JM|JP|JE|JO|KZ|KE|KI|KP|KR|KW|KG|LA|LV|LB|LS|LR|LY|LI|LT|LU|MO|MK|MG|MW|MY|MV|ML|MT|MH|MQ|MR|MU|YT|MX|FM|MD|MC|MN|ME|MS|MA|MZ|MM|NA|NR|NP|NL|NC|NZ|NI|NE|NG|NU|NF|MP|NO|OM|PK|PW|PS|PA|PG|PY|PE|PH|PN|PL|PT|PR|QA|RE|RO|RU|RW|BL|SH|KN|LC|MF|PM|VC|WS|SM|ST|SA|SN|RS|SC|SL|SG|SX|SK|SI|SB|SO|ZA|GS|SS|ES|LK|SD|SR|SJ|SZ|SE|CH|SY|TW|TJ|TZ|TH|TL|TG|TK|TO|TT|TN|TR|TM|TC|TV|UG|UA|AE|GB|US|UM|UY|UZ|VU|VE|VN|VG|VI|WF|EH|YE|ZM|ZW|AFG|ALB|DZA|ASM|AND|AGO|AIA|ATA|ATG|ARG|ARM|ABW|AUS|AUT|AZE|BHS|BHR|BGD|BRB|BLR|BEL|BLZ|BEN|BMU|BTN|BOL|BIH|BWA|BVT|BRA|IOT|VGB|BRN|BGR|BFA|BDI|KHM|CMR|CAN|CPV|CYM|CAF|TCD|CHL|CHN|CXR|CCK|COL|COM|COD|COG|COK|CRI|CIV|CUB|CYP|CZE|DNK|DJI|DMA|DOM|ECU|EGY|SLV|GNQ|ERI|EST|ETH|FRO|FLK|FJI|FIN|FRA|GUF|PYF|ATF|GAB|GMB|GEO|DEU|GHA|GIB|GRC|GRL|GRD|GLP|GUM|GTM|GIN|GNB|GUY|HTI|HMD|VAT|HND|HKG|HRV|HUN|ISL|IND|IDN|IRN|IRQ|IRL|ISR|ITA|JAM|JPN|JOR|KAZ|KEN|KIR|PRK|KOR|KWT|KGZ|LAO|LVA|LBN|LSO|LBR|LBY|LIE|LTU|LUX|MAC|MKD|MDG|MWI|MYS|MDV|MLI|MLT|MHL|MTQ|MRT|MUS|MYT|MEX|FSM|MDA|MCO|MNG|MSR|MAR|MOZ|MMR|NAM|NRU|NPL|ANT|NLD|NCL|NZL|NIC|NER|NGA|NIU|NFK|MNP|NOR|OMN|PAK|PLW|PSE|PAN|PNG|PRY|PER|PHL|PCN|POL|PRT|PRI|QAT|REU|ROU|RUS|RWA|SHN|KNA|LCA|SPM|VCT|WSM|SMR|STP|SAU|SEN|SCG|SYC|SLE|SGP|SVK|SVN|SLB|SOM|ZAF|SGS|ESP|LKA|SDN|SUR|SJM|SWZ|SWE|CHE|SYR|TWN|TJK|TZA|THA|TLS|TGO|TKL|TON|TTO|TUN|TUR|TKM|TCA|TUV|VIR|UGA|UKR|ARE|GBR|UMI|USA|URY|UZB|VUT|VEN|VNM|WLF|ESH|YEM|ZMB|ZWE)$";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(country);
return matcher.matches();
}
}
public void setPassword(char[] password) {
String passwordStr = String.valueOf(password);
String regex = "^" +
"(?=.*[0-9])" + // a digit must occur at least once
"(?=.*[a-z])" + // a lower case letter must occur at least once
"(?=.*[A-Z])" + // an upper case letter must occur at least once
// "(?=.*[!@#$%^&*()+-=])" + // a special character must occur at least once
// "(?=\\S+$)" + // no whitespace allowed in the entire string
".{8,}" + // anything, at least eight characters
"$";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(passwordStr);
if(!matcher.matches()) {
throw new IllegalArgumentException("Password must contain a minimum of 8 characters, 1 digit, 1 lowercase letter, and 1 uppercase letter.");
}
this.password = password;
}
Set the email and password only if they are valid.
The setPassword method will allow null values because we will set the password to null after it has been validated.
public void setEmail(String email) {
if (!Validators.isValidEmail(email)) {
throw new IllegalArgumentException("Invalid email address");
}
this.email = email;
}
public void setPassword(char[] password) {
if (password != null) {
String passwordStr = String.valueOf(password);
if (!Validators.isStrongPassword(passwordStr)) {
throw new IllegalArgumentException("Password requires 8 characters with at least 3 out 4 (uppercase letter, lowercase letter, number, special character)");
}
}
this.password = password;
}
The getPassword() method returns a char[] and not a String. This helps in security. This is mainly due to the immutability of Strings.
Since String ’s are an immutable data type, they cannot be changed after a value has been created on the String Pool . This will be automatically removed by the Java Garbage Collection Process after a while. Until then, this value will continue to exist on the Heap.
Therefore, the char[] is used to store the password. This is because the character array can be clear as an empty array immediately after using it. Therefore it will not be stored in the heap so no unauthorized user can access the heap and retrieve the entered password.
After setting the attributes in the doPost method, create a User object and validate all of the inputs.
User user = new User();
boolean errorFound = false;
try {
user.setEmail(email);
} catch(IllegalArgumentException e) {
req.setAttribute("emailError", e.getMessage());
errorFound = true;
}
try {
user.setPassword(password1.toCharArray());
} catch(IllegalArgumentException e) {
req.setAttribute("password1Error", e.getMessage());
errorFound = true;
}
if(password2.equals("")) {
req.setAttribute("password2Error", "This input is required");
errorFound = true;
}
if(!password1.equals(password2)) {
req.setAttribute("password2Error", "Passwords don't match");
errorFound = true;
}
if(terms == null || !terms[0].equals("agree")){
req.setAttribute("termsError", "You must agree to our terms of use");
errorFound = true;
}
Use c:if tags with expressions to display error messages after each label.
<c:if test="${not empty emailError}"><div class="invalid-feedback">${emailError}</div></c:if>
<c:if test="${not empty password1Error }"><div class="invalid-feedback">${password1Error}</div></c:if>
<c:if test="${not empty password2Error }"><div class="invalid-feedback">${password2Error}</div></c:if>
<c:if test="${not empty termsError }"><div class="invalid-feedback">${termsError}</div></c:if>
Use c:if tags with expressions in the form input class attributes.
<c:if test="${not empty emailError}">is-invalid</c:if>
<c:if test="${not empty password1Error }">is-invalid</c:if>
<c:if test="${not empty password2Error }">is-invalid</c:if>
<c:if test="${not empty termsError}">is-invalid</c:if>
Run the project and test invalid inputs.
CREATE PROCEDURE sp_get_user(IN p_email VARCHAR(255))
BEGIN
SELECT user_id, first_name, last_name, email, phone, password, language, status, privileges, created_at, timezone
FROM user
WHERE email = p_email;
END;
CREATE PROCEDURE sp_add_user(
IN p_email VARCHAR(255),
IN p_password VARCHAR(255),
IN p_status VARCHAR(10),
IN p_privileges VARCHAR(10)
)
BEGIN
INSERT INTO user (email, password, status, privileges)
VALUES (p_email,p_password,p_status,p_privileges);
END;
public static User get(String email) {
User user = null;
try (Connection connection = getConnection()) {
if (connection != null) {
try (CallableStatement statement = connection.prepareCall("{CALL sp_get_user(?)}")) {
statement.setString(1, email);
try (ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
int userId = resultSet.getInt("user_id");
String firstName = resultSet.getString("first_name");
String lastName = resultSet.getString("last_name");
String phone = resultSet.getString("phone");
char[] password = resultSet.getString("password").toCharArray();
String language = resultSet.getString("language");
String status = resultSet.getString("status");
String privileges = resultSet.getString("privileges");
Instant created_at = resultSet.getTimestamp("created_at").toInstant();
String timezone = resultSet.getString("timezone");
user = new User(userId, firstName, lastName, email, phone, password, language, status, privileges, created_at, timezone);
}
}
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return user;
}
CallableStatement
CallableStatement is an interface used to execute stored procedures.
Connection
An interface for classes representing a communication session with a database server.
ResultSet
A ResultSet is a table-like structure used to hold records returned from a SQL query.
public static void main(String[] args) {
System.out.println(get("test@example.com"));
}
CALL sp_add_user('test@example.com', 'badpassword');
if(UserDAO.get(email) != null) {
req.setAttribute("emailError", "A user with that email already exists. <a href="login">Login</a> or <a href=\"reset-password\">reset the password</a>.");
errorFound = true;
}
public static boolean add(User user) {
try (Connection connection = getConnection()) {
if (connection != null) {
try (CallableStatement statement = connection.prepareCall("{CALL sp_add_user(?, ?)}")) {
statement.setString(1, user.getEmail());
String encryptedPassword = BCrypt.hashpw(new String(user.getPassword()), BCrypt.gensalt(12));
statement.setString(2, encryptedPassword);
int rowsAffected = statement.executeUpdate();
return rowsAffected == 1;
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return false;
}
public static void main(String[] args) {
User user = new User();
user.setEmail("test2@example.com");
user.setPassword("P@ssw0rd".toCharArray());
add(user);
}
CALL sp_get_all_users();
if (!errorFound) {
user.setStatus("active"); // Changes default 'inactive' status to 'active'
user.setPrivileges("user"); // Changes default 'subscriber' privileges to 'user'
boolean userAdded = false;
try {
userAdded = UserDAO.add(user);
} catch (RuntimeException e) {
req.setAttribute("userAddFail", "User not added.");
}
if(userAdded) {
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", "Welcome New User!");
resp.sendRedirect(resp.encodeRedirectURL(req.getContextPath() + "/")); // Redirects to the home page
return;
}
}
<session-config>
<session-timeout>60</session-max-age>
</session-config>
<c:if test="${not empty userAddFail}">
<div class="alert alert-danger mb-2" role="alert">${userAddFail}</div>
</c:if>
To test the fail message, comment out the code in the Signup servlet that checks if an email already exists. Then, try to create a new user with an existing email address.
Our session object will track things such as logged in users, shopping cart contents, and more.
Edit the Login and Sign-up buttons so they only display when the "activeUser" session variable is not set.
When "activeUser" is set, display the My Profile and Sign Out buttons.
<div class="text-end">
<c:choose>
<c:when test="${empty sessionScope.activeUser}">
<a href="#" class="btn btn-outline-dark me-2">Login</a>
<a href="${appURL}/signup" class="btn btn-warning">Sign-up</a>
</c:when>
<c:otherwise>
<a href="#" class="btn btn-outline-secondary">Sign out</a>
<a href="#" class="btn btn-primary me-2">My Profile</a>
</c:otherwise>
</c:choose>
</div>
<div class="container">
<c:choose>
<c:when test="${not empty sessionScope.flashMessageSuccess}">
<div class="alert alert-success my-2" role="alert">
${sessionScope.flashMessageSuccess}
</div>
<c:remove var="flashMessageSuccess" scope="session" />
</c:when>
<c:when test="${not empty sessionScope.flashMessageDanger}">
<div class="alert alert-danger my-2" role="alert">
${sessionScope.flashMessageDanger}
</div>
<c:remove var="flashMessageDanger" scope="session" />
</c:when>
<c:when test="${not empty sessionScope.flashMessageWarning}">
<div class="alert alert-warning my-2" role="alert">
${sessionScope.flashMessageWarning}
</div>
<c:remove var="flashMessageWarning" scope="session" />
</c:when>
</c:choose>
</div>
Test out the form using an email that doesn't exist.
If everything works correctly, you will be redirected to the home page, and a green success message will appear between the navigation and the carousel slider. Also, the "Login" and "Sign-up" buttons will now say "Sign out" and "My Profile"
Query the database to see the new user.
CALL sp_get_all_users();
If you refresh the page, the flash message will disappear.
If you add two users with different email addresses but the same password, you will notice their hashed passwords are different.
Delete any unnecessary user database records before continuing.
DELETE FROM user WHERE user_id = N;
If you delete a row from the query console output, be sure to press the up arrow button to submit changes.
if(!errorFound){
user.setPrivileges("user");
user.setStatus("active");
boolean userAdded = false;
try {
userAdded = UserDAO.add(user);
user = UserDAO.get(user.getEmail());
} catch (RuntimeException e) {
req.setAttribute("userAddFail","User could not be added");
}
if(userAdded) {
user.setPassword(null);