Secure your business layer with JSR-250 and Spring Security in a JSF application

In the previouspost we looked at the different option that we have as part of Java EE and Spring security framework. In this tutorial we will see how we can leverage JSR-250 security annotation.

To being with you need the source code from previous post. With some code behind us, let’s see how JSR-250 can be used to secure business methods.

First let’s modify our secured.xhtml, where we will add one extra commandLink to navigate to next page. This commandLink should only be visible to admin user but to showcase the usage of JSR-250 we will randomly make it visible to any logged in user.

secured.xhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:h="http://java.sun.com/jsf/html"
	xmlns:f="http://java.sun.com/jsf/core"
	xmlns:ui="http://java.sun.com/jsf/facelets">
<h:head>
</h:head>
<h:body>
	<h:form>
		<h:outputText value="This message is for admin only" id="adminonlyoutputtext" rendered="#{securityBean.isUserAdmin}"></h:outputText>
		<br/>
		<h:outputText value="This message is for user only" id="guestonlyoutputtext" rendered="#{securityBean.isUser}"></h:outputText>
		<br/>
		<h:commandLink action="#{navigator.showAdminPersonalPage}" rendered="#{securityBean.showAdminPersonalPage}" value="MyPersonalPage"></h:commandLink>
		<br/>
		<h:outputLabel value="I am secured, if you are seeing me it means you have logged in correctly, great!"></h:outputLabel>
		<br/>
		<h:commandLink id="logout" action="#{loginMgmtBean.logout}" value="Logout"></h:commandLink>
		<h:messages></h:messages>
	</h:form>
</h:body>
</html>

Second we will modify our Navigator.java and add implementation for showAdminPersonalPage, which will invoke the business layer which is secured and it requires admin to be logged in.

Navigator.java


package com.mumz.jsfspringsec.beans;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.ManagedProperty;
import javax.faces.bean.SessionScoped;

import com.mumz.jsfspringsec.business.model.BusinessModel;

/**
 * The Class Navigator.
 * @author prabhat.jha
 */
@ManagedBean(name="navigator")
@SessionScoped
public class Navigator {
	
	/** The page to navigate. */
	private String pageToNavigate = "";
	
	/** The business model. */
	@ManagedProperty(value="businessModel")
	private BusinessModel businessModel = null;
	
	/** The latest news. */
	private String latestNews = null;
	
	/**
	 * Navigate to.
	 *
	 * @return the string
	 */
	public String navigateTo(){
		if("ToSecure".equalsIgnoreCase(pageToNavigate)){
			return "Secured";
		} else if("ToUnSecure".equalsIgnoreCase(pageToNavigate)){
			return "UnSecured";
		} else {
			//This will never happen but we will use this to extend this application
			return "none";
		}
	}
	
	/**
	 * Show admin personal page.
	 *
	 * @return the string
	 */
	public String showAdminPersonalPage(){
		this.setLatestNews(this.getBusinessModel().getBusinessLatestNews());
		return "adminPersonalDetails.xhtml";
	}
	
	/**
	 * Gets the page to navigate.
	 *
	 * @return the page to navigate
	 */
	public String getPageToNavigate() {
		return pageToNavigate;
	}
	
	/**
	 * Sets the page to navigate.
	 *
	 * @param option the new page to navigate
	 */
	public void setPageToNavigate(String option) {
		this.pageToNavigate = option;
	}

	/**
	 * Sets the business model.
	 *
	 * @param businessModel the new business model
	 */
	public void setBusinessModel(BusinessModel businessModel) {
		this.businessModel = businessModel;
	}

	/**
	 * Gets the business model.
	 *
	 * @return the business model
	 */
	public BusinessModel getBusinessModel() {
		return businessModel;
	}

	/**
	 * Sets the latest news.
	 *
	 * @param latestNews the new latest news
	 */
	public void setLatestNews(String latestNews) {
		this.latestNews = latestNews;
	}

	/**
	 * Gets the latest news.
	 *
	 * @return the latest news
	 */
	public String getLatestNews() {
		return latestNews;
	}
}

Third our business layer which happens to be a very simple class but will suffice for our use case.

BusinessModel.java


package com.mumz.jsfspringsec.business.model;

import javax.annotation.security.RolesAllowed;

/**
 * The Class BusinessModel.
 * @author prabhat.jha
 */
public class BusinessModel {
	
