Filter Name | Class |
---|---|
The remainder of this book is a tutorial presented in steps designed to let you easily follow along.
First, install the tutorial tool set so that you can follow the screenshots:
Before starting the project, configure your system. Install the JDK, database, etc., and create and configure the project environment and IDE
Start the tutorial by creating a new Maven project — a web application.
Next, complete the Name and Location pane.
And next, fill in the server and Java EE settings.
After creating the project, go to the pom.xml file and edit it.
<?xml version="1.0" encoding="UTF-8"?>
<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>com.targa.dev</groupId>
<artifactId>ShiroTutorial</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>ShiroTutorial</name>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<failOnMissingWebXml>false</failOnMissingWebXml>
</properties>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>ShiroTutorial</finalName>
</build>
</project>
We will be using the entity-control-boundary (ECB) pattern, a variation of the ubiquitous model-view-controller (MVC) pattern. When identifying the elements for an execution sequence in your system, you can classify each participating element into one of three categories: entity, control, or boundary.
An entity is a long-lived, passive element that is responsible for some meaningful chunk of information. This is not to say that entities are "data" while other design elements are "function", just that entities perform behavior that is organized around some cohesive sets of data.
A control element manages the flow of interaction within a scenario. A control element can manage the end-to-end behavior of a scenario or it can manage the interactions between a subset of the elements. You should classify as entities those behavior and business rules relevant to the scenario that relate to the information; control elements are responsible only for the flow of the scenario.
A boundary element lies just within the periphery of a system or subsystem. For any scenario being considered either across the whole system or within some subsystem, some boundary elements will be front-end elements that accept input from outside the area under design and other elements will be back end, managing communication to supporting elements outside the system or subsystem.
Boundary-control-entity pattern.
Create some empty Java packages just to highlight the BCE-pattern implementation in your application. The packages will take this format:
BCE-pattern package structure.
Let’s call the functional layer for the authentication and authorization needs "security". This is the output:
Now start making your entities.
Use JPA 2.1 for your persistent entities. Create two entities in the com.targa.dev.shirotutorial.security.entity package.
For our Auth² implementation, we will have two entities:
User, which refers to the client of the application, and Role, which refers to the authorization level of the user.
Class diagram of User and Role entities.
You can extend the "roles and rights" strategy (for permission-based authorization) by adding a Permission entity that contains the rights attributed for the associated Role — for example, "Read only" or "Control". The resulting class diagram is:
Class diagram of Role and Permission entities.
@Entity ①
@NamedQueries({ ②
@NamedQuery(name = "User.findAll", query = "select u from User u"),
@NamedQuery(name = "User.findByUsername", query = "SELECT u FROM User u WHERE u.username = :username") ③
})
public class User {
private static final long serialVersionUID = 1L;
@Id ④
@GeneratedValue ⑤
@Column(name = "id", nullable = false, updatable = false) ⑥
private long id;
@NotNull ⑦
@Column(name = "username", length = 50, nullable = false)
private String username;
@NotNull
@Column(name = "password", nullable = false)
private String password;
@OneToOne(cascade = CascadeType.PERSIST) ⑧
private Role role;
@Column(name = "enabled", nullable = false)
private Boolean enabled;
@Version ⑨
@Column(name = "version", nullable = false)
private Timestamp version; ⑩
//Getters & setters
}
@Entity
@NamedQueries({
@NamedQuery(name = "Role.findAll", query = "SELECT r FROM Role r")
})
public class Role {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
@Column(name = "id", nullable = false, updatable = false)
private long id;
@NotNull
@Column(name = "name", length = 50, nullable = false)
private String name;
@NotNull
@Past ①
@Column(name = "creation", nullable = false, updatable = false)
@Temporal(TemporalType.TIMESTAMP) ②
private Date creation;
@Column(name = "enabled", nullable = false)
private Boolean enabled;
//Getters & setters
}
Next, create a service for every entity. Your services will be created in the com.targa.dev.shirotutorial.security.control package. The EntityManager is an object that packages the CRUD operations needed to handle the entity:
@Stateless
public class UserService {
@PersistenceContext
EntityManager em;
public User save(User entity) {
return this.em.merge(entity);
}
public void delete(long id) {
try {
User reference = this.em.getReference(User.class, id);
this.em.remove(reference);
} catch (EntityNotFoundException e) {
//It doesn't exist already
}
}
public User findById(long id) {
return this.em.find(User.class, id);
}
public List<User> findAll() {
return this.em.createNamedQuery("User.findAll", User.class)
.getResultList();
}
}
@Stateless
public class RoleService {
@PersistenceContext
EntityManager em;
public Role save(Role entity) {
return this.em.merge(entity);
}
public void delete(long id) {
try {
Role reference = this.em.getReference(Role.class, id);
this.em.remove(reference);
} catch (EntityNotFoundException e) {
// It doesn't exist already
}
}
public Role findById(long id) {
return this.em.find(Role.class, id);
}
public List<Role> findAll() {
return this.em.createNamedQuery("Role.findAll", Role.class)
.getResultList();
}
}
Your services are now ready to use. For simpler and more flexible context dependency injection (CDI), create a beans.xml file, and change the bean-discovery-mode from annotated to all.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="all">
</beans>
This recommended step lets you inject all the components, not only the annotated object.
First, add the Apache Shiro dependency to your project pom:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.4</version>
</dependency>
Apache Shiro uses an INI configuration file to define parameters. Here is an example of a shiro.ini file:
[main]
authc.loginUrl = /login.jsp ①
authc.successUrl = /home.jsp
passwordMatcher = org.apache.shiro.authc.credential.TempFixPasswordMatcher ②
passwordService = org.apache.shiro.authc.credential.DefaultPasswordService
passwordMatcher.passwordService = $passwordService
ds = com.jolbox.bonecp.BoneCPDataSource ③
ds.driverClass=com.mysql.jdbc.Driver
ds.jdbcUrl=jdbc:mysql://localhost:3306/simple_shiro_web_app
ds.username = root
ds.password = 123qwe
jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealm ④
jdbcRealm.permissionsLookupEnabled = true
jdbcRealm.authenticationQuery = SELECT password FROM USERS WHERE username = ?
jdbcRealm.userRolesQuery = SELECT role_name FROM USERS_ROLES WHERE username = ?
jdbcRealm.permissionsQuery = SELECT permission_name FROM ROLES_PERMISSIONS WHERE role_name = ?
jdbcRealm.credentialsMatcher = $passwordMatcher
jdbcRealm.dataSource=$ds
securityManager.realms = $jdbcRealm ⑤
[urls] ⑥
# The /login.jsp is not restricted to authenticated users (otherwise no one could log in!), but
# the 'authc' filter must still be specified for it so it can process that url's
# login submissions. It is 'smart' enough to allow those requests through as specified by the
# shiro.loginUrl above.
/login.jsp = authc
/home.jsp = anon, authc
/logout = logout
/account/** = authc
This shiro.ini file has to be placed in src/main/webapp/WEB-INF or src/main/resources. To enable the Shiro framework, you have to configure your web.xml file.
<listener>
<listener-class>
org.apache.shiro.web.env.EnvironmentLoaderListener
</listener-class>
</listener>
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
I hear your protests! These configuration files totally conflict with the Java EE "convention over configuration" approach!
In every new Java EE release, you should strive to minimize configuration files. One focus of this tutorial will be to build our Shiro framework application without messy configuration files. You can try to circumvent the problem with some innovative solutions. For example, you can use the Servlet 3.0 features instead of the web.xml file to define the filter in the code:
A filter is an object that can transform the header and content (or both) of a request or response. Filters differ from web components in that filters usually do not themselves create a response. Instead, a filter provides functionality that can be attached to any kind of web resource. Consequently, a filter should not have any dependencies on a web resource for which it is acting as a filter; this way, it can be composed with more than one type of web resource.
The main tasks that a filter can perform are:
The Shiro filter is the primary filter for web applications implementing the Shiro framework.
To do so, introduce the class ShiroFilterActivator that extends org.apache.shiro.web.servlet.ShiroFilter, and apply the filter to all requests ("/*").
@WebFilter("/*")
public class ShiroFilterActivator extends ShiroFilter {
private ShiroFilterActivator() {
}
}
Create a ShiroListener class that extends org.apache.shiro.web.env.EnvironmentLoaderListener. In the contextInitialized() method, initialize ENVIRONMENT_CLASS_PARAM (shiroEnvironmentClass) with the DefaultWebEnvironment class’s name. It is used to define the WebEnvironment implementation class to use in the servlet context.
@WebListener
public class ShiroListener extends EnvironmentLoaderListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
sce.getServletContext()
.setInitParameter(ENVIRONMENT_CLASS_PARAM, DefaultWebEnvironment.class.getName());
super.contextInitialized(sce);
}
}
To eliminate the use of the INI configuration file, use the createEnvironment(ServletContext sc) method to define the WebEnvironment.
@Override
protected WebEnvironment createEnvironment(ServletContext sc)
DefaultWebEnvironment webEnvironment = (DefaultWebEnvironment) super.createEnvironment(sc);
...
return webEnvironment;
}
Within the WebEnvironment component, you must define two parameters: the FilterChainResolver and the SecurityManager. But before defining the FilterChainResolver, you first must understand the concept of a FilterChain.
To paraphrase the Java EE 5 JavaDoc, the servlet container provides the FilterChain object to allow the developer to view the invocation chain of a filtered request for a resource. Filters use the FilterChain to invoke the next filter in the chain or, if the calling filter is the last filter in the chain, to invoke the resource at the end of the chain.
The official Shiro JavaDoc defines some WebEnvironment components.
It states that a FilterChainResolver can resolve an appropriate FilterChain to execute during a ServletRequest. It allows resolution of arbitrary filter chains which can be executed for any given request or URI/URL.
This mechanism allows for more flexible FilterChain resolution than normal web.xml servlet filter definitions. It allows you to define arbitrary filter chains per URL in a more concise and easy-to-read manner, and even allows filter chains to be dynamically resolved or constructed at run time if the underlying implementation supports it.
The SecurityManager executes all security operations for all Subjects (i.e. users) across a single application. The interface itself primarily exists as a convenience —t extends the Authenticator, Authorizer, and SessionManager interfaces, thereby consolidating these behaviors into a single point of reference. In most cases, this simplifies configuration by allowing developers to interact with a single SecurityManager instead of having to reference Authenticator, Authorizer, and SessionManager instances separately.
Obviously, you have to create these components: FilterChainResolver & SecurityManager.
Start by creating a new class to hold the configuration for your ShiroConfiguration class. The class will have two public getters.
public class ShiroConfiguration {
private ShiroConfiguration() {
}
public WebSecurityManager getSecurityManager() {
}
public FilterChainResolver getFilterChainResolver() {
}
}
But this solution is still incomplete; you want to use dependency injection to set the FilterChainResolver and the SecurityManager instances. To enable injection for the two configuration components, bind the instance to your session by annotating the two getter methods with the @Produces annotation.
public class ShiroConfiguration {
private ShiroConfiguration() {
}
@Produces
public WebSecurityManager getSecurityManager() {
}
@Produces
public FilterChainResolver getFilterChainResolver() {
}
}
You can create a more elegant ShiroListener that sports @Inject for both parameters.
@WebListener
public class ShiroListener extends EnvironmentLoaderListener {
@Inject
WebSecurityManager webSecurityManager;
@Inject
FilterChainResolver filterChainResolver;
@Override
protected WebEnvironment createEnvironment(ServletContext sc) {
DefaultWebEnvironment webEnvironment = (DefaultWebEnvironment) super.createEnvironment(sc);
webEnvironment.setSecurityManager(securityManager);
webEnvironment.setFilterChainResolver(filterChainResolver);
return webEnvironment;
}
@Override
public void contextInitialized(ServletContextEvent sce) {
sce.getServletContext()
.setInitParameter(ENVIRONMENT_CLASS_PARAM, DefaultWebEnvironment.class.getName());
super.contextInitialized(sce);
}
}
The ShiroListener class is now ready, and it’s time to move on to defining your ShiroWebEnvironmentConfiguration class.
The Shiro SecurityManager has many features and options for tweaking your security experience. For web-based applications, Shiro provides a default SecurityManager implementation called DefaultWebSecurityManager, which features many preconfigured options. This choice is especially useful for newbies as well as for applications with minor functional requirements. I recommend that you start with the default implementation and modify each option as the need arises.
The following are the most important core elements of the Apache Shiro framework:
Before we get into the code, let’s review the definitions as specificed in the official Shiro Javadoc:
The Shiro JavaDoc leaves something to be desired, but hopefully our explanations will improve that.
For your SecurityManager, you will:
To create the Realm, you must create a class that extends the org.apache.shiro.realm.AuthorizingRealm abstract class and implement the critical methods:
As this project uses Java EE features with the Shiro framework, you will be using EJB to deal with data. So we will use the injection (or try to) to call your EJBs. As the context dependency injection (CDI) is not enabled for the Shiro components, the Shiro Realm is not a CDI-aware component, because of some final methods incoming from the inheritance tree.
Let’s take full advantage of the Java EE and use EJB to deal with data, and try to use dependency injection to call your EJB. Note that context dependency injection (CDI) is not enabled for Shiro components because the Shiro Realm is not a CDI-aware component, because some final methods are inherited.
Stack Overflow has some posts that shed light on the problem:
But not to worry — there are some tricks to make this work! There are at least two options to inject our EJB: UserService and RoleService.
public class ShiroConfiguration {
@Inject
UserService userService;
@Produces
public WebSecurityManager getSecurityManager() {
DefaultWebSecurityManager securityManager = null;
if (securityManager == null) {
AuthorizingRealm realm = new SecurityRealm(userService);
…
securityManager = new DefaultWebSecurityManager(realm);
}
return securityManager;
}
}
Unfortunately, this strategy violates proper separation of concerns, so let’s look at another approach.
Method 2) Use a classic JNDI lookup in the Realm to grab an access to the EJB: The Java EE 6 specification provides for EJB JNDI lookup names of the general form java:global[/appName]/moduleName/beanName.
The appName component of the lookup name is shown as optional because it does not apply to beans deployed in standalone modules. Only beans packaged in .ear files contain the appName component in the java:global lookup name. As your application is packaged as a WAR, you will not have the appName value in the JNDI names of your EJB. Your moduleName is "ShiroTutorial" (see the pom.xml).
So, your EJB’s JNDI names are:
java:global/ShiroTutorial/UserService
java:global/ShiroTutorial/RoleService
Now, you can write your SecurityRealm as:
public class SecurityRealm extends AuthorizingRealm {
private Logger logger;
private UserService userService;
public SecurityRealm() {
super();
this.logger = Logger.getLogger(SecurityRealm.class.getName());
try {
InitialContext ctx = new InitialContext();
this.userService = (UserService) ctx.lookup("java:global/ShiroTutorial/UserService");
} catch (NamingException ex) {
logger
.warning("Cannot do the JNDI Lookup to instantiate the User service : " + ex);
}
}
}
The specification has fixed predefined JNDI names for the appName and the moduleName, so you can grab them dynamically.
java:app/AppName
java:module/ModuleName
Using that, you can write your JNDI lookup code as:
try {
InitialContext ctx = new InitialContext();
String moduleName = (String) ctx.lookup("java:module/ModuleName");
this.userService = (UserService) ctx.lookup(String.format("java:global/%s/UserService", moduleName));
} catch (NamingException ex) {
logger
.warning("Cannot do the JNDI Lookup to instantiate the User service : " + ex);
}
Now, use your EJB in the doGetAuthenticationInfo() and doGetAuthorizationInfo() methods.
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// Identify account to log to
UsernamePasswordToken userPassToken = (UsernamePasswordToken) token;
String username = userPassToken.getUsername();
if (username == null) {
logger.warning("Username is null.");
return null;
}
// Read the user from DB
User user = this.userService.findByUsername(username);
if (user == null) {
logger.warning("No account found for user [" + username + "]");
throw new IncorrectCredentialsException();
}
return new SimpleAuthenticationInfo(username, user.getPassword(), getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//null usernames are invalid
if (principals == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
String username = (String) getAvailablePrincipal(principals);
Set<String> roleNames = new HashSet<>();
roleNames.add(this.userService.findByUsername(username).getRole().getName());
AuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
/**
* If you want to do Permission Based authorization, you can grab the Permissions List associated to your user:
* Set<String> permissions = new HashSet<>();
* permissions.add(this.userService.findByUsername(username).getRole().getPermissions());
* ((SimpleAuthorizationInfo)info).setStringPermissions(permissions);
*/
return info;
}
The code is straightforward and the components names tell the whole story.
CredentialsMatcher is the component that validates credentials and checks if access can be granted. The CredentialsMatcher interface has many implementations:
Each implementation of CredentialsMatcher has specific properties and functionality. For example, SimpleCredentialsMatcher is used for direct, plain comparison. PasswordMatcher uses an internal PasswordService to compare the passwords. The Shiro JavaDoc describes the PasswordMatcher as "a CredentialsMatcher that employs best-practices comparisons for hashed text passwords."
Choose PasswordMatcher as the implementation of CredentialsMatcher so that you can define a customized PasswordService for password verification.
Let’s talk a little bit about about the password itself. We have to choose a hash algorithm for storing and comparing passwords. After some research, I chose bcrypt as a hash algorithm. Based on the Blowfish symmetric-key block-cipher cryptographic algorithm, bcrypt is an adaptive hash function that introduces a work factor (also known as security factor) that allows you to determine the cost of the hash function.
Let’s use the jBCrypt implementation of bcrypt, and you must add that dependency to the pom.xml file.
<dependency>
<groupId>de.svenkubiak</groupId>
<artifactId>jBCrypt</artifactId>
<version>0.4</version>
</dependency>
Next, write your customized PasswordService class. Call the class BCryptPasswordService and implement the PasswordService interface and its two methods, encryptPassword() and passwordsMatch().
public class BCryptPasswordService implements PasswordService {
public static final int DEFAULT_BCRYPT_ROUND = 10;
private int logRounds;
public BCryptPasswordService() {
this.logRounds = DEFAULT_BCRYPT_ROUND;
}
public BCryptPasswordService(int logRounds) {
this.logRounds = logRounds;
}
@Override
public String encryptPassword(Object plaintextPassword) {
if (plaintextPassword instanceof String) {
String password = (String) plaintextPassword;
return BCrypt.hashpw(password, BCrypt.gensalt(logRounds));
}
throw new IllegalArgumentException(
"BCryptPasswordService encryptPassword only support java.lang.String credential.");
}
@Override
public boolean passwordsMatch(Object submittedPlaintext, String encrypted) {
if (submittedPlaintext instanceof char[]) {
String password = String.valueOf((char[]) submittedPlaintext);
return BCrypt.checkpw(password, encrypted);
}
throw new IllegalArgumentException(
"BCryptPasswordService passwordsMatch only support char[] credential.");
}
public void setLogRounds(int logRounds) {
this.logRounds = logRounds;
}
public int getLogRounds() {
return logRounds;
}
}
This class will be used in the CredentialsMatcher by its passwordMatch() method. But it can also be used in the registration process of a new user, before invoking the UserService's save method.
So, update your ShiroConfiguration class.
public class ShiroConfiguration {
@Inject
UserService userService;
@Produces
public WebSecurityManager getSecurityManager() {
DefaultWebSecurityManager securityManager = null;
if (securityManager == null) {
AuthorizingRealm realm = new SecurityRealm(userService);
CredentialsMatcher credentialsMatcher = new PasswordMatcher();
((PasswordMatcher) credentialsMatcher).setPasswordService(new BCryptPasswordService());
realm.setCredentialsMatcher(credentialsMatcher);
…
securityManager = new DefaultWebSecurityManager(realm);
}
return securityManager;
}
}
Next, add to SecurityManager a RememberMeManager that will use cookies to remember security sessions. Use the CookieRememberMeManager implementation for this.
The cookie value is a Base64-encoded representation of an AES-encrypted representation of a serialized representation of a collection of principals. Basically, it contains the necessary user information, which can be decrypted with the right key. Using the default key will never do, since a hacker could easily figure that out by looking at Shiro’s source code, so specify a custom AES cipher key.
Your customized RememberMeManager will look like this:
byte[] cypherKey = String.format("0x%s", Hex.encodeToString(new AesCipherService().generateNewKey().getEncoded())).getBytes();
RememberMeManager rememberMeManager = new CookieRememberMeManager();
((CookieRememberMeManager) rememberMeManager).setCipherKey(cypherKey);
securityManager.setRememberMeManager(rememberMeManager);
Your customized RememberMeManager is ready for use.
Shiro has started supporting caching systems, and although the support currently is somewhat lacking, it should become more robust in upcoming releases. But Shiro supports EhCache, and that works nicely for this project’s needs. Let’s add that dependency to the pom.xml file.
<!-- EhCache Dependencies -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.1</version>
</dependency>
Next, create the EhCache configuration file in the src/main/resources folder. You can call it, for example, ehcache.xml.
<ehcache name="shiro" updateCheck="false">
<diskStore path="/Users/nebrass/shiro-ehcache"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"/>
</ehcache>
I suggest that you avoid the defaults and always specify the diskStore value as the path to the cache storage.
Now, you can define the CacheManager for your SecurityManager in the ShiroConfiguration class.
CacheManager cacheManager = new EhCacheManager();
((EhCacheManager) cacheManager).setCacheManagerConfigFile("classpath:ehcache.xml");
securityManager.setCacheManager(cacheManager);
The classpath:ehcache.xml snippet indicates that the ehcache.xml file is already available in the classpath. You’ve just set the EhCacheManager to be the CacheManager for your SecurityManager. The cache will be handled according the settings defined in the specified configuration file.
Your SecurityManager is just about ready. Your SecurityManager getter method will be:
@Produces
public WebSecurityManager getSecurityManager() {
DefaultWebSecurityManager securityManager = null;
if (securityManager == null) {
AuthorizingRealm realm = new SecurityRealm();
CredentialsMatcher credentialsMatcher = new PasswordMatcher();
((PasswordMatcher) credentialsMatcher).setPasswordService(new BCryptPasswordService());
realm.setCredentialsMatcher(credentialsMatcher);
securityManager = new DefaultWebSecurityManager(realm);
CacheManager cacheManager = new EhCacheManager();
((EhCacheManager) cacheManager).setCacheManagerConfigFile("classpath:ehcache.xml");
securityManager.setCacheManager(cacheManager);
byte[] cypherKey = String.format("0x%s",
Hex.encodeToString(new AesCipherService().generateNewKey().getEncoded()))
.getBytes();
RememberMeManager rememberMeManager = new CookieRememberMeManager();
((CookieRememberMeManager) rememberMeManager).setCipherKey(cypherKey);
securityManager.setRememberMeManager(rememberMeManager);
}
return securityManager;
}
The CDI @Produces annotation identifies a "producer" method or field, which means that it will identify the getSecurityManager() method as a producer of WebSecurityManager instances in the CDI context.
The WebSecurityManager producer is ready for use. The next step is to define your FilterChainResolver.
The FilterChainResolver producer method provides the ability to define ad hoc filter chains for any matching URL path in your application. This approach is far more flexible, powerful, and concise than the familiar strategy for defining filter chains in web.xml; even if you never use any other Shiro feature, this alone would justify its use. The <URL path expression, filterName> maps each filter to its applied URL path expression.
The URL path expressions are evaluated against an incoming request in the order in which they are defined, and the first match wins.
Shiro provides a large selection of predefined filters.
Filter Name | Class |
---|---|
This project mainly uses anon, authc, and user:
After selecting your filters, specify your selections in the FilterChainManager.
FormAuthenticationFilter authc = new FormAuthenticationFilter();
AnonymousFilter anon = new AnonymousFilter();
UserFilter user = new UserFilter();
FilterChainManager fcMan = new DefaultFilterChainManager();
fcMan.addFilter("authc", authc);
fcMan.addFilter("anon", anon);
fcMan.addFilter("user", user);
Next, define the properties needed for the filters. For example, your authc and user filters need to have the loginUrl defined.
authc.setLoginUrl("/login.html");
user.setLoginUrl("/login.html");
Next, map the URL path expressions with the associated filter name.
fcMan.createChain("/index.html", "anon");
fcMan.createChain("/css/**", "anon");
fcMan.createChain("/api/auth", "anon");
fcMan.createChain("/login.html", "authc");
fcMan.createChain("/**", "user");
Set your fcMan (FilterChainManager) to the FilterChainResolver that you are producing.
PathMatchingFilterChainResolver resolver = new PathMatchingFilterChainResolver();
resolver.setFilterChainManager(fcMan);
filterChainResolver = resolver;
return filterChainResolver;
Finally, here’s the FilterChainResolver producer method.
@Produces
public FilterChainResolver getFilterChainResolver() {
FilterChainResolver filterChainResolver = null;
if (filterChainResolver == null) {
FormAuthenticationFilter authc = new FormAuthenticationFilter();
AnonymousFilter anon = new AnonymousFilter();
UserFilter user = new UserFilter();
authc.setLoginUrl(WebPages.LOGIN_URL);
user.setLoginUrl(WebPages.LOGIN_URL);
FilterChainManager fcMan = new DefaultFilterChainManager();
fcMan.addFilter("authc", authc);
fcMan.addFilter("anon", anon);
fcMan.addFilter("user", user);
fcMan.createChain("/index.html", "anon");
fcMan.createChain("/css/**", "anon");
fcMan.createChain("/api/auth", "anon");
fcMan.createChain(WebPages.LOGIN_URL, "authc");
fcMan.createChain("/**", "user");
PathMatchingFilterChainResolver resolver = new PathMatchingFilterChainResolver();
resolver.setFilterChainManager(fcMan);
filterChainResolver = resolver;
}
return filterChainResolver;
}
At this point, your WebSecurityManager and FilterChainResolver CDI producers are ready for use and you can update your ShiroListener class.
@WebListener
public class ShiroListener extends EnvironmentLoaderListener {
@Inject
WebSecurityManager securityManager;
@Inject
FilterChainResolver filterChainResolver;
@Override
protected WebEnvironment createEnvironment(ServletContext sc) {
DefaultWebEnvironment webEnvironment = (DefaultWebEnvironment) super.createEnvironment(sc);
webEnvironment.setSecurityManager(securityManager);
webEnvironment.setFilterChainResolver(filterChainResolver);
return webEnvironment;
}
@Override
public void contextInitialized(ServletContextEvent sce) {
sce.getServletContext().setInitParameter(ENVIRONMENT_CLASS_PARAM, DefaultWebEnvironment.class.getName());
super.contextInitialized(sce);
}
}
As you see, CDI awareness is enabled in your ShiroListener.
Java EE 7 introduced strong support for REST web services, HTML5, WebSockets, and much more. To interact with our application from the client side, let’s use JAX-RX to create some RESTful web services to expose server operations such as:
To enable JAX-RS services, you must define a JAX-RS application class. Create a class called JaxRsConfiguration in the root of the com.targa.dev.shirotutorial.security package. But why in the root?
In proper conformity with the BCE pattern, this class is configuration; it is not boundary, controller, or entity. Since it concerns the configuration of the application, it belongs in the package root.
@ApplicationPath("api")
public class JaxRsConfiguration extends Application {
}
The root URL of the RESTful web services will be http://[ApplicationPath]/api/.
You want to have two services: the Authentication web service for login, logout, listing connected users, and getting connected user information and the User Management web service for CRUD users.
The web services will be created in the com.targa.dev.shirotutorial.security.boundary as they are boundaries of your application and will help in interacting with external clients.
Start by creating the Authentication web service with the following functionalities:
@Path("auth")
public class AuthenticationResource {
public Response login(){}
public Response logout(){}
public Response getSubjectInfo(){}
public Response getConnectedUsers(){}
}
This web service will be accessed at http://[ApplicationPath]/api/auth.
To log in, use this:
try {
SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password, rememberMe));
} catch (AuthenticationException e) {
throw new IncorrectCredentialsException("Unknown user, please try again");
}
In the code above, username and password are strings and rememberMe is a boolean. The user supplies these values through an HTML form, for example. So the login() method is:
@POST
@Path("login")
public Response login(@NotNull @FormParam("username") String username, ①
@NotNull @FormParam("password") String password, ②
@NotNull @FormParam("rememberMe") boolean rememberMe,
@Context HttpServletRequest request) { ③
boolean justLogged = false;
if (!SecurityUtils.getSubject().isAuthenticated() ) { ④
justLogged = true;
}
try {
SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password, rememberMe));
} catch (AuthenticationException e) {
throw new IncorrectCredentialsException("Unknown user, please try again");
}
SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request); ⑤
if (savedRequest != null) {
return this.getRedirectResponse(savedRequest.getRequestUrl(), request);
} else {
if (justLogged) {
return this.getRedirectResponse(WebPages.DASHBOARD_URL, request); ⑦
}
return this.getRedirectResponse(WebPages.HOME_URL, request); ⑥
}
}
private Response getRedirectResponse(String requestedPath, HttpServletRequest request) {
String appName = request.getContextPath();
String baseUrl = request.getRequestURL().toString().split(request.getRequestURI())[0] + appName;
try {
if (requestedPath.split(appName).length > 1) {
baseUrl += requestedPath.split(appName)[1];
} else {
baseUrl += requestedPath;
}
return Response.seeOther(new URI(baseUrl)).build();
} catch (URISyntaxException ex) {
logger.warning("URL redirection failed for request[" + request + "] : " + ex);
return Response.serverError()
.status(404)
.type(MediaType.TEXT_HTML)
.build();
}
}
The logout() method is simpler.
@GET
@Path("logout")
public Response logout(@Context HttpServletRequest request) {
SecurityUtils.getSubject().logout();
return this.getRedirectResponse(WebPages.HOME_URL, request);
}
Here’s the getSubjectInfo().
@GET
@Path("me")
public Response getSubjectInfo() {
Subject subject = SecurityUtils.getSubject();
if (subject != null && subject.isAuthenticated()) {
User connectedUser = this.userService.findByUsername(subject.getPrincipal().toString());
return Response.ok(connectedUser).build();
} else {
return Response.serverError()
.type(MediaType.TEXT_HTML)
.build();
}
}
The idea behind the getConnectedUsers() is to create a collection that holds all connected users. When a user is logged in, the user is added to the collection. When the user logs out, the user is removed from the collection. We will leverage a nice benefit of CDI: the CDI events mechanism.
Start by injecting Event<T> to listen for fired events of type T.
@Inject
private Event<AuthenticationEvent> monitoring;
@Inject
private AuthenticationEventMonitor authenticationEventMonitor;
The AuthenticationEventMonitor class is a holder class for the event producer and monitoring.
Create the AuthenticationEvent event class as follows.
public class AuthenticationEvent {
public enum Type { LOGIN, LOGOUT }
private String username;
private Date creation;
private Type type;
public AuthenticationEvent(String username, Type type) {
this.username = username;
this.type = type;
this.creation = new Date();
}
...
@Override
public String toString() {
return "AuthenticationEvent{" +
"username='" + username + '\'' +
", creation=" + creation +
", type=" + type + '}';
}
}
Next, create a holder for the events producer and manager.
public class AuthenticationEventMonitor {
@Inject
private Logger logger;
private CopyOnWriteArrayList<String> connectedUsers;
@PostConstruct
public void init() {
this.connectedUsers = new CopyOnWriteArrayList<>();
}
public void onAuthenticationEvent(@Observes AuthenticationEvent event) {
if (event != null &&
((event.getType() == AuthenticationEvent.Type.LOGIN) ||
(event.getType() == AuthenticationEvent.Type.LOGOUT))) {
if (event.getType() == AuthenticationEvent.Type.LOGIN) {
if (!connectedUsers.contains(event.getUsername())) {
this.connectedUsers.add(event.getUsername());
}
} else {
if (connectedUsers.contains(event.getUsername())) {
this.connectedUsers.remove(event.getUsername());
}
}
} else {
logger.warning("Unrecognized AuthenticationEvent : [" + event + "]");
}
}
public boolean isUserAlreadyConnected(String username) {
return this.connectedUsers.contains(username);
}
public List<String> getConnectedUsers() {
return connectedUsers;
}
}
The AuthenticationResource class becomes the following.
@Path("auth")
@Produces(MediaType.APPLICATION_JSON)
public class AuthenticationResource {
@Inject
private Logger logger;
@Inject
private UserService userService;
@Inject
private Event<AuthenticationEvent> monitoring;
@Inject
private AuthenticationEventMonitor authenticationEventMonitor;
@POST
@Path("login")
public Response login(@NotNull @FormParam("username") String username,
@NotNull @FormParam("password") String password,
@NotNull @FormParam("rememberMe") boolean rememberMe,
@Context HttpServletRequest request) {
boolean justLogged = false;
if (!SecurityUtils.getSubject().isAuthenticated()) {
justLogged = true;
}
try {
SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password, rememberMe));
} catch (Exception e) {
throw new IncorrectCredentialsException("Unknown user, please try again");
}
SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request);
if (savedRequest != null) {
return this.getRedirectResponse(savedRequest.getRequestUrl(), request);
} else {
if (justLogged) {
return this.getRedirectResponse(WebPages.DASHBOARD_URL, request);
}
return this.getRedirectResponse(WebPages.HOME_URL, request);
}
}
@GET
@Path("logout")
public Response logout(@Context HttpServletRequest request) {
SecurityUtils.getSubject().logout();
return this.getRedirectResponse(WebPages.HOME_URL, request);
}
@GET
@Path("me")
public Response getSubjectInfo() {
Subject subject = SecurityUtils.getSubject();
if (subject != null && subject.isAuthenticated()) {
User connectedUser = this.userService.findByUsername(subject.getPrincipal().toString());
return Response.ok(connectedUser).build();
} else {
return Response.serverError().type(MediaType.TEXT_HTML).build();
}
}
@GET
@Path("users")
public Response getConnectedUsers() {
List<String> connectedUsers = this.authenticationEventMonitor.getConnectedUsers();
return Response.ok(connectedUsers).build();
}
private Response getRedirectResponse(String requestedPath, HttpServletRequest request) {
String appName = request.getContextPath();
String baseUrl = request.getRequestURL().toString().split(request.getRequestURI())[0] + appName;
try {
if (requestedPath.split(appName).length > 1) {
baseUrl += requestedPath.split(appName)[1];
} else {
baseUrl += requestedPath;
}
return Response.seeOther(new URI(baseUrl)).build();
} catch (URISyntaxException ex) {
return Response.serverError().status(404).type(MediaType.TEXT_HTML).build();
}
}
}
Then, create the UserResource class.
@Path("users")
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {
@Inject
private UserService userService;
@POST
public Response addUser(@Valid User user, @Context UriInfo info) {
User saved = this.userService.save(user);
long id = saved.getId();
URI uri = info.getAbsolutePathBuilder().path("/" + id).build();
return Response.created(uri).build();
}
@DELETE
@Path("{id}")
public Response deleteUser(@PathParam("id") long id) {
this.userService.delete(id);
return Response.ok().build();
}
@PUT
public Response editUser(@Valid User user, @Context UriInfo info) {
User searched = this.userService.save(user);
return Response.ok(searched).build();
}
@GET
@Path("{id}")
public Response findUser(@PathParam("id") long id) {
User searched = this.userService.findById(id);
return Response.ok(searched).build();
}
@GET
public Response listUsers() {
List<User> all = this.userService.findAll();
return Response.ok(all).build();
}
}
The last item in the JAX-RS layer is the ExceptionMapper class.
@Provider
public class ShiroExceptionMapper implements ExceptionMapper<Exception> {
private static final String CAUSE = "cause";
@Override
public Response toResponse(Exception ex) {
if (ex instanceof UnknownAccountException) {
return Response.status(Response.Status.FORBIDDEN)
.header(ShiroExceptionMapper.CAUSE, "Your username wrong")
.type(MediaType.TEXT_HTML)
.build();
}
if (ex instanceof IncorrectCredentialsException) {
return Response.status(Response.Status.UNAUTHORIZED)
.header(ShiroExceptionMapper.CAUSE, "Password is incorrect")
.type(MediaType.TEXT_HTML)
.build();
}
if (ex instanceof LockedAccountException) {
return Response.status(Response.Status.CONFLICT)
.header(ShiroExceptionMapper.CAUSE, "This username is locked")
.type(MediaType.TEXT_HTML)
.build();
}
if (ex instanceof AuthenticationException) {
return Response.status(Response.Status.BAD_REQUEST)
.header(ShiroExceptionMapper.CAUSE, ex.getMessage())
.type(MediaType.TEXT_HTML)
.build();
}
return Response.serverError().
header(ShiroExceptionMapper.CAUSE, ex.toString()).build();
}
}
At this point, your JAX-RS services are ready for use, and all of your Shiro services are exposed via RESTful services.
JSESSIONID is a cookie generated by a servlet container, such as Tomcat or Jetty, and is used for session management in Java EE web apps for HTTP. Since HTTP is a stateless protocol, a webserver must work to relate separate requests coming from a single client. Session management is the process of tracking a user session using standard techniques such as cookies and URL rewriting.
Once a session is created, the container sends the JSESSIONID cookie in the client response. In the case of HTML access, no user session is created. If the client has disabled cookies, then the container uses URL rewriting to manage the session, where the JSESSIONID is appended to the URL like this: https://localhost:8080/ShiroTutorial/login.htm;jsessionid=9e934f9330d5081e34ce607b478a8e32dd9b0297dbb8
<session-config>
<session-timeout>
30
</session-timeout>
<tracking-mode>COOKIE</tracking-mode>
</session-config>
Let’s write a small client in the form of a small Java SE app to test the web services. The client app will send requests to our web services and you will use JUnit to check that all goes well.
For testing the web services we are going to write a small client in the form of a small Java SE app. Our client app, will make some requests to our webservices to check if it is ok. This check will be done using JUnit.
Now that everything is explained, assemble the pieces into a working project:
And next…
After creating the project, go to the pom.xml and change it as follows.
<?xml version="1.0" encoding="UTF-8"?>
<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>com.targa.dev</groupId>
<artifactId>ShiroTutorialClient</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<build>
<finalName>ShiroTutorialClient</finalName>
</build>
</project>
Add the JUnit dependency to your pom.xml.
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
</dependencies>
After adding these dependencies with the "Test" scope, NetBeans will add a new section called "Test Packages" to the project.
Create rour first JUnit class. Right-click on Test Packages and choose New → JUnit Test.
Since you’ll test the login feature in the authentication web service, you must place your LoginTest class in the same package as the Authentication class, the com.targa.dev.shirotutorial.security.boundary package.
To make a RESTful client, you have to add the dependencies. The following assumes you are using Payara Server and uses its bundled Jersey module as the JAX-RS implementation:
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>2.22.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-processing</artifactId>
<version>2.22.1</version>
</dependency>
Define the first version of your LoginTest class.
public class LoginTest {
private Client client;
private WebTarget target;
@Before
public void setUp() {
client = ClientBuilder.newClient();
target = client.target("http://localhost:8080/ShiroTutorial/")
.path("api/auth/login"); ①
}
...
}
Use this class to test the login operation. Verify that a successful login and a failed login work as expected.
For a successful login attempt:
For a failing login attempt:
Before testing the login operations, we create a new User record. There are many ways to do that but let’s build an initializer EJB that inserts a new user when the application starts.
@Singleton
@Startup
public class InsertUser {
@Inject
UserService us; ①
@Inject
BCryptPasswordService passwordService; ②
@PostConstruct ③
public void insert() {
User craftsman = new User("shiro", passwordService.encryptPassword("netbeans"));
Role role = new Role();
role.setName("ADMIN");
craftsman.setRole(role);
this.us.save(craftsman);
}
}
The EJB is a @Singleton marked with the @Startup annotation, which means it starts when the application starts — every time our application starts. To avoid duplicating records on every restart, we can ask the ORM to drop-and-create tables each time the application starts. To do so, just add the following line to the properties section of your persistence.xml file.
<property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
The test methods will be :
@Test
public void testSuccessfulLogin() {
Form form = new Form();
form.param("username", "shiro");
form.param("password", "netbeans");
form.param("rememberMe", "false");
Response response = target.request(MediaType.APPLICATION_JSON_TYPE)
.post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED));
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
}
@Test
public void testFailingLogin() {
Form form = new Form();
form.param("username", "shiro");
form.param("password", "netbeans2");
form.param("rememberMe", "false");
Response response = target.request(MediaType.APPLICATION_JSON_TYPE)
.post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED));
assertEquals(response.getStatus(), Response.Status.UNAUTHORIZED.getStatusCode());
}
Execute the test.
You will get the following result.
You can keep making JUnit tests to check your RESTful services, which I recommend because you can easily debug them. In contrast, browser plugins such as Postman can make debugging more difficult.