Implement custom UserDetailsService for spring security
In the last article, we have only one user name and password. In this post, we will extend the class and add more user name and passwords.
This is not complete implementation, later we will see how to connect database and check the user name and password, But this will help you how to start on that.
Technologies used in this example:
1. ZK 6.5.1 CE Version.
2. Spring-security-core 3.0.5
Step 1:
Download this document and setup the development environment.
Step 2:
Let us create ZK Maven Project. Follow the below instruction.
In the Eclipse IDE, Select File –> New-> Other-> Maven Project as shown here.
Click Next.
Uncheck the option “Create a simple Project(skip archetype selection) and Click next.
Select “ZK” in the catalog and select “ZK-archetype-webapp 6.0” as shown here."
Click Next after selecting the ZK Archetype.
Enter “zkloginexample3” for Group ID, Artifact id and Package. And important, please select “ZK-Version-since” from the bottom list and then select finish.
Now, the following folder structure will be created.
Step 3:
Next we will include the spring dependency in the POM File.
Double click the POM.XML file in the project navigator.
Click on the POM.XML tab. The following modification has been done.
1. Change the ZK Version to latest CE Version.
2. Remove the Maven ZK EE Version Repository.
3. Add the spring security dependency.
Here is the modified POM.XML file.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>zkloginexample1</groupId>
<artifactId>zkloginexample1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<zk.version>6.5.1</zk.version>
<commons-io>1.3.1</commons-io>
<maven.build.timestamp.format>yyyy-MM-dd</maven.build.timestamp.format>
<packname>-${project.version}-FL-${maven.build.timestamp}</packname>
</properties>
<packaging>war</packaging>
<name>The zkloginexample1 Project</name>
<description>The zkloginexample1 Project</description>
<licenses>
<license>
<name>GNU LESSER GENERAL PUBLIC LICENSE, Version 3</name>
<url>http://www.gnu.org/licenses/lgpl.html</url>
<distribution>repo</distribution>
</license>
</licenses>
<repositories>
<repository>
<id>ZK CE</id>
<name>ZK CE Repository</name>
<url>http://mavensync.zkoss.org/maven2</url>
</repository>
<repository>
<id>ZK EVAL</id>
<name>ZK Evaluation Repository</name>
<url>http://mavensync.zkoss.org/eval</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>zkmaven</id>
<name>ZK Maven Plugin Repository</name>
<url>http://mavensync.zkoss.org/maven2/</url>
</pluginRepository>
</pluginRepositories>
<dependencies>
<dependency>
<groupId>org.zkoss.zk</groupId>
<artifactId>zkbind</artifactId>
<version>${zk.version}</version>
</dependency>
<dependency>
<groupId>org.zkoss.zk</groupId>
<artifactId>zul</artifactId>
<version>${zk.version}</version>
</dependency>
<dependency>
<groupId>org.zkoss.zk</groupId>
<artifactId>zkplus</artifactId>
<version>${zk.version}</version>
</dependency>
<dependency>
<groupId>org.zkoss.zk</groupId>
<artifactId>zhtml</artifactId>
<version>${zk.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io}</version>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>3.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>3.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>3.0.5.RELEASE</version>
</dependency>
<!-- ZK 5 breeze theme <dependency> <groupId>org.zkoss.theme</groupId>
<artifactId>breeze</artifactId> <version>${zk.version}</version> <optional>true</optional>
</dependency> -->
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- Run with Jetty -->
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.10</version>
<configuration>
<scanIntervalSeconds>5</scanIntervalSeconds>
<stopKey>foo</stopKey>
<stopPort>9999</stopPort>
</configuration>
<executions>
<execution>
<id>start-jetty</id>
<phase>pre-integration-test</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<scanIntervalSeconds>0</scanIntervalSeconds>
<daemon>true</daemon>
</configuration>
</execution>
<execution>
<id>stop-jetty</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Compile java -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<!-- Build war -->
<plugin>
<artifactId>maven-war-plugin</artifactId>
<groupId>org.apache.maven.plugins</groupId>
<version>2.1.1</version>
</plugin>
<!-- Pack zips -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2</version>
<executions>
<execution>
<id>webapp</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>zkloginexample1${packname}</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>assembly/webapp.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Step 4:
Next step we will update web.xml to take care spring filter.
Here is the updated web.xml file.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<description><![CDATA[My ZK Application]]></description>
<display-name>zkloginexample1</display-name>
<!-- ====================================================== -->
<!-- CONFIGURATION FILES -->
<!-- ====================================================== -->
<!-- =========================================================================================================== -->
<!-- The security configuration is done in XML file and can have any name such as applicationContext-security.xml. -->
<!-- This file needs to be loaded explicitly from web.xml. This is done by adding ContextLoadListener. -->
<!-- The following lines needs to be added before security filter definition in web.xml. -->
<!-- =========================================================================================================== -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring-security.xml
</param-value>
</context-param>
<!-- ====================================================== -->
<!-- SPRING CONTEXT LOADER -->
<!-- ====================================================== -->
<!-- The org.springframework.web.context.ContextLoaderListener class uses a context parameter called contextConfigLocation to
determine the location of the Spring configuration file. The context parameter is configured using the context-parameter element.
The context-param element has two children that specify parameters and their values.
The param-name element specifies the parameter's name. The param-value element specifies the parameter's value
-->
<listener>
<display-name>Spring Context Loader</display-name>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Spring Security -->
<!-- A servlet filter capturing every user requests and sending them to the configured security filters to make sure access is authorized. -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- //// -->
<!-- ZK -->
<listener>
<description>ZK listener for session cleanup</description>
<listener-class>org.zkoss.zk.ui.http.HttpSessionListener</listener-class>
</listener>
<servlet>
<description>ZK loader for ZUML pages</description>
<servlet-name>zkLoader</servlet-name>
<servlet-class>org.zkoss.zk.ui.http.DHtmlLayoutServlet</servlet-class>
<!-- Must. Specifies URI of the update engine (DHtmlUpdateServlet). It
must be the same as <url-pattern> for the update engine. -->
<init-param>
<param-name>update-uri</param-name>
<param-value>/zkau</param-value>
</init-param>
<!-- Optional. Specifies whether to compress the output of the ZK loader.
It speeds up the transmission over slow Internet. However, if you configure
a filter to post-processing the output, you might have to disable it. Default:
true <init-param> <param-name>compress</param-name> <param-value>true</param-value>
</init-param> -->
<!-- [Optional] Specifies the default log level: OFF, ERROR, WARNING, INFO,
DEBUG and FINER. If not specified, the system default is used. <init-param>
<param-name>log-level</param-name> <param-value>OFF</param-value> </init-param> -->
<load-on-startup>1</load-on-startup><!-- Must -->
</servlet>
<servlet-mapping>
<servlet-name>zkLoader</servlet-name>
<url-pattern>*.zul</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>zkLoader</servlet-name>
<url-pattern>*.zhtml</url-pattern>
</servlet-mapping>
<!-- [Optional] Uncomment it if you want to use richlets. <servlet-mapping>
<servlet-name>zkLoader</servlet-name> <url-pattern>/zk/*</url-pattern> </servlet-mapping> -->
<servlet>
<description>The asynchronous update engine for ZK</description>
<servlet-name>auEngine</servlet-name>
<servlet-class>org.zkoss.zk.au.http.DHtmlUpdateServlet</servlet-class>
<!-- [Optional] Specifies whether to compress the output of the ZK loader.
It speeds up the transmission over slow Internet. However, if your server
will do the compression, you might have to disable it. Default: true <init-param>
<param-name>compress</param-name> <param-value>true</param-value> </init-param> -->
<!-- [Optional] Specifies the AU extension for particular prefix. <init-param>
<param-name>extension0</param-name> <param-value>/upload=com.my.MyUploader</param-value>
</init-param> -->
</servlet>
<servlet-mapping>
<servlet-name>auEngine</servlet-name>
<url-pattern>/zkau/*</url-pattern>
</servlet-mapping>
<!-- [Optional] Uncomment if you want to use the ZK filter to post process
the HTML output generated by other technology, such as JSP and velocity.
<filter> <filter-name>zkFilter</filter-name> <filter-class>org.zkoss.zk.ui.http.DHtmlLayoutFilter</filter-class>
<init-param> <param-name>extension</param-name> <param-value>html</param-value>
</init-param> <init-param> <param-name>compress</param-name> <param-value>true</param-value>
</init-param> </filter> <filter-mapping> <filter-name>zkFilter</filter-name>
<url-pattern>your URI pattern</url-pattern> </filter-mapping> -->
<!-- //// -->
<!-- ///////////// -->
<!-- DSP (optional) Uncomment it if you want to use DSP However, it is turned
on since zksandbox uses DSP to generate CSS. <servlet> <servlet-name>dspLoader</servlet-name>
<servlet-class>org.zkoss.web.servlet.dsp.InterpreterServlet</servlet-class>
<init-param> <param-name>class-resource</param-name> <param-value>true</param-value>
</init-param> </servlet> <servlet-mapping> <servlet-name>dspLoader</servlet-name>
<url-pattern>*.dsp</url-pattern> </servlet-mapping> -->
<!-- /////////// -->
<!-- [Optional] Session timeout -->
<session-config>
<session-timeout>60</session-timeout>
</session-config>
<!-- [Optional] MIME mapping -->
<mime-mapping>
<extension>doc</extension>
<mime-type>application/vnd.ms-word</mime-type>
</mime-mapping>
<mime-mapping>
<extension>gif</extension>
<mime-type>image/gif</mime-type>
</mime-mapping>
<mime-mapping>
<extension>htm</extension>
<mime-type>text/html</mime-type>
</mime-mapping>
<mime-mapping>
<extension>html</extension>
<mime-type>text/html</mime-type>
</mime-mapping>
<mime-mapping>
<extension>jpeg</extension>
<mime-type>image/jpeg</mime-type>
</mime-mapping>
<mime-mapping>
<extension>jpg</extension>
<mime-type>image/jpeg</mime-type>
</mime-mapping>
<mime-mapping>
<extension>js</extension>
<mime-type>text/javascript</mime-type>
</mime-mapping>
<mime-mapping>
<extension>pdf</extension>
<mime-type>application/pdf</mime-type>
</mime-mapping>
<mime-mapping>
<extension>png</extension>
<mime-type>image/png</mime-type>
</mime-mapping>
<mime-mapping>
<extension>txt</extension>
<mime-type>text/plain</mime-type>
</mime-mapping>
<mime-mapping>
<extension>xls</extension>
<mime-type>application/vnd.ms-excel</mime-type>
</mime-mapping>
<mime-mapping>
<extension>xml</extension>
<mime-type>text/xml</mime-type>
</mime-mapping>
<mime-mapping>
<extension>zhtml</extension>
<mime-type>text/html</mime-type>
</mime-mapping>
<mime-mapping>
<extension>zul</extension>
<mime-type>text/html</mime-type>
</mime-mapping>
<welcome-file-list>
<welcome-file>index.zul</welcome-file>
<welcome-file>index.zhtml</welcome-file>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
</welcome-file-list>
</web-app>
Step 5:
As you can see in the web.xml file, we are loading new xml file configuration named as “spring-security.xml”.
Select the Folder “WEB-INF”, right click, Select New->Other-XML File and give the file name as “spring-security.xml”
<!-- Spring namespace-based configuration -->
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.xsd">
<!-- ====================================================== -->
<!-- For catching the @Secured annotation methods -->
<!-- Tells the Spring Security engine that we will use Spring Security's -->
<!-- pre and post invocation Java annotations (@PreFilter, @PreAuthorize, -->
<!-- @PostFilter, -->
<!-- @PostAuthorize) to secure service layer methods.. -->
<!-- Look in GFCBaseCtrl.java onEvent() method. -->
<!-- ====================================================== -->
<!-- Enable the @Secured annotation to secure service layer methods -->
<global-method-security secured-annotations="enabled" />
<http auto-config="true">
<!-- ====================================================== -->
<!-- If we have our own LoginPage. So we must -->
<!-- tell Spring the name and the place. -->
<!-- In our case we take the same page -->
<!-- for a error message by a failure. -->
<!-- Further the page after a successfully login. -->
<!-- ====================================================== -->
<form-login login-page="/login.zul"
authentication-failure-url="/login.zul?login_error=1"
default-target-url="/index.zul" />
<intercept-url pattern="/login.zul" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<intercept-url pattern="/*.zul" access="IS_AUTHENTICATED_REMEMBERED" />
<intercept-url pattern="/zk/*" access="IS_AUTHENTICATED_REMEMBERED" />
</http>
<authentication-manager>
<authentication-provider user-service-ref="myUserDetailsService">
</authentication-provider>
</authentication-manager>
<beans:bean id="myUserDetailsService"
class="zkloginexample2.MyUserDetailsService"/>
</beans:beans>
login-page : The URL that should be used to render the login page.
Just for this example, we have some user names and their password are encoded.
As you can see that, we have removed all the user name and password in the xml file and used our own custom authentication pointer by referring java bean with id as “myUserDetailsService”. So let us create this java class now
In the zkloginexample2 package(which is created by default), right click-> New->Other->Class and Give the name as MyUserDetailsService.
MyUserDetailsService.java file will be created as shown here.
Here is the content
package zkloginexample3;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.dao.DataAccessException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
public class MyUserDetailsService implements UserDetailsService {
private UserDAO userDAO = new UserDAO();
/*
* You just have to make sure that the user-by-username-query returns three
* fields. 1) the userName 2) the password 3) boolean for is the user
* active. If you don't have an active field, make your query always return
* true for that third field.
*/
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException {
// Declare a null Spring User
UserDetails user = null;
try {
// Search database for a user that matches the specified username
// You can provide a custom DAO to access your persistence layer
// Or use JDBC to access your database
// DbUser is our custom domain user. This is not the same as
// Spring's User
DbUser dbUser = userDAO.searchDatabase(username);
// Populate the Spring User object with details from the dbUser
// Here we just pass the username, password, and access level
// getAuthorities() will translate the access level to the correct
// role type
user = new User(dbUser.getUsername(), dbUser.getPassword()
.toLowerCase(), true, true, true, true,
getAuthorities(dbUser.getAccess()));
} catch (Exception e) {
throw new UsernameNotFoundException("Error in retrieving user");
}
// Return user to Spring for processing.
// Take note we're not the one evaluating whether this user is
// authenticated or valid
// We just merely retrieve a user that matches the specified username
return user;
}
/**
* Retrieves the correct ROLE type depending on the access level, where
* access level is an Integer. Basically, this interprets the access value
* whether it's for a regular user or admin.
*
* @param access
* an integer value representing the access of the user
* @return collection of granted authorities
*/
public Collection<GrantedAuthority> getAuthorities(Integer access) {
// Create a list of grants for this user
List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>(2);
// All users are granted with ROLE_USER access
// Therefore this user gets a ROLE_USER by default
authList.add(new GrantedAuthorityImpl("ROLE_USER"));
// Check if this user has admin access
// We interpret Integer(1) as an admin user
if (access.compareTo(1) == 0) {
// User has admin access
authList.add(new GrantedAuthorityImpl("ROLE_ADMIN"));
}
// Return list of granted authorities
return authList;
}
}
As you can see, we have created additional two java classes such as DbUser.java and UserDAO.java. So let us create this two classes in the package zkloginexample3 as shown.
Here is the code for DBUser.Java
package zkloginexample3;
public class DbUser {
/**
* The username
*/
private String username;
/**
* The password as an MD5 value
*/
private String password;
/**
* Access level of the user. 1 = Admin user 2 = Regular user
*/
private Integer access;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getAccess() {
return access;
}
public void setAccess(Integer access) {
this.access = access;
}
}
Here is the code for UserDAO.java
package zkloginexample3;
import java.util.ArrayList;
import java.util.List;
public class UserDAO {
/**
* Simulates retrieval of data from a database.
* http://krams915.blogspot.in/2010/12/spring-security-mvc-integration_18.html
*/
public DbUser searchDatabase(String username) {
// Retrieve all users from the database
List<DbUser> users = internalDatabase();
// Search user based on the parameters
for (DbUser dbUser : users) {
if (dbUser.getUsername().equals(username) == true) {
// return matching user
return dbUser;
}
}
throw new RuntimeException("User does not exist!");
}
/**
* Our fake database. Here we populate an ArrayList with a dummy list of
* users.
*/
private List<DbUser> internalDatabase() {
// Dummy database
// Create a dummy array list
List<DbUser> users = new ArrayList<DbUser>();
DbUser user = null;
// Create a new dummy user
user = new DbUser();
user.setUsername("rod");
// Actual password: admin
user.setPassword("a564de63c2d0da68cf47586ee05984d7");
// Admin user
user.setAccess(1);
// Add to array list
users.add(user);
// Create a new dummy user
user = new DbUser();
user.setUsername("dianne");
// Actual password: user
user.setPassword("65d15fe9156f9c4bbffd98085992a44e");
// Regular user
user.setAccess(2);
// Add to array list
users.add(user);
// Create a new dummy user
user = new DbUser();
user.setUsername("scott");
// Actual password: user
user.setPassword("2b58af6dddbd072ed27ffc86725d7d3a");
// Regular user
user.setAccess(2);
// Add to array list
users.add(user);
return users;
}
}
Step 6:
Next step is to add the login.zul. Select the folder “webapp”. Right click, Select New->Other->Zul file. Enter the file name as login.zul
Here is the file content.
<?page id="loginPage" title="Digital medical sheet - Login page" onLoad="win.doOverlapped();"?>
<window id="win" title="LOGIN" border="normal" width="300px"
position="center">
<!-- this form-login-page form is also used as the
form-error-page to ask for a login again. -->
<html style="color:red" if="${not empty param.login_error}">
<![CDATA[ Your login attempt was not successful, try
again.<br/><br/> Reason:
${SPRING_SECURITY_LAST_EXCEPTION.message} ]]>
</html>
<groupbox>
<caption>Login</caption>
<h:form id="f" name="f" action="j_spring_security_check"
method="POST" xmlns:h="http://www.w3.org/1999/xhtml">
<grid>
<columns sizable="false">
<column width="50%" />
<column width="50%" />
</columns>
<rows>
<row>
<label value="User:" />
<textbox id="u" name="j_username" />
</row>
<row>
<label value="Password:" />
<textbox id="p" type="password"
name="j_password" />
</row>
<row spans="2">
<div align="center">
<h:input type="submit" value="login" />
<h:input type="reset" value="Reset" />
</div>
</row>
</rows>
</grid>
</h:form>
</groupbox>
</window>
Step 7:
Now the let us change the index.zul file content which is created by default.
<zk>
<window >
Welcome to ZK Secured Page
<button label="Logout" href="/j_spring_security_logout"/>
</window>
</zk>
Step 8:
Now you can select the Project “zkloginexample3”, right click-> Run as –> Run on Server
Select the tomcat server and Click finish button
Now you can enter any valid username/passwords as follows
rod/koala
dianne/emu
scott/wombat
peter/opal
You can download the source here
Reference
1. Introduction to Spring Security Concepts.
2. Getting Started Spring Security
3. Understanding Spring Security
4. Configuring spring security
5. Spring Security 3 - MVC Integration Tutorial (Part 2)
No comments:
Post a Comment