	/**
	 * Gets the business latest news.
	 *
	 * @return the business latest news
	 */
	@RolesAllowed(value="ROLE_ADMIN")
	public String getBusinessLatestNews(){
		return "Business is doing great!";
	}
}

Fourth we will update our SecurityHolderBean and add security information for ROLE_USER all this time we were playing with ROLE_ADMIN and ROLE_GUEST.

SecurityHolderBean.java


package com.mumz.jsfspringsec.beans;

import java.util.Random;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

/**
 * The Class SecurityHolderBean.
 * @author prabhat.jha
 */
@ManagedBean(name = "securityBean")
@SessionScoped
public class SecurityHolderBean {
	
	/** The is user admin. */
	private boolean isUserAdmin = false;
	
	/** The is user guest. */
	private boolean isUserGuest = false;
	
	/** The is user. */
	private boolean isUser = false;
	
	/** The show admin personal page. */
	private boolean showAdminPersonalPage = false;
	
	/**
	 * Gets the checks if is user admin.
	 * 
	 * @return the checks if is user admin
	 */
	public boolean getIsUserAdmin() {
		isUserAdmin = false;
		Authentication authentication = SecurityContextHolder.getContext()
				.getAuthentication();
		for (GrantedAuthority grantedAuthority : authentication
				.getAuthorities()) {
			if ("ROLE_ADMIN".equals(grantedAuthority.getAuthority())) {
				isUserAdmin = true;
				break;
			}
		}
		return isUserAdmin;
	}

	/**
	 * Gets the checks if is user guest.
	 * 
	 * @return the checks if is user guest
	 */
	public boolean getIsUserGuest() {
		isUserGuest = false;
		Authentication authentication = SecurityContextHolder.getContext()
				.getAuthentication();
		for (GrantedAuthority grantedAuthority : authentication
				.getAuthorities()) {
			if ("ROLE_GUEST".equals(grantedAuthority.getAuthority())) {
				isUserGuest = true;
				break;
			}
		}
		return isUserGuest;
	}

	/**
	 * Sets the user admin.
	 * 
	 * @param isUserAdmin
	 *            the new user admin
	 */
	public void setUserAdmin(boolean isUserAdmin) {
		this.isUserAdmin = isUserAdmin;
	}

	/**
	 * Sets the user guest.
	 * 
	 * @param isUserGuest
	 *            the new user guest
	 */
	public void setUserGuest(boolean isUserGuest) {
		this.isUserGuest = isUserGuest;
	}

	/**
	 * Sets the user.
	 * 
	 * @param isUser
	 *            the new user
	 */
	public void setIsUser(boolean isUser) {
		this.isUser = isUser;
	}

	/**
	 * Checks if is user.
	 * 
	 * @return true, if is user
	 */
	public boolean getIsUser() {
		isUser = false;
		Authentication authentication = SecurityContextHolder.getContext()
				.getAuthentication();
		for (GrantedAuthority grantedAuthority : authentication
				.getAuthorities()) {
			if ("ROLE_USER".equals(grantedAuthority.getAuthority())) {
				isUser = true;
				break;
			}
		}
		return isUser;
	}

	/**
	 * Sets the show admin personal page.
	 * 
	 * @param showAdminPersonalPage
	 *            the new show admin personal page
	 */
	public void setShowAdminPersonalPage(boolean showAdminPersonalPage) {
		this.showAdminPersonalPage = showAdminPersonalPage;
	}

	/**
	 * Gets the show admin personal page.
	 * 
	 * @return the show admin personal page
	 */
	public boolean getShowAdminPersonalPage() {
		showAdminPersonalPage = false;
		if(getIsUserAdmin()){
			showAdminPersonalPage = true;
		} else {
			/**
			 * Randomly decide whether to show admin Personal Page or not.
			 */
			Random random = new Random();
			if(random.nextInt(2) % 2 == 0){
				showAdminPersonalPage = true;
			}
		}
		return showAdminPersonalPage;
	}
}

Fifth since all this while we have been using JSF explicit navigation we will follow the same and update our faces-config.xml to add navigation to move to AdminPersonal page.

