Usually when developing complex multi tier applications at some point architects and/or developers need to discuss about concepts for user authentication and authorization. Some resources of your application might be accessible for anyone, while other resources are only accessible for authenticated users or even only for authenticated users in a specific role (authorization). In such scenarios you have different options for implementing your requirements. You could implement everything from scratch on your own - with all the benefits and all the drawbacks. You could also use one of the many frameworks available instead of implementing everything on your own. In case you like Java, you ever heard about Java EE 6 and you are running your applications on Glassfish 3 you can make use of the built-in authentication and authorization features that Glassfish 3 offers. Using Glassfish and Java EE 6 allows you to make use of JAAS (Java Authentication and Authorization Service) for securing your Java EE 6 (Web) Applications. This tutorial goes even further and makes suggestions for designing your user management data model in combination with jdbcRealms. Basic user management services such as user login, user logout and user (self-) registration are also discussed. But even tha's not all: you will also see how you could use Ajax in combination with jQuery to implement a simple Web Frontend for you application. Server and client will communicate via JSON data. For the conversion between Java object to JSON and the other way around we are not going to use JAXB (we will use Jackson instead). All over this tutorial you will find some further hints and Best Practices that you can leverage. Finally you will have a web application which you could use as a basic starting point for implementing your own requirements.
This tutorial has been tested with Glassfish 3.1.2. It will tell you how to integrate container managed authentication and authorization features offerd by Glasfish 3 into your Java EE application using different technologies. We will create a very simple application with the following features and requirements:
To implement our "little" requirements we will use Glassfish 3, PostgreSQL, EJBs, JAAS, JPA, REST, JSON, AJAX, jQuery and even much more. So it would help if you are kind of familiar with all that stuff. If you are familiar with all that, you might need only a few hours to walk through this tutorial. Please consider that I have chosen not to use Maven. I believe that this allows a larger audience to follow this tutorial. This tutorial does not handle how to install Glassfish 3. I also expect you have already installed a PostgreSQL database. Other databases are also fine, but I will only handle PostgreSQL in detail when it comes to creating a JDBC resource on Glassfish and then using JPA to access it. Please refer to the security section of the official Oracle Java EE 6 Tutorial for an overview of how to secure Java EE 6 Web Applications. If you don't know it already you might want to check The Open Web Application Security Project (OWASP). As a starting point you could begin with their page about Access Control In Your J2EE Application.
This tutorial requires some external libraries. Why and where they are needed is also discussed in this tutorial. Since I decided not to use maven (see above) please make sure to download the external dependecies (see below) manually and place them into the lib folder of your web application (you could still use maven if you want). All these dependencies are also packaged in the Downloads I offer below - so you might prefer that one instead of finding and downloading each jar one by one. To be honest, I used maven to get all these dependencies and then I just copied them one by one into the lib folder. Since Glassfish 3.1.2 ships with Jersey 1.11 you should make sure to get jersey-json version 1.11 and not 1.12.
Table of Contents:
Creating this tutorial meant a lot of effort. I hope it will help others. If you have any questions do not hesitate to contact me. Any feedback is welcome! Also feel free to leave a comment (see below). For helping me to maintain my tutorials any donation is welcome. But now enough words - enjoy the tutorial.
I guess you will either use Eclipse or NetBeans IDE for following this tutorial. If you follow all the steps (see the Table of Contents above) you will have the following project structures (Netbeans and Eclipse):
As we have defined requirements for allowing new users to register new accounts we need to define where and how we want to store the account data for each user. An already registered user can login with her or his credentials. That means we need to be able to create new user accounts (user registration) as well as reading/finding existing user account data (login) for authentication and authorization. Authorization means we want to consider that authenticated users need to have a specific role assigned in order to be authorized to access restricted content. The relationship between users and roles needs also to be considered for our data storage.
Besides username and user password usually you also want to have some more information from your users, i.e. first name, last name, address etc. This kind of data depends on your own requirements - for illustration purposes I will only store the users first name, last name and registration date. When developing web applications at some point you have to ask yourself what data you want and how to store them. This can lead to a user management component which offers APIs for your requirements. Once you know what data you want to store you need to think about the storage itself. In this tutorial we will benefit from the Glassfish 3 and Java EE 6 features for storing all of our data in a database (i.e. credentials, last name, first name) and then using them for securing our web application (athentication and authorization). Later we will explain how to configure Glassfish for weaving everything together, but for now we will create our data model for our user management component.
When having a data model in your mind you could either write down the SQL statements to create the tables you want. Then you could write your own layer for accessing and manipulating the data. This is what we will not do here. Instead, we will implement JPA 2.0 Entity Classes and make our tables being generated automatically by JPA. This also allows us to easily access available user data stored in our database, manipulate them or to create new entities (i.e. for creating a new user account). The data model for our user management component looks as follows:
As you can see we will have two tables: USERS and USERS_GROUPS. The primary key of our USERS table is the email address. The password of each user is also stored in that table as well as the firsname, lastname and the registration date. Later we will see how to assure that the password field does not contain a clear text password. The second table is the USERS_GROUPS table. It contains the mapping between users and groups. Therefore it has exactly two fields: the email field is a foreign key and the groupname contains the group name (String/varchar) assigend to a user. Both fields together are unique, i.e. a user can be assigned to a group only once while the user can be assigned to multiple different groups. From that little piece of information we can derive how our JPA classes should look like. Please consider that you usually want to start with the JPA classes before thinking about how your DB tables will look like. I have choosen to describe it here the other way around only for pedagogical reasons.
To make it easy I believe that it's a good idea to model our groups as a simple enum. Let's assume we have administrators and registered users that use our web application. We also could have other groups, i.e. I have just added a default group for demonstration reasons. The Group enum could look as follows:
package com.nabisoft.model.usermanagement; public enum Group { ADMINISTRATOR, USER, DEFAULT; }
Now we can think about our User Entity class. Every user has an email address which serves as the unique identifier of the user. I have defined that the length of the field is 128 - actually this might be way more than you will ever need (you could also use 64 or what ever instead). The attributes firstName, lastName and registeredOn are also pretty much straight forward - I want them always to be filled and that's why they cannot be null.
The password field is more interesting. As I mentioned above the user password should never be stored as clear text on the database. If you won't follow that rule then people who might have gained access to our USERS table could see the original passwords and abuse them (there are different horrible scenarios for that but we will not discuss them here). So a good idea to solve this issue is to only store hashed values on the DB. Please consider that MD5 is not save anymore, so never use MD5 in a critical environment. I have chosen to use SHA-512 which is considered to be pretty safe. I have also chosen to use SHA-512 in combination with a hexadecimal encoding/representation. But what length do we now need for our password field (length of column in DB)? As its name indicates SHA-512 is 512 bits long. Since we are using an hexadecimal encoding, each digit codes for 4 bits. So you need 512 : 4 = 128 digits to represent 512 bits. That means on the DB you would need a varchar(128) to store SHA-512 HEX values. Actually, you could also use a char(128) because the length stays always the same - it is not varying at all, no matter how long the input String initially was before letting SHA-512 + HEX to run over it. Please consider that in real life projects you might even go one step further and hash the password with a "salt". You might want to check this to see why you should add a salt. I will not use a salt in this tutorial because I want this tutorial to be kind of easy to follow. For creating a SHA-512 HEX value for a given String we can simply use the Apache Commons Codec library. So please make sure to add the Apache Commons Codec library to your application!
Attention: I store the password as a String. Everywhere I use a password (also in my REST APIs which will be discussed later) I use a String. From security point of view this is not a good idea! From security point of view it is much better to use a char array for such sensitive data! For more information about that please ask Google - this topic is out of scope for this tutorial. I have not used a char array in this tutorial only for pedagogical reasons (I don't want to make it too complex...). As you will see later the Servlet 3.0 API offers HttpServletRequest.login(String username, String password) which is also using a String instead of a char array for passing the password. I am sure the Java EE Spec guys have a good reason reason for that.
The next interesting attribute is groups. It contains a list of all the groups which the user has been assigned to. One of our constructors offers to initialize some of the attributes by expecting an UserDTO. DTO stands for Data Transfer Object and is a simple pattern for passing data. As you will see later we will user the UserDTO for our registration REST Service (although some of you might say that DTOs are kind of old school...). Since we have this constructor we also have to offer a default constructor (parameterless). Usually you should validate your attributes at some point. For demonstration I only validate the passwords - and that only in the the constructor which expects a UserDTO (later you will understand what password1 and password2 is). Actually, you could also user Bean Validation (JSR 303) but in this tutorial we are not going to use Bean Validation at all. Please also consider that we do not use any JAXB annotations (i.e. @XmlRootElement). The JPA annotations you see avoid using a mapping table. And here is the source for both the User Entity and UserDTO:
package com.nabisoft.model.usermanagement; import java.io.Serializable; import java.util.Date; import java.util.List; import javax.persistence.Cacheable; import javax.persistence.CollectionTable; import javax.persistence.Column; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.UniqueConstraint; import org.apache.commons.codec.digest.DigestUtils; import com.nabisoft.model.usermanagement.dto.UserDTO; @Entity @Table(name="USERS") @Cacheable(false) public class User implements Serializable { @Id @Column(unique=true, nullable=false, length=128) private String email; @Column(nullable=false, length=128) private String firstName; @Column(nullable=false, length=128) private String lastName; /** * A sha512 is 512 bits long -- as its name indicates. If you are using an hexadecimal representation, * each digit codes for 4 bits ; so you need 512 : 4 = 128 digits to represent 512 bits -- so, you need a varchar(128), * or a char(128), as the length is always the same, not varying at all. */ @Column(nullable=false, length=128) //sha-512 + hex private String password; @Temporal(javax.persistence.TemporalType.TIMESTAMP) @Column(nullable=false) private Date registeredOn; @ElementCollection(targetClass = Group.class) @CollectionTable(name = "USERS_GROUPS", joinColumns = @JoinColumn(name = "email", nullable=false), uniqueConstraints = { @UniqueConstraint(columnNames={"email","groupname"}) } ) @Enumerated(EnumType.STRING) @Column(name="groupname", length=64, nullable=false) private List<Group> groups; public User(){ } public User(UserDTO user){ if (user.getPassword1() == null || user.getPassword1().length() == 0 || !user.getPassword1().equals(user.getPassword2()) ) throw new RuntimeException("Password 1 and Password 2 have to be equal (typo?)"); this.email = user.getEmail(); this.firstName = user.getFName(); this.lastName = user.getLName(); this.password = DigestUtils.sha512Hex(user.getPassword1() ); this.registeredOn = new Date(); } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } /** * @return the password in SHA512 HEX representation */ public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Date getRegisteredOn() { return registeredOn; } public void setRegisteredOn(Date registeredOn) { this.registeredOn = registeredOn; } public List<Group> getGroups() { return groups; } public void setGroups(List<Group> groups) { this.groups = groups; } @Override public String toString() { return "User [email=" + email + ", firstName=" + firstName + ", lastName=" + lastName + ", password=" + password + ", registeredOn=" + registeredOn + ", groups=" + groups + "]"; } }
package com.nabisoft.model.usermanagement.dto; public class UserDTO { private String email; private String fName; private String lName; private String password1; private String password2; public String getFName() { return fName; } public void setFName(String firstName) { this.fName = firstName; } public String getLName() { return lName; } public void setLName(String lastName) { this.lName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword1() { return password1; } public void setPassword1(String password) { this.password1 = password; } public String getPassword2() { return password2; } public void setPassword2(String password) { this.password2 = password; } @Override public String toString() { return "User [email=" + email + ", fName=" + fName + ", lName=" + lName + ", password1=" + password1 +", password2=" + password2 + "]"; } }
So far we have defined a very simple model for our user management compoment. It's time to implement an interface that uses our Entities, i.e. for saving a new user to the database. For this purpose we will use a simple Stateless Session Bean. Per default our UserBean EJB is only accessible locally (No-Interface View). We don't want it to be accessible from any remote clients:
package com.nabisoft.model.usermanagement; import java.util.List; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; @Stateless public class UserBean { @PersistenceContext private EntityManager em; public List<User> findAll() { TypedQuery<User> query = em.createQuery("SELECT usr FROM User ORDER BY usr.registeredOn ASC", User.class); return query.getResultList(); } public void save(User user) { em.persist(user); } public void update(User user) { em.merge(user); } public void remove(String email) { User user = find(email); if (user != null) { em.remove(user); } } public void remove(User user) { if (user != null && user.getEmail()!=null && em.contains(user)) { em.remove(user); } } public User find(String email) { return em.find(User.class, email); } public void detach(User user) { em.detach(user); } }
We are using JPA, therefore we also want to define a persistence.xml file:
<?xml version="1.0" encoding="UTF-8"?> <persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> <persistence-unit name="authPU" transaction-type="JTA"> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <!-- see step 2 below --> <jta-data-source>jdbc/auth</jta-data-source> <properties> <property name="eclipselink.ddl-generation" value="create-tables"/> <!-- logging --> <!-- log JPA Statements --> <property name="eclipselink.logging.level" value="FINE"/> <!-- also log of the values of the parameters used for the query --> <property name="eclipselink.logging.parameters" value="true"/> </properties> </persistence-unit> </persistence>
We have named our one and only Persistence Unit authPU. If we had multiple Persistence Units then we would have to specify which of our Persistence Units shall be used for our EntityManager in our UserBean, i.e. like this (see UserBean.java above):
//this is enough for our case because we have only one PU //@PersistenceContext //we could also use this @PersistenceContext(unitName="authPU") private EntityManager em;
Now we want to tell Glassfish where our DB is, which credentials to use when connecting to the DB and so on. As I have mentioned earlier we want to connect to a PorstgreSQL database. There are different options for adding new JDBC Resources to Glassfish:
I never use option 1 because it doesn't support automated scripts. I prefer option 2 or 3, but I will only tell you how to configure all of your resources (in our case only a jdbc-connection-pool) into a glassfish-resources.xml file and afterwards how to add all of your resources defined in that file via asadmin add-resources to Glassfish (option 3). So first of all we will create the glassfish-resources.xml file:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource Definitions//EN" "http://glassfish.org/dtds/glassfish-resources_1_5.dtd"> <resources> <!-- JDBC --> <jdbc-connection-pool name="auth-Pool" allow-non-component-callers="false" associate-with-thread="false" connection-creation-retry-attempts="0" connection-creation-retry-interval-in-seconds="10" connection-leak-reclaim="false" connection-leak-timeout-in-seconds="0" connection-validation-method="auto-commit" datasource-classname="org.postgresql.ds.PGSimpleDataSource" fail-all-connections="false" idle-timeout-in-seconds="300" is-connection-validation-required="false" is-isolation-level-guaranteed="true" lazy-connection-association="false" lazy-connection-enlistment="false" match-connections="false" max-connection-usage-count="0" max-pool-size="32" max-wait-time-in-millis="60000" non-transactional-connections="false" pool-resize-quantity="2" res-type="javax.sql.DataSource" statement-timeout-in-seconds="-1" steady-pool-size="8" validate-atmost-once-period-in-seconds="0" wrap-jdbc-objects="false"> <property name="serverName" value="localhost"/> <property name="portNumber" value="5432"/> <property name="databaseName" value="db_auth"/> <property name="User" value="myUser"/> <property name="Password" value="mySecretPassword"/> <property name="URL" value="jdbc:postgresql://localhost:5432/db_auth"/> <property name="driverClass" value="org.postgresql.Driver"/> </jdbc-connection-pool> <jdbc-resource enabled="true" jndi-name="jdbc/auth" object-type="user" pool-name="auth-Pool"/> <!-- other resources, i.e. Mail --> <!-- ... --> </resources>
As you can see our glassfish-resources.xml defines a JDBC resource. With that configuration Glassfish can manage DB connections. In our case we have configured a PorstgreSQL database. Please see the official documentation for more details regarding the parameters I have used. The next step is to tell Glassfish about our jdbc-resource defined in the glassfish-resources.xml file by executing a asadmin add-resources command:
#you might not need "--secure" asadmin --secure add-resources glassfish-resources.xml #you can also specify the complete path to your glassfish-resources.xml file asadmin --secure add-resources /home/myUser/glassfish-resources.xml
A common format for data exchange between web frontend and backend system is JSON. Most tutorials you find will tell you how to use JAXB (Java API for XML Binding) and its Java annotations for generating JSON. Once you have done what those tutorials told you suddenly you see some very strange JSON fromat which is not what you expected. At this point you might look for other tutorials or blogs that tell you what else you have to do in order to get a natural JSON representation of your POJOs by using JAXB. If you are lucky, then you will find the POJOMappingFeature that is shipped with Jersey. But then you find out that you still have null values in your generated JSON Strings. So you keep asking Google for aome annotations or some other Features like the POJOMappingFeature that allow you to get rid of the null values. Then you find some annotations and code snippets that seem not to work for you because you don't have the correct libraries or library versions on your Glassfish classpath. With your last breath you have come to a point where you are getting ready to develop your own @Provider classes to generate JSON from POJOs and vice versa. Since you have found this tutorial you don't have to implement your own @Provider classes and you don't have to use JAXB.
JAXB is a great thing. But in case you only need JSON and no XML at all, like in our case, then JAXB might not be the best choice. JAXB allows to convert your POJOs to JSON and vice versa. But it always generates XML in an intermediate step and that's what we actually don't need. Furthermore you have to annotate your classes with @XmlRootElement, but what happens if you don't have the sources of classes that you want to convert to JSON? So there can be good reasons for not using JAXB, it depends from case to case... As an alternative you could use Google's gson (which is by the way really great). But since we are in a Glassfish world (at least here in this tutorial) we will use Jackson (Jersey is based on Jackson). Please keep in mind that I have tested this tutorial with a Glassfish 3.1.2 installation. The 3.1.2 version ships with Jersey 1.11 (you can easily check your Jersey version in the server output when you start your Glassfish, i.e. something like this: Initiating Jersey application, version 'Jersey: 1.11 12/09/2011 10:27 AM'). By the end of 2011 Jersey was modularized. What we actually want is to have all the jersey-json maven dependencies in our project. But since I have promised not to use Maven you have to add them all one by one to your classpath. Please download all the jars listed on the Jersey Dependencies page under 11.4.1. JAXB (we only need the jersey-json dependencies). Hint: I have added all the relevant dependencies to the /WEB-INF/lib/ folder of the downloadable sources below. If you want some more information about the different Jackson packages please check here. By the way: we are using Jackson 1.9.2 and we want to configure Jackson globally in our own @Provider class. Instead we could also use @Json* annotations, i.e. @JsonSerialize(include = Inclusion.NON_EMPTY). Inclusion.NON_EMPTY tells Jackson not to include null values or empty collections. That's it - the rest happens without any further configuration :-) Please check here in case you have multiple Providers for the same type.
package com.nabisoft.jaxrs.provider; import javax.inject.Singleton; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.ext.ContextResolver; import javax.ws.rs.ext.Provider; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.annotate.JsonSerialize; @Provider @Produces(MediaType.APPLICATION_JSON) @Singleton public class MyJacksonJsonProvider implements ContextResolver<ObjectMapper> { private static final ObjectMapper MAPPER = new ObjectMapper(); static { // since Jackson 1.9 // this default configuration can be overwritten via annotations MAPPER.setSerializationInclusion(JsonSerialize.Inclusion.NON_EMPTY); } public MyJacksonJsonProvider() { System.out.println("Constructor called: com.nabisoft.jaxrs.provider.MyJacksonProvider"); } @Override public ObjectMapper getContext(Class<?> type) { System.out.println("MyJacksonProvider.getContext(): "+type); return MAPPER; } }
The browser (or any client) sends HTTP requests to our Glassfish server in order to consume our REST Services. If the server is able to process the request it returns the response data itself and a HTTP Status code that tells the caller if everything was ok or not. There are a lot of HTTP Staus codes defined in the HTTP Spec, I am sure you know at least 404 (Not Found), 200 (OK), 500 (Internal Server Error). On our frontend we use jQuery (frontend is discussed later). When executing Ajax requests with jQuery the error callback function that you define for your Ajax call is called automatically by jQuery depending on the HTTP Status code of the response. Besides that you could also define a statusCode object for your Ajax call and define callback functions for each HTTP Status code you are interested in (later you will see some example code below). When implementing your backend services you always want to implement a safe error handling, you want to consider everything that could happen. Once you know what could happen you start thinking about what HTTP Status code should be returned for each specific case, i.e. you might want to return a 404 in case the ID for your getBookById Service doesn't exist in the database, or you might want to return a 403 in case the user doesn't have enough privileges to call your getBookById service. This little example should only tell you that you could and should make use of the HTTP Status codes. But then there are also cases where you cannot clearly identify which HTTP Status you should return. Furthermore using all of the possible HTTP Status codes could also require additional lines of code within your frontend in order to "react" correctly. You should decide whether you want to treat the HTTP Status code as part of your REST APIs or not, and that decision can be different from project to project, or even from service to service (latter might not be the best solution). I usually make use of only a few HTTP Status codes, i.e. 404, 403, 500, 200 etc. Besides that I use a JsonEnvelope which separates the response data itself and the "application" status of the request. In all my JSON responses I use this JsonEnvelope - a very simple POJO which is not even extending javax.ws.rs.core.Response (ok, maybe I was a little lazy to extend javax.ws.rs.core.Response...). The application status (you might want to use another term for that, maybe "app request status") is something different than the HTTP Status. To demonstrate this all let's talk about our login service which is discussed in detail later: For now we only want to consider two cases (although there can be more). The first case is "Login succeeded" - in that case the HTTP Status is 200 (OK) and the JsonResponse status (aka application status) could be "SUCCESS". The second case is "Login failed" - in that case the HTTP Status still stays at 200, whereas the JsonResponse status is "FAILED". You don't have to do it the way I decided, there are other options as well. Feel free to choose your own "Best Practics". Please also consider that I have added a version attribute to the JsonResponse class because we might have different client technologies (not only our web frontend) and we might want to change the attributes of our JsonResponse. Here is the source code for our JsonResponse class:
package com.nabisoft.json; import java.util.Map; //import javax.xml.bind.annotation.XmlElement; //import javax.xml.bind.annotation.XmlRootElement; //import org.codehaus.jackson.map.annotate.JsonSerialize; //@XmlRootElement //we don't need this thanks to Jackson //@JsonSerialize(include = JsonSerialize.Inclusion.NON_EMPTY) //we already use global configuration, see MyJacksonJsonProvider.java public class JsonResponse{ private static final float version = 1.0f; private String status; private String errorMsg; private Map<String, Object> fieldErrors; private Object data; public JsonResponse() { } public JsonResponse(String status) { this.status = status; } //@XmlElement //we don't need this thanks to Jackson public float getVersion() { return JsonResponse.version; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getErrorMsg() { return errorMsg; } public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; } public Map<String, Object> getFieldErrors() { return fieldErrors; } public void setFieldErrors(Map<String, Object> fieldErrors) { this.fieldErrors = fieldErrors; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } }
In the previous steps we have implemented our JPA 2.0 User Entity as well as a locally accessible (No-Interface View) Stateless Session Bean (UserBean). We also have added a JDBC Resource to our Glassfish installation. In this step we want to implement some RESTful Web Services that can be called from any remote client. Glassfish comes with Jersey - the JAX-RS (JSR 311) Reference Implementation for building RESTful Web Services (see http://jersey.java.net/). As you can guess we will use Jersey for implementing our REST Services. Our REST Services will allow to login and logout an existing user. We will also implement a service that allows to register a new user. The REST Services use the Stateless Session Bean we have created in step 1. The following class implements our REST Services:
package com.nabisoft.service.usermanagement; import java.util.ArrayList; import java.util.List; import javax.ejb.EJB; import javax.ejb.Stateless; import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttributeType; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import com.nabisoft.json.JsonResponse; import com.nabisoft.model.usermanagement.Group; import com.nabisoft.model.usermanagement.User; import com.nabisoft.model.usermanagement.UserBean; import com.nabisoft.model.usermanagement.dto.UserDTO; @Path("/auth") @Produces(MediaType.TEXT_PLAIN) @Stateless public class UserManagementService { @EJB private UserBean userBean; @GET @Path("ping") public String ping() { return "alive"; } @POST @Path("login") @Produces(MediaType.APPLICATION_JSON) public Response login(@FormParam("email") String email, @FormParam("password") String password, @Context HttpServletRequest req) { JsonResponse json = new JsonResponse(); //only login if not already logged in... if(req.getUserPrincipal() == null){ try { req.login(email, password); req.getServletContext().log("Authentication Demo: successfully logged in " + email); } catch (ServletException e) { e.printStackTrace(); json.setStatus("FAILED"); json.setErrorMsg("Authentication failed"); return Response.ok().entity(json).build(); } }else{ req.getServletContext().log("Skip logged because already logged in: "+email); } //read the user data from db and return to caller json.setStatus("SUCCESS"); User user = userBean.find(email); req.getServletContext().log("Authentication Demo: successfully retrieved User Profile from DB for " + email); json.setData(user); //we don't want to send the hashed password out in the json response userBean.detach(user); user.setPassword(null); user.setGroups(null); return Response.ok().entity(json).build(); } @GET @Path("logout") @Produces(MediaType.APPLICATION_JSON) public Response logout(@Context HttpServletRequest req) { JsonResponse json = new JsonResponse(); try { req.logout(); json.setStatus("SUCCESS"); req.getSession().invalidate(); } catch (ServletException e) { e.printStackTrace(); json.setStatus("FAILED"); json.setErrorMsg("Logout failed on backend"); } return Response.ok().entity(json).build(); } @POST @Path("register") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @TransactionAttribute(TransactionAttributeType.NEVER) public Response register(UserDTO newUser, @Context HttpServletRequest req) { JsonResponse json = new JsonResponse(); json.setData(newUser); //just return the date we received //do some validation (in reality you would do some more validation...) //by the way: i did not choose to use bean validation (JSR 303) if (newUser.getPassword1().length() == 0 || !newUser.getPassword1().equals(newUser.getPassword2())) { json.setErrorMsg("Both passwords have to be the same - typo?"); json.setStatus("FAILED"); return Response.ok().entity(json).build(); } User user = new User(newUser); List<Group> groups = new ArrayList<Group>(); groups.add(Group.ADMINISTRATOR); groups.add(Group.USER); groups.add(Group.DEFAULT); user.setGroups(groups); //this could cause a runtime exception, i.e. in case the user already exists //such exceptions will be caught by our ExceptionMapper, i.e. javax.transaction.RollbackException userBean.save(user); // this would use the clients transaction which is committed after save() has finished req.getServletContext().log("successfully registered new user: '" + newUser.getEmail() + "':'" + newUser.getPassword1() + "'"); req.getServletContext().log("execute login now: '" + newUser.getEmail() + "':'" + newUser.getPassword1() + "'"); try { req.login(newUser.getEmail(), newUser.getPassword1()); json.setStatus("SUCCESS"); } catch (ServletException e) { e.printStackTrace(); json.setErrorMsg("User Account created, but login failed. Please try again later."); json.setStatus("FAILED"); //maybe some other status? you can choose... } return Response.ok().entity(json).build(); } }
The UserManagementService class offers three services: login, logout and register. All of our services produce JSON (we will not produce XML or plain text). The register service even consumes JSON. We also want to offer a simple ping service that could tell whether our REST services are reachable or not. The ping service is a very "stupid" one without any special logic (feel free to extend or disable it).
The login service uses the Servlet 3.0 API for logging in a user. As you can see the API requires to pass both username and password as Strings. In our case the username is the email address. We only want to login a user if the user is not already logged in. If we would not consider that then the re-login would cause an exception: javax.servlet.ServletException: Attempt to re-login while the user identity already exists. In fact, if req.login(email, password) fails due to wrong credentials we also get a ServletException. So the first thing you should keep in mind is that we don't have something like a LoginException here although you can see something like this in your stack trace: Web Login Failed: com.sun.enterprise.security.auth.login.common.LoginException: Login failed: Security Exception. But this exception is wrapped in a ServletException. So if you want to catch only the LoginExceptions in an ExceptionMapper (we will implement an ExceptionMapper later) then forget it. The only way to catch a LoginException is by putting req.login(email, password) into a try-catch block. If you would try to use a ExceptionMapper, then you would have to implement ExceptionMapper<ServletException> because req.login(email, password) only throws a ServletException. Unfortunately, the ServletException is also thrown at other places, so in the ExceptionMapper you cannot distinguish if the ServletException came from a failed login or not. So we are stuck to put req.login(email, password) into a try-catch block. Our login service returns a User entity in JSON format. Since we don't want to send the SHA-512 HEX value of the user's password to the frontend "clear" it, what means that will simply set the value to null. Before we can do that we need to detach the entity. The same we do for the assigned groups - we don't want to tell the caller which groups the user is assigned to (this information is only relevant for the backend). Because of these two fields we could also have used a DTO or other ways to ignore these two fields. Besides the DTO an other way would be to use the @JsonIgnore annotation which is offered by Jackson.
The logout service uses the Servlet 3.0 API for logging out a user. I also make sure to clear the session as the API does not really tell me what happens with the session if logout() is called. I guess that the session is not invalidated, so I want to do it on my own. In fact, I believe that it is a good design approach to not automatically invalidate the user's session when logout() is called. Depending on your own business logic you can best decide on your own if the session shall be invalidated or not. There are even good examples where you might not want to invalidate the users session, i.e. do you want your Amazon shopping cart to be cleared after you have logged out (assuming the shopping cart data is stored in the sesison)? After logout() is called then request.getUserPrincipal() will return null indicating the caller is not authenticated (not logged in). Please have a look at Oracle's official Java EE 6 Tutorial for some example code telling you how you could use the HttpServletRequest interface to authenticate users for a web application programmatically by using the Servlet 3.0 methods login(userName,password) and logout(). The logout service returns a very simple JSON response.
The register service both produces and consumes JSON. When registering a new user we want to automatically login the user we have just created. In case the user creation (= registration) fails the user, of course, is not logged in. As you can see from the register API a UserDTO is expected. Our strategy is to simply return to the caller what ever we have received from the caller (both in JSON format) with one exception: in case our call to userBean.save(user) fails we will simply return what ever our ThrowableExceptionMapper returns (see ThrowableExceptionMapper.java below). The newUser parameter of type UserDTO will contain the values which have been passed from the client. Jersey and Jackson will manage to fill the corresponding fields of our UserDTO by parsing the incoming JSON String (remind that the service consumes JSON). We only have to make sure that the client really sends a JSON String instead of regular HTML form data via POST. Please consider that there are differences between the UserDTO and our User entity. The UserDTO has password1 and password2, while the User entity only has one password attribute. Furthermore the UserDTO does not contain any information ragarding the groups assigned to a user. While we usually don't want our User entity data to be published to clients (think of our SHA-512 HEX passwords and Groups) the UserDTO is meant to be used for passing data from client to backend and (maybe) the other way around. As already mentioned above you could also have implemented all that without an additional UserDTO class. Basically, our UserDTO serves as a container for our HTML registration form data passed as JSON to our register service (the registration form will be discussed later). The only validation we want to implement is checking if password1 and password2 of our UserDTO are equal and not empty Strings. This allows to prevent typos, I am sure you know the idea behind that. As you can see we don't have to check for null values what makes the code to look a little better. In a real world application you would have some more validations, i.e. you might want to check if the passed email address is a real email address. You might also want to benefit from Bean Validation (JSR 303), but this is out of scope for this tutorial. Please feel free to implement your own validations. After we have verified that our validations are fine we can continue with creating the user. First we create an instance of our User entity by passing the UserDTO to the constructor. We assign every new user to all three groups we have defined in step 1 (see Group.java). Please consider how the user's password is set within the constructor of our User entity: we have to create a SHA512 + HEX representation and store exactly this value to the DB - not the clear text (String)! For that we simply use the Apache Commons Codec library found at http://commons.apache.org/codec/. It offers exactly what we want: DigestUtils.sha512Hex(password1). For saving the new user via JPA we simply use our UserBean EJB. We don't have to create an instance, instead Glassfish injects a reference for us. If everything worked then we login the user we have just persisted by using the same Servlet 3.0 API we have used for our login REST Service (see above).
But how about transactions? As you might have noticed our UserManagementService is actually a Stateless Session Bean. Glassfish allows to inject resources into an EJB via Dependency Injection. In our case we inject another EJB (UserBean) into our UserManagementService EJB by using the @EJB annotation (keep in mind that our UserManagementService EJB is only used for making our REST Services available for the public). I have choosen to implement it this way to put your attention on a common pitfall when working with transactions and EJBs. As we know Statefull/Stateless EJBs are per default transactional and the default TransactionAttributeType is REQUIRED. That means that each business method of an EJB is executed within a transaction. The official JavaDoc says: "If a client invokes the enterprise bean's method while the client is associated with a transaction context, the container invokes the enterprise bean's method in the client's transaction context." In our case this means that when ever a REST Service is called Glassfish will create a new transaction context - especially for our register service this piece of information is very important. In the register service we use another EBJ (UserBean) and call its save() method. This save() method is not executed in a new transaction - instead it is executed within the same transaction that was created initially for our register service. The transaction is committed after the register service has finished. Our problem is now that within our register service method we create a new user and right after that we want to login that user. The login will access the DB and will only succeed if the user data can be found on the DB, but the user data is committed to the DB after we want to execute req.login(email, password1). So what we actually want is to make our UserBean commit right after userBean.save(user) is executed. There are different ways to implement this requirement, I have simply choosen to add a @TransactionAttribute(TransactionAttributeType.NEVER) annotation to our register service's implementation method. This will tell Glassfish not to create a new transaction when the register service is called, which means that our UserBean will get its own transaction (created by Glassfish) and after userBean.save(user) has finished the transaction will be committed. That's exactly what we want.
In our REST Services we have used try-catch blocks to catch Exceptions that might occur. But in some cases RuntimeExceptions can be thrown and so far we have no code for handling such exceptions. Our register service, to name one example, could throw a RuntimeException in case the user to be stored in the database is already available ("duplicate key exception") - I think you would get a javax.transaction.RollbackException to be more precise. Of course, you could simply put userBean.save(user) into a try-catch block and handle any Exception that might occur. Only for demonstration purposes I did not do that. Instead, I have choosen to implement an ExceptionMapper for all of our REST Services. This ExceptionMapper I have named ThrowableExceptionMapper. It is responsible to catch all uncaught Exceptions (more precisely: all uncaught Throwables) that might be thrown. This way we can make sure to always send JSON to the frontend instead of some stack traces (except the case where the user's session has timed out...), even in case of RuntimeExceptions. The implementation of our ThrowableExceptionMapper class is very simple and looks as follows:
package com.nabisoft.jaxrs.provider; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; import com.nabisoft.json.JsonResponse; @Provider public class ThrowableExceptionMapper implements ExceptionMapper<Throwable>{ private static final Response RESPONSE; private static final JsonResponse JSON = new JsonResponse("ERROR"); static { RESPONSE = Response.status(500).entity(JSON).build(); } @Override @Produces(MediaType.APPLICATION_JSON) public Response toResponse(Throwable ex) { System.out.println("ThrowableExceptionMapper: "+ex.getClass()); ex.printStackTrace(); //usually you don't pass detailed info out (don't do this here in production environments) JSON.setErrorMsg(ex.getMessage()); return RESPONSE; } }
We want to offer a self-registration feature for our web application. Any user should be allowed to register a new account by entering a username and a password. So what we actually need is a very simple user registration form. Our form has input fields for first name, last name, email address, password and another one for the password to make sure the password is free of typos. The email address serves as the unique user identifier in our system. But in this tutorial I am not going to check if the email address is a valid email address (you could use one of the many libraries available for that purpose or write your own regex to validate the email address). Furthermore, right below the registration form, we want to offer a login form allowing a user to login with an existing account.
Before we will discuss about our /welcome.jsp let's first discuss about some JavaScript, jQuery and JSON stuff. Our REST Services all produce JSON responses. One of our services even consumes JSON data. From that we can tell that we need to be able to create JSON from JavaScript Objects and to create JavaScript Objects from JSON (the latter is very simple because jQuery does it all for us). Unfortunately, jQuery does not really offer support for converting a JavaScript Object to a JSON String. Since we want to use jQuery we could simply use the jquery-json plugin. I have not chosen to use it because of a blog entry from John Resig, the author of jQuery. He said: In the meantime PLEASE start migrating your JSON-using applications over to Crockford's json2.js. On the other side jquery-json is actually based on json2, so why not using json2 directly? So we will use json2, please see json2 at Github for more information. The library detects if the browser offers native JSON support or not, i.e. JSON.stringify. In case the browser does not offer native JSON support json2 will add ECMAScript 5 compatible support for JSON. ECMAScript 5 was published in December 2009, so json2 is especially relevant for older browser versions published around that date. ECMAScript 5 also adds the "use strict" mode which is a good practice to use - especially while developing your web application (we also make use of "use strict"). For more information about strict mode please check John Resig's blog mentioned above.
Before you continue please first download the latest json2.js and the latest jQuery.js version (at the time writing this tutorial the lates jQuery was 1.7.2, so my js file is jquery-1.7.2.js):
Oh well, we have talked about some jQuery stuff now, so let's start to integrate jQuery into our web application. When integrating jQuery into your web applications it is a good idea to use a common Content Delivery Network (CDN). One of the most popular CDNs comes from Google and is called Google Libraries API or simply Google APIs (I usually call it Google APIs). Within your aplications you can simply reference jQuery directly from the Google APIs. And there is a good reason to do so: imagine you just visited some webpage that references jQuery from the Google APIs, then you go to some other page which also references jQuery from the Google APIs. In this case you could profit from a better performance due to caching. The more webpages use the Google APIs the better the overall performance of the web will be (please keep in mind that HTTPS usually is a killer for caching). Integrating jQuery from Google APIs is very simple. When integrating jQuery you (or any other external content) you should you should always think about the protocol. If your site is accessed via HTTPS then you shold also reference the external resources (i.e. js or css files) also via HTTPS. If you would use HTTP in such cases then the browsers usually show some warning. As section 4.2 of RFC 3986 states you could simply use this here:
<!-- instead of this (this is old school) --> <script src="<%=request.isSecure() ? "https":"http" %>://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.js"></script> <!-- you should use this --> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.js"></script>
Doesn't the second script tag look much better than the first one? The second script tag simply tells the browser to choose HTTPS or HTTP depending on what ever (HTTP or HTTPS) is used on the current page. See here for a great overview by Dave Ward. Actually, also see Dave Ward's comment about 3 reasons why you should let Google host jQuery for you. His blogs describe really good why you should simply use "//" and how that works as well as why you should use a CDN and what you should have in your mind when working with HTTPS.
But now back to this tutorial: there is one problem we have: what if the referenced resource is not available? Or in our case: what if jQuery cannot be loaded from the Google APIs, i.e. if Google is blocked for some reason? Before answering this question we will implement a JSP include file called jquery.jsp which can be used by all our other JSPs to include jQuery or jQuery Plugins (we will only discuss jQuery Plugins). Here is the code for /WEB-INF/includes/head/jquery.jsp:
<%@page contentType="text/html" pageEncoding="UTF-8"%> <!-- jq integration from google cdn (content delivery network) --> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.js"></script> <!-- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.min.js"></script> --> <script> //make sure jq is really loaded from google, else load it from our local server if (!window.jQuery){ document.write(unescape("%3Cscript src='<%=request.getContextPath() %>/js/jquery/jquery-1.7.2.js' type='text/javascript'%3E%3C/script%3E")); } </script> <!-- jq-ui integration from google cdn (content delivery network) --> <!-- you could do it this way, but we don't really need it for our tutorial--> <!-- <script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.js"></script> <link href="//ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/base/jquery-ui.css" rel="stylesheet" type="text/css"/> -->
First of all we will reference jQuery from the Google APIs either via HTTP or HTTPS. We have to consider HTTPS to avoid browser warnings, which usually come up when an HTTPS page tries to access some content via HTTP. Furthermore you can choose between jquery.js or jquery.min.js from the Google APIs. The latter is minified while the jquery.js file is better for debugging. And now the magic: if jQuery could not be laoded for some reason then window.jQuery will be undefined. In such cases we will simply fall back to a locally deployed version of jQuery which is placed at /js/jquery/jquery-1.7.2.js (as you can see we use version 1.7.2 while the one from the Google APIs is the latest 1.7.x version). But why are we using document.write(...)? This will add the new script tag to your head at the right place! Please consider that you should not use DOM methonds to create a new element and then adding it to the DOM. This would create a Non-blocking script element, which means that the script is loaded asynchronously instead of synchronously! See Stoyan Stefanov's comment about Non-blocking JavaScript Downloads in the YUI Blog. Andrea Giammarchi's blog about discusses document.write() is also really nice. On her blog you will learn where the new script tag is added when using document.write() in the header for loading a remote script.
Now let's start with the welcome.jsp which is placed at the application's context root and contains two simple HTML forms. The first thing you can see is an automatic redirection to /secure/index.jsp in case the user is already logged in. Then we include both json2 and jQuery. The action attribute of the registration form is set to /services/auth/register and the method is post. This means when submitting the form the data entered by the user is sent to /services/auth/register via post which is exactly the endpoint of our register REST Service. At this point you can go back to the previous step and have a look at our UserManagementService class to recall what happens in our register REST Service. The second HTML form we have in the /welcome.jsp file is for users that want to login with their already existing user account. This form submits to our login REST Service which we implemented earlier. So the /welcome.jsp file contains two HTML forms (one for login and one for registration) and each of the forms submit to the corresponding REST Service for executing a login or a registration. Last but not least our /welcome.jsp offers a link for navigation to /secure/index.jsp. As you will see later everything under /secure/* is secured content which means that you can only access that content if you are logged in. Both login and logout as well as registration backend logic have been discussed in the previous step. For both forms (register and login) we have registerd submit event listeners. They are executed when clicking on one of the submit buttons and are responsible for making an Ajax call. In case of the registration submit button all form data to be sent is converted into a JSON String. I also handle the server responses in one of the handlers. If the registration or login succeeds ew simply forward to /secure/index.jsp via JavaScript. As you can see in the success handler we check for data.status == "SUCCESS". This comes from our JsonResponse Envelope (see JsonResponse.java above).
For styling I have used an external css file. I will not discuss it here in detail. Below you find the sources of /welcome.jsp and /css/auth.css (below you will also find the corresponding screenshot):
<%@page contentType="text/html" pageEncoding="UTF-8" %><%@ taglib uri='http://java.sun.com/jsp/jstl/core' prefix='c' %><c:if test="${pageContext.request.userPrincipal!=null}"> <c:redirect url="/secure/index.jsp"/> <!-- this will redirect if user is already logged in --> </c:if> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <link rel="stylesheet" type="text/css" href="./css/auth.css" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Welcome Page</title> <!-- see https://github.com/douglascrockford/JSON-js --> <!-- alternative: http://code.google.com/p/jquery-json/ --> <!-- John Resig (author of jQuery) said: "In the meantime PLEASE start migrating your JSON-using applications over to Crockford's json2.js" see here: http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/ --> <script src="<%=request.getContextPath() %>/js/json2.js"></script> <%@ include file="/WEB-INF/includes/head/jquery.jsp" %> <script> $(function(){ "use strict"; $(document.forms['registerForm']).submit(function(event){ var data = { fname: this.fname.value, lname: this.lname.value, email: this.email.value, password1: this.password1.value, password2: this.password2.value }; var destinationUrl = this.action; $.ajax({ url: destinationUrl, type: "POST", //data: data, data: JSON.stringify(data), contentType: "application/json", cache: false, dataType: "json", success: function (data, textStatus, jqXHR){ //alert("success"); if (data.status == "SUCCESS" ){ //redirect to secured page window.location.replace("https://"+window.location.host+"<%=request.getContextPath() %>/secure/index.jsp"); }else{ alert("failed"); } }, error: function (jqXHR, textStatus, errorThrown){ alert("error - HTTP STATUS: "+jqXHR.status); }, complete: function(jqXHR, textStatus){ //alert("complete"); //i.e. hide loading spinner }, statusCode: { 404: function() { alert("page not found"); }, } }); //event.preventDefault(); return false; }); $(document.forms['loginForm']).submit(function(event){ var data = { email: this.email.value, password: this.password.value }; var destinationUrl = this.action; $.ajax({ url: destinationUrl, type: "POST", data: data, cache: false, dataType: "json", success: function (data, textStatus, jqXHR){ //alert("success"); if (data.status == "SUCCESS" ){ //redirect to secured page window.location.replace("https://"+window.location.host+"<%=request.getContextPath() %>/secure/index.jsp"); }else{ alert("failed"); } }, error: function (jqXHR, textStatus, errorThrown){ alert("error - HTTP STATUS: "+jqXHR.status); }, complete: function(jqXHR, textStatus){ //alert("complete"); } }); //event.preventDefault(); return false; }); }); </script> </head> <body> <h1>Welcome to our secured Web Application</h1> <a href="<%=request.getContextPath() %>/secure/index.jsp" >go to secured page</a> <br/><br/><br/> <div class="register"> <form id="registerForm" name="registerForm" action="<%=request.getContextPath() %>/services/auth/register" method="post"> <fieldset> <legend>Registration</legend> <div> <label for="fname">First Name</label> <input type="text" id="fname" name="fname"/> </div> <div> <label for="lname">Last Name</label> <input type="text" id="lname" name="lname"/> </div> <div> <label for="email">Email</label> <input type="text" id="email" name="email"/> </div> <div> <label for="password1">Password</label> <input type="password" id="password1" name="password1"/> </div> <div> <label for="password2">Password (repeat)</label> <input type="password" id="password2" name="password2"/> </div> <div class="buttonRow"> <input type="submit" value="Register and Login" /> </div> </fieldset> </form> </div> <br/><br/><br/> <div class="login"> <form id="loginForm" name="loginForm" action="<%=request.getContextPath() %>/services/auth/login" method="post"> <fieldset> <legend>Login</legend> <div> <label for="email">Email</label> <input type="text" id="email" name="email"/> </div> <div> <label for="password">Password</label> <input type="password" id="password" name="password"/> </div> <div class="buttonRow"> <input type="submit" value="Login" /> </div> </fieldset> </form> </div> </body> </html>
.login, .register{ font-family: Arial,sans-serif; font-size: 13px; } .login fieldset, .register fieldset{ padding-top: 1em; padding-bottom: 1em; border: 1px solid #0053A1; width: 380px; } .login legend, .register legend{ color: #fff; background: #0053A1; border: 1px solid #0053A1; padding: 1px 5px; } .login div, .register div{ margin-bottom: 0.5em; text-align: left; } .login label, .register label{ width: 140px; float: left; text-align: left; margin-right: 10px; padding: 2px 0px; display: block; } .login input[type=text], .login input[type=password], .register input[type=text], .register input[type=password]{ color: #0053A1; background: #fee3ad; border: 1px solid #0053A1; padding: 2px 5px; width:200px; } .login .buttonRow, .register .buttonRow{ text-align: left; } .login input[type=submit], .register input[type=submit]{ color: #fff; background: #0053A1; border: 2px outset #0053A1; } .login .authError{ margin-bottom:20px; color:red; text-align:center; }
But what happens if the user tries to access secured content? In that case we want a login form to be displayed. In just a moment (see below) you will see how to configure Glassfish to display a login form automatically if an unauthenticated (=anonymous) user tries to access secured content. The same login form we are talking about is only relevant for Glassfish and should not be accessible through a direct HTTP/HTTPS request. Therefore we will place it at /WEB-INF/login.jsp. When submitting this login form a post request with the form data is sent to Glassfish but this time the data is not sent to our login REST Service! Instead Glassfish will check the credentials automatically. This is standard Java EE stuff, for more information about the process flow of form based authentication please have a look at the official Oracle Java EE 6 documentation. The great thing is that Glassfish remembers what secured content was initially requested and after a successful login (authentication) this resource will be displayed. So there is no need to implement some forward logic. The /WEB-INF/login.jsp looks as follows:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <%@ taglib uri='http://java.sun.com/jsp/jstl/core' prefix='c' %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> <html> <head> <title>Login</title> <link rel="stylesheet" type="text/css" href="<%=request.getContextPath() %>/css/auth.css" /> </head> <body> <center> <div class="login"> <!-- did we already try to login and it failed? --> <c:if test="false"> <div class="authError"> Invalid User Name or Password. Please try again. </div> </c:if> <form action="j_security_check" method="post"> <fieldset> <legend>Login</legend> <div> <label for="email">Email</label> <input type="text" id="j_username" name="j_username"/> </div> <div> <label for="password">Password</label> <input type="password" id="j_password" name="j_password"/> </div> <div class="buttonRow"> <input type="submit" value="Login" /> </div> </fieldset> </form> </div> </center> </body> </html>
After a user has logged in successfully she or he can access the restricted area. In our case the restricted area is everything we put under /secure/*. For demonstration purposes we offer some secured content at /secure/index.jsp. We some information printed out, one logout link and a "Get Server Time" button. The logout is very simple to explain: once you click it an Ajax request is sent to our logout REST Service. In case of a SUCCESS response we redirect the user back to the welcome page via JavaScript. Keep in mind that in case someone tries to access restricted content Glassfish will automatically show our login.jsp - of course, the outcome is a simple HTML file. The same thing happens if you had a REST Service which is secured. Once authenticated and authorized it could still be possible that you are not allowed to access a REST Service, i.e. in case your session has timed out. The "Get Server Time" button is added to that page to demonstrate how you could handle a session timeout in your browser via JavaScript and jQuery. If the "Get Server Time" button is clicked an Ajax call to /services/secure/timestamp/now is triggered - this is the endpoint of another REST Service which will be introduced below. For now it is enough to know that everything below /services/secure/* is also secured. If we had a session timeout the the response of the Ajax call will be html instead of JSON. Therefore jQuery fails to parse the response as JSON and calls the error handler. Here we can simply check for textStatus == "parsererror" and forward to our welcome page (or somewhere else) in case. Here is the code of /secure/index.jsp:
<%@page import="java.security.Principal"%> <%@page contentType="text/html" pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Secured JSP Page</title> <!-- see https://github.com/douglascrockford/JSON-js --> <script src="<%=request.getContextPath() %>/js/json2.js"></script> <%@ include file="/WEB-INF/includes/head/jquery.jsp" %> <script> $(function(){ "use strict"; $('#logoutLink').click(function(){ var destinationUrl = this.href; $.ajax({ url: destinationUrl, type: "GET", cache: false, dataType: "json", success: function (data, textStatus, jqXHR){ //alert("success"); if (data.status == "SUCCESS" ){ //redirect to welcome page window.location.replace("https://"+window.location.host+"<%=request.getContextPath() %>/welcome.jsp"); }else{ alert("failed"); } }, error: function (jqXHR, textStatus, errorThrown){ alert("error - HTTP STATUS: "+jqXHR.status); }, complete: function(jqXHR, textStatus){ //alert("complete"); } }); return false; }); }); $(function(){ $("#getTimeStampButton").click(function(){ $.ajax({ url: "<%=request.getContextPath() %>/services/secure/timestamp/now", type: "GET", cache: false, dataType: "json", success: function (data, textStatus, jqXHR){ //alert("success"); if (data.status == "SUCCESS" ){ $("#timeStampContent").html("Timestamp: "+data.data); }else{ alert("failed"); } }, error: function (jqXHR, textStatus, errorThrown){ //alert("error - HTTP STATUS: "+jqXHR.status); if (textStatus == "parsererror"){ alert("You session has timed out"); //forward to welcomde page window.location.replace("https://"+window.location.host+"<%=request.getContextPath() %>/welcome.jsp"); } }, complete: function(jqXHR, textStatus){ //alert("complete"); } }); }); }); </script> </head> <body> <h1>You are logged in.</h1> <a id="logoutLink" href="<%=request.getContextPath() %>/services/auth/logout" >logout</a> <br/><br/> <button id="getTimeStampButton">Get Server Time</button> <br/><br/> <div id="timeStampContent"></div> <% Principal p = request.getUserPrincipal(); out.write("<br/><br/>"); if (p == null){ //if you get here the something is really wrong, because //you can only see that page if you have been authenticated //and therefore there is a principal available out.write("<div>Principal = NULL</div>"); }else{ out.write("<div>Principal.getName() = "+p.getName()+"</div>"); out.write("<div>request.getRemoteUser() = "+request.getRemoteUser()+"</div>"); out.write("<div>request.getAuthType() = "+request.getAuthType()+"</div>"); out.write("<div>request.isUserInRole(ADMINISTRATOR) = "+request.isUserInRole("ADMINISTRATOR") +"</div>"); out.write("<div>request.isUserInRole(USER) = "+request.isUserInRole("USER") +"</div>"); out.write("<div>request.isUserInRole(DEFAULT) = "+request.isUserInRole("DEFAULT") +"</div>"); out.write("<div>request.isUserInRole(CUSTOMER) = "+request.isUserInRole("CUSTOMER") +"</div>"); } %> </body> </html>
Our "Get Server Time" button calls /services/secure/timestamp/now via Ajax. This is the endpoint of our TimeService REST Service. The response is a very simple one without any logic. An example closer to the real world would be a getMyUserProfile Service. But this one here is easy enough to demonstrate what I have described above:
package com.nabisoft.service.me; import java.util.Date; import javax.ejb.Stateless; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import com.nabisoft.json.JsonResponse; @Path("/secure/timestamp") @Produces(MediaType.APPLICATION_JSON) @Stateless public class TimeService { @GET @Path("now") @Produces(MediaType.APPLICATION_JSON) public Response getCurrentDate(@Context HttpServletRequest req) { JsonResponse json = new JsonResponse("SUCCESS"); json.setData(new Date()); return Response.ok().entity(json).build(); } }
In case unauthenticated users try to access /secure/index.jsp they will be automatically redirected to our login.jsp (actually "redirected" is a wrong word here) - this is only a matter of Glassfish configuration. We also have to tell Glassfish what content is secured and what not. The configuration is placed in the web.xml and glassfish-web.xml. This kind of configuration is discussed in the next step.
We will start with the web.xml. The first interesting part is is the configuration of Jersey. As you can see everything that matches the url pattern /services/* is handled by the Jersey Servlet - so those requests are considered to be our REST services (of course, if they don't exist you will get an HTTP 404 error):
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <display-name>Authentication</display-name> <welcome-file-list> <welcome-file>welcome.jsp</welcome-file> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <!-- Jersey REST --> <servlet> <servlet-name>ServletAdaptor</servlet-name> <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class> <!-- this would allow us to get rid of @XmlRootElement --> <!-- but since we use jersey-json we don't need this --> <!-- <init-param> <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name> <param-value>true</param-value> </init-param> --> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>ServletAdaptor</servlet-name> <url-pattern>/services/*</url-pattern> </servlet-mapping> <listener> <listener-class>com.nabisoft.web.listener.EventListener</listener-class> </listener> <login-config> <auth-method>FORM</auth-method> <realm-name>userMgmtJdbcRealm</realm-name> <form-login-config> <form-login-page>/WEB-INF/login.jsp</form-login-page> <form-error-page>/WEB-INF/login.jsp?auth-error=1</form-error-page> </form-login-config> </login-config> <!-- you could also define a page that is displayed if glassfish determins that an authenticated user is not authorized to access a resource <error-page> <error-code>403</error-code> <location>/not-authorized.html</location> </error-page> --> <security-constraint> <!-- everything below /secure/* and /services/secure/* requires authentication --> <web-resource-collection> <web-resource-name>Secured Content</web-resource-name> <url-pattern>/secure/*</url-pattern> <url-pattern>/services/secure/*</url-pattern> </web-resource-collection> <!-- only users with at least one of these roles are allowed to access the secured content --> <auth-constraint> <role-name>ADMINISTRATOR</role-name> <role-name>USER</role-name> </auth-constraint> <!-- we always want https! --> <user-data-constraint> <description>highest supported transport security level</description> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint> <!-- declare the roles relevant for our webapp --> <security-role> <role-name>ADMINISTRATOR</role-name> </security-role> <security-role> <role-name>USER</role-name> </security-role> <security-role> <role-name>DEFAULT</role-name> </security-role> <session-config> <!-- on productive systems you might have another value for the timeout --> <session-timeout>5</session-timeout> <!-- we don't want to use the default name JSESSIONID because this tells everyone (especially hackers) that our application is based on java --> <cookie-config> <name>SESSIONID</name> </cookie-config> </session-config> </web-app>
Right after the Jersey/REST configuration I have configured a listener (see EventListener.java below). I thought it might be interesting to see what happens in the background, i.e. the listener tells you whenever a session has been destroyed. The source code is listed below. Finally we come to our login configuration. We will use form based login with our own login page. Please consider that our login config has specified userMgmtJdbcRealm as the realm name to be used (keep this in mind for later). Next we want to tell Glassfish that everything under /secure/* is secured content which requires users to be authenticated (login) and afterwards authorized if they want to access that content. We want only users that have at least one of the roles ADMINISTRATOR or USER to be allowed accessing our secured content. And last but not least we want to secure the communication via HTTPS by using CONFIDENTIAL for transport guarantee. This tells Glassfish to automatically redirect HTTP requests to HTTPS when trying to access restricted content. But as you might remember we want both the restricted content as well as our login/registration page to be accessible via HTTPS only. We don't want to accept HTTP for those pages! To achieve this we need to find a way to programically redirect all HTTP requests going to our login/registration page to their HTTPS equivalents. This can be easily achived by implementing a simple filter (see HttpToHttpsFilter.java below) that checks the protocol and always redirects to HTTPS in case we have an HTTP request. I have choosen to configure the filter via the @WebFilter("/") annotation instead of adding the corresponding configuration to the web.xml file. For demonstration I have only choosen "/" as the URL pattern that shall match our filter. I have choosen this URL pattern only because our user registration page is found there. The design I have choosen for implementing the filter is not good for re-using the filter. A better implementation would be to configure the filter via web.xml and to pass some init-params which would tell the filter which URL patterns should be redirected to HTTPS and which port to use for HTTPS. But we are not going to make it too complicated here :-) You could also implement the redirection directly in the corresponding /index.jsp file instead of using a filter. Actually, this would be better because it's simply a waste to have a filter for exactly one URL pattern. I just wanted to show you how to use a filter with annotations which allows you to switch from HTTP to HTTPS. Let me try to come to a conclusion: Glassfish will automatically ask for login in case an unauthenticated user tries to access secured content by showing our custom login page (see /WEB-INF/login.jsp above). Glassfish will also make sure that only HTTPS requests are allowed for our secured content. If our login form is displayed automatically by Glassfish then Glassfish also makes sure to use HTTPS here as well. Our HttpToHttpsFilter will only redirect from HTTP to HTTPS if the requested resource is our registration page that can be found at /index.jsp (see above).
package com.nabisoft.web.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; //instead of defining the filter in our web.xml we will use an annotation @WebFilter("/welcome.jsp") public class HttpToHttpsFilter implements Filter { @Override public void init(FilterConfig config) throws ServletException { // if you have any init-params in web.xml then you could retrieve them // here by calling config.getInitParameter("my-init-param-name"). // example: you could define a comma separated list of paths that should be // checked for http to https redirection } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; System.out.println("HttpToHttpsFilter: URL requested = "+request.getRequestURL().toString()); if ( !request.isSecure() ) { String url = request.getRequestURL().toString().replaceFirst("http", "https"); url = url.replaceFirst(":8080/", ":8181/"); //quick and dirty!!! //don't forget to add the parameters if (request.getQueryString() != null) url += "?" + request.getQueryString(); System.out.println("HttpToHttpsFilter redirect to: "+url); response.sendRedirect(url); } else { chain.doFilter(req, res); // we already have a https connection ==> so just continue request } } @Override public void destroy() { // release resources if you have any } }
package com.nabisoft.web.listener; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.http.HttpSessionAttributeListener; import javax.servlet.http.HttpSessionBindingEvent; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; public class EventListener implements ServletContextListener, HttpSessionAttributeListener, HttpSessionListener { /** * The servlet context with which we are associated. */ private ServletContext context = null; /** * Record the fact that a servlet context attribute was added. * * @param event * The session attribute event */ public void attributeAdded(HttpSessionBindingEvent event) { log("attributeAdded('" + event.getSession().getId() + "', '" + event.getName() + "', '" + event.getValue() + "')"); } /** * Record the fact that a servlet context attribute was removed. * * @param event * The session attribute event */ public void attributeRemoved(HttpSessionBindingEvent event) { log("attributeRemoved('" + event.getSession().getId() + "', '" + event.getName() + "', '" + event.getValue() + "')"); } /** * Record the fact that a servlet context attribute was replaced. * * @param event * The session attribute event */ public void attributeReplaced(HttpSessionBindingEvent event) { log("attributeReplaced('" + event.getSession().getId() + "', '" + event.getName() + "', '" + event.getValue() + "')"); } /** * Record the fact that this web application has been destroyed. * * @param event * The servlet context event */ public void contextDestroyed(ServletContextEvent event) { log("contextDestroyed()"); this.context = null; } /** * Record the fact that this web application has been initialized. * * @param event * The servlet context event */ public void contextInitialized(ServletContextEvent event) { this.context = event.getServletContext(); log("contextInitialized()"); } /** * Record the fact that a session has been created. * * @param event * The session event */ public void sessionCreated(HttpSessionEvent event) { log("sessionCreated('" + event.getSession().getId() + "')"); } /** * Record the fact that a session has been destroyed. * * @param event * The session event */ public void sessionDestroyed(HttpSessionEvent event) { log("sessionDestroyed('" + event.getSession().getId() + "')"); } /** * Log a message to the servlet context application log. * * @param message * Message to be logged */ private void log(String message) { if (context != null) context.log("EventListener: " + message); else System.out.println("EventListener: " + message); } /** * Log a message and associated exception to the servlet context application * log. * * @param message * Message to be logged * @param throwable * Exception to be logged */ private void log(String message, Throwable throwable) { if (context != null) context.log("EventListener: " + message, throwable); else { System.out.println("EventListener: " + message); throwable.printStackTrace(System.out); } } }
In our web.xml we have declared some roles as well as userMgmtJdbcRealm as the realm name to be used for our login configuration (see above). As you will see soon below userMgmtJdbcRealm is a jdbcRealm. Glassfish will use it for authentication and authorization. For authorization Glassfish will use our jdbcRealm to find out which Groups are assigned to the authenticated user. Now in the glassfish-web.xml the Groups retrieved from the database have to be mapped to Roles that our application knows. As a naming convention I have used the same name for both Groups and Roles. Please also compare this to our Group Enum which we have defined in Step 1 (see above). Before we go to Step 6 please have a look at our glassfish-web.xml and make sure not to get confused about Roles and Groups and how they work together:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd"> <glassfish-web-app error-url=""> <context-root>/AuthenticationDemo</context-root> <!-- role mapping --> <security-role-mapping> <role-name>USER</role-name> <group-name>USER</group-name> </security-role-mapping> <security-role-mapping> <role-name>DEFAULT</role-name> <group-name>DEFAULT</group-name> </security-role-mapping> <security-role-mapping> <role-name>ADMINISTRATOR</role-name> <group-name>ADMINISTRATOR</group-name> </security-role-mapping> <!-- default --> <class-loader delegate="true"/> <jsp-config> <property name="keepgenerated" value="true"> <description>Keep a copy of the generated servlet class' java code.</description> </property> </jsp-config> </glassfish-web-app>
Now we want to create a jdbcRealm which is used by Glassfish for authentication and authorization. You could do this by using the Admin Concole of Glassfish or by using an asadmin command. I prefere the asadmin command, please hee http://docs.oracle.com/cd/E18930_01/html/821-2433/create-auth-realm-1.html for more details:
#one liner for copy and paste asadmin create-auth-realm --classname com.sun.enterprise.security.auth.realm.jdbc.JDBCRealm --property jaas-context=jdbcRealm:datasource-jndi="jdbc/auth":user-table=users:user-name-column=email:password-column=password:group-table=users_groups:group-name-column=groupname:digest-algorithm=SHA-512 userMgmtJdbcRealm #better to read (do not copy and paste this => use previous line for copy and paste!!): asadmin create-auth-realm --classname com.sun.enterprise.security.auth.realm.jdbc.JDBCRealm --property jaas-context=jdbcRealm :datasource-jndi="jdbc/auth" :user-table=users :user-name-column=email :password-column=password :group-table=users_groups :group-name-column=groupname :digest-algorithm=SHA-512 userMgmtJdbcRealm
The asadmin command will create a jdbcRealm. The new jdbcRealm is called userMgmtJdbcRealm and will use our jdbc/auth jdbc resource which we have created in Step 2 (see above). We have also to tell where our user table can be found and which columns represent the user name and user password. The user table is called USERS, the user name column is email and the password column is password. Besides that we also have o specify the name of our group table (in our case USERS_GROUPS) as well as the column that specifies the groupname (in our case groupname). We also have specified to use SHA-512 as digest algorithm. We don't have to specify HEX for encoding because HEX is the default encoding in case digest-algorithm is specified. Please compare all that to our User Management Data Model which we have defined in Step 1 (see above).
When deploying the application to your local Glassfish just call http://localhost:8080/AuthenticationDemo/secure/index.jsp and see what happens (I assume you have deployed to "AuthenticationDemo", the HTTP port is 8080 and the HTTPS port is 8181). You will see the login page we have defined at /WEB-INF/login.jsp and the protocol has changed from HTTP to HTTPS. Before you create a new User Account try to access the same URL via HTTPS: https://localhost:8181/AuthenticationDemo/secure/index.jsp. As you can see the same login page shows up again. Now go to http://localhost:8080/AuthenticationDemo/ to see what happens here. The HttpToHttpsFilter we have implemented will forward you to https://localhost:8181/AuthenticationDemo/. On that page you can create a new User Account. Once you have created a User Account you are automatically forwarded to https://localhost:8181/AuthenticationDemo/secure/index.jsp. Just to make sure, please try to access https://localhost:8181/AuthenticationDemo/secure/index.jsp directly in a new tab. This time no login screen shows up because the register service executes a login for the just created user. Next you could play with logout and login again. Have fun.
Instead of creating your Eclipse or NetBeans project according to this tutorial you might prefer to download the preconfigured project I offer. This might save you some time because you can import it into Eclipse IDE and have a runnung example within only a few minutes (or even seconds). Make sure you have already installed Glassfish 3.1.2 and added it to your IDE. You should also have a PostgreSQL already installed on your machine. The Eclipse project has been created using Eclipse 3.7.2 JEE. The NetBeans project has been created usin NetBeans 7.1.1.