faces-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<faces-config
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
    version="2.0">
    <!-- Enable Spring -->
    <application>
    	<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
    </application>
    <!-- Simple Navigation Rule -->
    <!-- If  user keys in ToSecure, move to /pages/secure/secured.xhtml-->
    <!-- Else If user keys in ToUnSecure, move to /pages/unsecure/unsecured.xhtml-->
    <navigation-rule>
    	<display-name>pages/home/home.xhtml</display-name>
    	<from-view-id>/pages/home/home.xhtml</from-view-id>
    	<navigation-case>
    		<from-action>#{navigator.navigateTo}</from-action>
    		<from-outcome>Secured</from-outcome>
    		<to-view-id>/pages/secure/secured.xhtml</to-view-id>
    		<redirect></redirect>
    	</navigation-case>
    </navigation-rule>
    <navigation-rule>
    	<display-name>pages/home/home.xhtml</display-name>
    	<from-view-id>/pages/home/home.xhtml</from-view-id>
    	<navigation-case>
    		<from-action>#{navigator.navigateTo}</from-action>
    		<from-outcome>UnSecured</from-outcome>
    		<to-view-id>/pages/unsecure/unsecured.xhtml</to-view-id>
    		<redirect></redirect>
    	</navigation-case>
    </navigation-rule>
    <navigation-rule>
    	<display-name>pages/secure/secured.xhtml</display-name>
    	<from-view-id>/pages/secure/secured.xhtml</from-view-id>
    	<navigation-case>
    		<from-action>#{loginMgmtBean.logout}</from-action>
    		<from-outcome>loggedout</from-outcome>
    		<to-view-id>/pages/home/home.xhtml</to-view-id>
    		<redirect></redirect>
    	</navigation-case>
    </navigation-rule>
    <navigation-rule>
    	<display-name>pages/common/login.xhtml</display-name>
    	<from-view-id>/pages/common/login.xhtml</from-view-id>
    	<navigation-case>
    		<from-action>#{loginMgmtBean.login}</from-action>
    		<from-outcome>Secured</from-outcome>
    		<to-view-id>/pages/secure/secured.xhtml</to-view-id>
    	</navigation-case>
    </navigation-rule>
    <navigation-rule>
    	<display-name>pages/secure/secured.xhtml</display-name>
    	<from-view-id>/pages/secure/secured.xhtml</from-view-id>
    	<navigation-case>
    		<from-action>#{navigator.showAdminPersonalPage}</from-action>
    		<from-outcome>adminPersonalDetails</from-outcome>
    		<to-view-id>/pages/secure/adminPersonalDetails.xhtml</to-view-id>
    		<redirect />
    	</navigation-case>
    </navigation-rule>
</faces-config>

Sixth we will update our bean configuration as we are not using auto wiring capability of Spring.

jsf-spring-sec-bean-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:sec="http://www.springframework.org/schema/security"
	xsi:schemaLocation="
	   http://www.springframework.org/schema/beans
	   http://www.springframework.org/schema/beans/spring-beans-3.1.xsd 
       http://www.springframework.org/schema/security
	   http://www.springframework.org/schema/security/spring-security-3.1.xsd">
       
	<beans:bean id="navigator" name="navigator" class="com.mumz.jsfspringsec.beans.Navigator" scope="session">
		<beans:property name="businessModel" ref="businessModel"></beans:property>
	</beans:bean>
	
	<beans:bean id="loginBean" name="loginBean" class="com.mumz.jsfspringsec.beans.LoginBean" scope="prototype">
		<beans:property name="authenticationManager" ref="authenticationManager"></beans:property>
		<beans:property name="rememberMeServices" ref="rememberMeServices"></beans:property>
		<beans:property name="userDetailsService" ref="customjdbcUserService"></beans:property>
	</beans:bean>
	
	<beans:bean id="securityBean" class="com.mumz.jsfspringsec.beans.SecurityHolderBean" scope="session">
	</beans:bean>
	
	<beans:bean id="businessModel" class="com.mumz.jsfspringsec.business.model.BusinessModel" scope="prototype"></beans:bean>
	
	<beans:bean id="dataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">
	 	<beans:property name="driverClassName" value="com.mysql.jdbc.Driver" />
		<beans:property name="url" value="jdbc:mysql://localhost:3306/jsf-spring-security" />
		<beans:property name="username" value="root" />
		<beans:property name="password" value="root" />
   </beans:bean>
   
   <beans:bean id="customjdbcUserService" class="com.mumz.jsfspringsec.dao.CustomJDBCDaoImpl">
   		<beans:property name="dataSource" ref="dataSource"/>
		<beans:property name="enableAuthorities" value="false"/>
		<beans:property name="enableGroups" value="true"></beans:property>
		<beans:property name="usersByUsernameQuery">
			<beans:value>SELECT JSF_SPRING_SEC_USERS_USERNAME, JSF_SPRING_SEC_USERS_PASSWORD, JSF_SPRING_SEC_USERS_ENABLED FROM JSF_SPRING_SEC_USERS WHERE JSF_SPRING_SEC_USERS_USERNAME = ?</beans:value>
		</beans:property>
		<beans:property name="authoritiesByUsernameQuery">
			<beans:value>
				 SELECT JSF_SPRING_SEC_ROLES_USERNAME,JSF_SPRING_SEC_ROLES_ROLE_NAME from JSF_SPRING_SEC_ROLES where JSF_SPRING_SEC_ROLES_USERNAME = ?
			</beans:value>
		</beans:property>
		<beans:property name="groupAuthoritiesByUsernameQuery">
			<beans:value>
				SELECT 
						GROUPDTLS.JSF_SPRING_GROUPS_GROUP_ID, 
						GROUPDTLS.JSF_SPRING_GROUPS_GROUP_NAME, 
						GROUPPERMISSION.JSF_SPRING_SEC_GROUP_AUTHORITIES_AUTHORITY 
				FROM 
						JSF_SPRING_GROUPS GROUPDTLS, 
						JSF_SPRING_SEC_GROUP_AUTHORITIES GROUPPERMISSION, 
						JSF_SPRING_SEC_GROUP_MEMBERS GROUPMEMBERS, 
						JSF_SPRING_SEC_USERS USERS 
				WHERE 
						USERS.JSF_SPRING_SEC_USERS_USERNAME = ? AND 
						GROUPMEMBERS.JSF_SPRING_SEC_GROUP_MEMBERS_USER_ID = USERS.PK_JSF_SPRING_SEC_USERS AND 
						GROUPMEMBERS.JSF_SPRING_SEC_GROUP_MEMBERS_GROUP_ID = GROUPDTLS.JSF_SPRING_GROUPS_GROUP_ID AND 
						GROUPPERMISSION.JSF_SPRING_SEC_GROUP_AUTHORITIES_GROUP_ID = GROUPDTLS.JSF_SPRING_GROUPS_GROUP_ID 
			</beans:value>
		</beans:property>
   </beans:bean>
   
	<beans:bean id="rememberMeServices"
		class="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
		<beans:property name="key" value="jsfspring-sec" /> 
		<beans:property	name="userDetailsService" ref="customjdbcUserService" /> 
		<beans:property	name="alwaysRemember" value="true" /> 
		<beans:property	name="tokenValiditySeconds" value="60" /> 
	</beans:bean>
	
	<beans:bean id="rememberMeAuthenticationProvider" 
		class="org.springframework.security.authentication.RememberMeAuthenticationProvider">
  		<beans:property name="key" value="jsfspring-sec"/>
	</beans:bean>
	
	<beans:bean id="rememberMeFilter" 
		class="org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
  		<beans:property name="rememberMeServices" ref="rememberMeServices"/>
  		<beans:property name="authenticationManager" ref="authenticationManager" />
	</beans:bean>
	
	<beans:bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" id="passwordEncoder">
	</beans:bean>
	
	<beans:bean id="databasePasswordEncrypter" class="com.mumz.jsfspringsec.dao.security.DBPasswordEncrypterBean" init-method="encryptDBPassword" depends-on="dataSource">
		<beans:property name="passwordEncoder" ref="passwordEncoder"></beans:property>
		<beans:property name="dataSource" ref="dataSource"></beans:property>
		<beans:property name="selectQuery">
			<beans:value>SELECT JSF_SPRING_SEC_USERS_USERNAME, JSF_SPRING_SEC_USERS_PASSWORD, JSF_SPRING_SEC_USERS_ENCRYPTED FROM JSF_SPRING_SEC_USERS WHERE (JSF_SPRING_SEC_USERS_ENCRYPTED='' || JSF_SPRING_SEC_USERS_ENCRYPTED IS NULL)</beans:value>
		</beans:property>
		<beans:property name="updateQuery">
			<beans:value>UPDATE JSF_SPRING_SEC_USERS SET JSF_SPRING_SEC_USERS_PASSWORD = ?, JSF_SPRING_SEC_USERS_ENCRYPTED='YES' WHERE JSF_SPRING_SEC_USERS_USERNAME = ? </beans:value>
		</beans:property>
	</beans:bean>
</beans:beans>

Final step is to enable JSF-250 so we will update our jsf-spring-sec-security-config.xml.

jsf-spring-sec-security-config.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans:beans
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:sec="http://www.springframework.org/schema/security"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xsi:schemaLocation="
	   http://www.springframework.org/schema/beans
	   http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
	   http://www.springframework.org/schema/security
	   http://www.springframework.org/schema/security/spring-security-3.1.xsd">
	   
	 <sec:http auto-config="true" use-expressions="true">
		<sec:intercept-url pattern="/pages/secure/**" access="hasRole('ROLE_USER')" />
		<sec:intercept-url pattern="/pages/unsecure/**" access="permitAll"/>
		<sec:intercept-url pattern="/pages/common/**" access="permitAll"/>
		<sec:intercept-url pattern="/**" access="permitAll"/>
		<sec:form-login login-page="/pages/common/login.jsf"/>
		<sec:remember-me key="jsfspring-sec" services-ref="rememberMeServices"/>
		<sec:logout 
			invalidate-session="true" 
			delete-cookies="JSESSIONID,SPRING_SECURITY_REMEMBER_ME_COOKIE" 
			logout-success-url="/pages/common/login.jsf"></sec:logout>
	</sec:http>
	
	<sec:authentication-manager alias="authenticationManager">
		<sec:authentication-provider ref="rememberMeAuthenticationProvider"></sec:authentication-provider>
		<sec:authentication-provider user-service-ref="customjdbcUserService">
			<sec:password-encoder ref="passwordEncoder">
			</sec:password-encoder>
		</sec:authentication-provider>
	</sec:authentication-manager>
	
	<sec:global-method-security jsr250-annotations="enabled"></sec:global-method-security>
</beans:beans>

That’s all, before running this example remember to have a user whose has authority of ROLE_USER. If you login as user randomly you will see MyPeronalPage link, which is meant only for Admin. When you try to access it you will get an Not Authorized Exception. In our current implementation we have intentionally chosen to show that link to user but it is good enough to realize our use case of business layer security with JSR-250.

Data dump is given below:

INSERT INTO `jsf_spring_sec_users` (`PK_JSF_SPRING_SEC_USERS`, `JSF_SPRING_SEC_USERS_USERNAME`, `JSF_SPRING_SEC_USERS_PASSWORD`, `JSF_SPRING_SEC_USERS_ENABLED`, `JSF_SPRING_SEC_USERS_CREATED_DT`, `JSF_SPRING_SEC_USERS_MODIFIED_DT`) VALUES (1,'admin','admin','true','2012-07-17 10:07:41','2012-07-17 10:07:41'),(2,'guest','guest','true','2012-07-17 16:59:13','2012-07-17 16:59:13','YES'),(3,'user','user','true','2012-07-25 16:59:13','2012-07-25 16:59:13');

INSERT INTO `jsf_spring_sec_groups` (`JSF_SPRING_SEC_GROUPS_GROUP_ID`, `JSF_SPRING_SEC_GROUPS_GROUP_NAME`) 
VALUES (1,'ADMIN'),(2,'GUEST'),(3,'USER');

INSERT INTO `jsf_spring_sec_group_authorities` (`JSF_SPRING_SEC_GROUP_AUTHORITIES_AUTHORITY_ID`, `JSF_SPRING_SEC_GROUP_AUTHORITIES_GROUP_ID`, `JSF_SPRING_SEC_GROUP_AUTHORITIES_AUTHORITY`) VALUES (1,1,'ROLE_USER'),(2,2,'ROLE_GUEST'),(3,1,'ROLE_ADMIN'),(4,3,'ROLE_USER');

INSERT INTO `jsf_spring_sec_group_members` (`JSF_SPRING_SEC_GROUP_MEMBERS_ID`, `JSF_SPRING_SEC_GROUP_MEMBERS_GROUP_ID`, `JSF_SPRING_SEC_GROUP_MEMBERS_USER_ID`) VALUES (1,1,1),(2,2,2),(3,3,3);

There is one comment

  1. Spyros Non Serviam

    Why am I getting a NullPointerException when I check the remember me button?
    Also I am getting an access denied when I try logout after I was logged in as Guest.
    Can you please help me fix them?

Leave a reply to Spyros Non Serviam Cancel