An Introduction to Shiro (formerly JSecurity) – A Beginner’s Tutorial Part 5

Introduction

NOTE: Updated in December 2013.

In part 4 of this series, I explained how you can use Shiro's tag library to control what is rendered in the JSPs. In part 3 of this series, I explained how you can use a users role to control access to specific parts of a web application. In this part of the series, I demonstrate how you can add another level of security to your web application by using permissions. Each role (e.g. admin, user) can have one or more permissions associated with it. Using Shiro you can restrict what someone can access based upon permission.

Using Permission to Control Access

In this example, our web application has three areas needing to be secured:

/users
/staff
/admin

Our business rules for securing each area are:

/users – user is authenticated and has role of user
/staff – user is authenticated and has role of staff or role of admin
/admin – user is authenticated and has role of admin

Since roles staff and admin both need to access the staff area, we can use permissions to control access to that area. We can assign the same permission to both the staff and admin role. We'll call that permission "secure." Any user with permission of "secure" can access the pages in the staff area. If in the future we add additional roles that need to access the staff area, we can give those new roles the "secure" permission also.

So our revised security business rules for each area are:

/users – user is authenticated and has role of user
/staff – user is authenticated and has permission of secure
/admin – user is authenticated and has role of admin

Here are our three users, their roles, and their permissions:

username

password

role

permission

bruce@hotmail.com

bruce

admin

secure

jack@hotmail.com

jack

staff

secure

sue@hotmail.com

sue

user

none

Applying our security business rules to these users we can identify who will be able to access which areas:

/user – only sue
/staff – both bruce and jack but not sue
/admin - only bruce

Example Application

You can download an example application (an archived Eclipse dynamic web project using Maven). You'll also need to download a new version of the securityDB Derby database that includes the roles_permissions table.

After downloading the securityDB Derby database, unzip it to the folder c:/derby. It's OK to overwrite the database that was there as this new version of the securityDB database should work for the previous example applications.

Be sure to update the path to the securityDB in context.xml before building the .war file or running the Maven goal described below.

You can use the Maven Tomcat plugin (see reference below for how to install Maven if you've don't already have Maven) to run the web application if you're not using Eclipse and Tomcat. Just open a command window and navigate to where you unzipped the permissionsecuritywithtags.zip download. Make sure you're in the permissionsecuritywithtags directory. Then do the following:

mvn -e clean tomcat7:run-war

Once you see [INFO] Started Servlet Engine in the command window, open your web browser and go to this URL: http://localhost:8080/ permissionsecuritywithtags/ . You should see the contents of the index.jsp. To stop the Tomcat server type control-c in the command window.

Login as each user (bruce@hotmail.com, jack@hotmail.com, and sue@hotmail.com). When you're logged in as sue@hotmail.com try to go to either http://localhost:8080/permissionsecuritywithtags/staff/index.jsp or http://localhost:8080/permissionsecuritywithtags/admin/index.jsp. You should be redirected to the unauthorized web page. When you're logged in as jack@hotmail.com try to go to the http://localhost:8080/permissionsecuritywithtags/admin/index.jsp page.

Implementing Permission Security in Shiro

So how do we implement security that uses permissions in Shiro? When you attempt to authenticate a user, Shiro will look for any permissions associated with the role that is associated with that user. To use Shiro's default configuration you just need a table named roles_permissions with a column named role_name and a column named permission. The default permissions query is specified in class JdbcRealm, which my class RoleSecurityJdbcRealm extended. In web.xml I specified this class as the value for the IniShiroFilter's realm (a realm is a resource that Shiro will use to authenticate users).

If your project cannot follow Shiro's defaults, you can configure Shiro to use your projects conventions (see the Shiro references below).

In Servlet class LoginUser I created a Subject object and call the Subject class's login method passing it the UsernamePasswordToken object that has the user's username and password. If Shiro can successfully authenticate this user by querying the users table, Shiro will then query the user_roles table for roles associated with this username, and then query the roles_permissions table for permissions associated with each role_name associated with this username. All of this information will be stored in the Subject object and placed into session scope. For more information on interfaces Subject and its associated interfaces AuthenticationInfo and and AuthorizationInfo see the Shiro API.

If you examine the web.xml you'll see the following statements in the configuration for the IniShiroFilter:


[main]
realmA = name.brucephillips.somesecurity.dao.RoleSecurityJdbcRealm
realmA.permissionsLookupEnabled=true

[filters]
roles.unauthorizedUrl = /unauthorized.jsp
perms.unauthorizedUrl = /unauthorized.jsp

#only let authenticated users
#with the appropriate role or permission
#view the web pages in the staff, user,
#and admin areas
[urls]
/staff/** = authc, perms[secure]
/admin/** = authc, roles[admin]
/user/** = authc, roles[user]

Under the [main] section the realmA.permissionsLookupEnabled=true configures Shiro to also lookup the permissions associated with a role. By default permissions lookup is false.

The statement perms.unauthorizedUrl = /unauthorized.jsp tells the filter to redirect people who attempt to view a web page in an area of the site for which they don't have the correct permission to the unauthorized.jsp web page.

The statement /staff/** = authc, perms[secure] means only allow people who have logged in successfully and have a permission of secure to view pages in the /staff folder.

In the Servlet LoginUser.java you'll find this statement around line number 134:

if (subject.isPermitted("secure") )

This statement uses the isPermitted method of the Subject class. This method returns true if the Subject object has the permission sent as the argument, otherwise the isPermitted method returns false.

Shiro also has a tag you can use to check a user's permission. In the JSP /index.jsp I use the hasPermission tag (around line 21) to render content for only those users with the secure permission.

Summary

Shiro is a comprehensive security library that gives you control over all aspects of your web application. By using permissions you can enable more specific access rules.

References

  1. An Introduction to Shiro (formerly JSecurity) – A Beginner's Tutorial Part 4, http://www.brucephillips.name/blog/index.cfm/2009/4/5/An-Introduction-to-Ki-formerly-JSecurity--A-Beginners--Tutorial-Part-4
  2. An Introduction to Shiro (formerly JSecurity) – A Beginner's Tutorial Part 3, http://www.brucephillips.name/blog/index.cfm/2009/4/5/An-Introduction-to-Ki-formerly-JSecurity--A-Beginners--Tutorial-Part-3
  3. Permission Security With Tags Example Application, http://www.brucephillips.name/jsecurity_examples/permissionsecuritywithtags_mvn.zip
  4. Apache Shiro http://shiro.apache.org/
  5. Apache Shiro API, http://shiro.apache.org/static/current/apidocs/
  6. Apache Shiro Tags API, http://shiro.apache.org/static/current/apidocs/org/apache/shiro/web/tags/package-summary.html
  7. Apache Shiro Mailing Lists, http://shiro.apache.org/mailing-lists.html
  8. Shiro Custom Tags TLD, http://www.brucephillips.name/jsecurity_examples/ki%20(jsecurity)%20tld.pdf
  9. Using Custom Tags, J2EE Tutorial, http://java.sun.com/javaee/5/docs/tutorial/doc/bnaiy.html
  10. Apache Derby, http://db.apache.org/derby/
  11. Apache Tomcat, http://tomcat.apache.org/
  12. Jetty, http://jetty.mortbay.org/jetty5/index.html
  13. tp://incubator.apache.org/projects/ki.html
  14. Maven: The Definitive Guide, http://www.sonatype.com/books/maven-book/reference/public-book.html
  15. Developing with Eclipse and Maven, http://www.sonatype.com/books/m2eclipse-book/reference/index.html

Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
I downloaded the sample project and found it misspelled as "persmissionsecuritywithtags". To get this to run you will need to use http://localhost:8080/persmissionsecuritywithtags/...
# Posted By Gordon Dickens | 5/5/09 2:12 PM
Gordon - thank you for letting me know about the problem.

I uploaded a new archived project with the correct artifactID in the pom.xml. You should be able to download that project, unzip, and run the mvn commands as specified in the article.

Then load the application using this URL:

http://localhost:8080/permissionsecuritywithtags/
# Posted By Bruce | 5/5/09 7:25 PM
I wanted to convert the demo over to use the Apache Ki classes (replacing the former JSecurity) and had to make the following changes:
1. changed the pom.xml to import the following dependencies:
org.apachi.key/ki-all/1.0-incubating-SNAPSHOT
org.slf4j/slf4j-simple/1.5.6
commons-beanutils/commons-beanutils/1.7.0
commons-logging/commons-logging/1.1.1

2. Changed the tag URL in the jsp pages to:
<%@ taglib prefix="jsec" uri="http://ki.apache.org/tags"; %>

3. Changed the JSecurityFilter in web.xml
<filter-name>KiFilter</filter-name>
<filter-class>org.apache.ki.web.servlet.KiFilter</filter-class>
...
<filter-mapping>
<filter-name>KiFilter</filter-name>
...

4. Modified all the Java classes to use the classes from package: org.apache.ki.*


Hope this helps.
# Posted By Gordon Dickens | 5/8/09 10:22 AM
Gordon - thank you for posting how to convert an application to use the Apache Ki classes. I need to do this for a project at my job so what you've done is very helpful.
# Posted By Bruce | 5/8/09 11:33 AM
can you please provide DDL to define shiro database tables?
# Posted By Larry | 6/19/12 8:54 AM
Gordon - try generating the DDL from the securityDB database, which is a Derby database. Most IDE's have a database plugin that will work with Derby databases.

I don't have the DDL handy.

Bruce
# Posted By Bruce | 6/19/12 10:23 AM
Hi. Thanks to the tutorials again.
***************************************
When I run the project with jetty-run, I am able to view the home page then following login page. But I can't pass the the login page, caused "LOGIN NOT SUCCESSFUL".
Here are the exceptions I get:
---------------------------------------
javax.naming.NoInitialContextException: Need to specify class name in environment or system property, or as an applet parameter, or in an application resource file: java.naming.factory.initial
   at javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:662)
   at javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:307)
   at javax.naming.InitialContext.getURLOrDefaultInitCtx(InitialContext.java:344)
   at javax.naming.InitialContext.lookup(InitialContext.java:411)
   at name.brucephillips.somesecurity.dao.RoleSecurityJdbcRealm.<init>(RoleSecurityJdbcRealm.java:36)
   at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
   at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
   at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
   at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
   at java.lang.Class.newInstance(Class.java:374)
   at org.apache.shiro.util.ClassUtils.newInstance(ClassUtils.java:177)
   at org.apache.shiro.util.ClassUtils.newInstance(ClassUtils.java:164)
   at org.apache.shiro.config.ReflectionBuilder.createNewInstance(ReflectionBuilder.java:136)
   at org.apache.shiro.config.ReflectionBuilder.buildObjects(ReflectionBuilder.java:114)
   at org.apache.shiro.config.IniSecurityManagerFactory.buildInstances(IniSecurityManagerFactory.java:170)
   at org.apache.shiro.config.IniSecurityManagerFactory.createSecurityManager(IniSecurityManagerFactory.java:119)
   at org.apache.shiro.config.IniSecurityManagerFactory.createSecurityManager(IniSecurityManagerFactory.java:97)
   at org.apache.shiro.config.IniSecurityManagerFactory.createInstance(IniSecurityManagerFactory.java:83)
   at org.apache.shiro.config.IniSecurityManagerFactory.createInstance(IniSecurityManagerFactory.java:41)
   at org.apache.shiro.config.IniFactorySupport.createInstance(IniFactorySupport.java:123)
   at org.apache.shiro.util.AbstractFactory.getInstance(AbstractFactory.java:47)
   at org.apache.shiro.web.servlet.IniShiroFilter.applySecurityManager(IniShiroFilter.java:353)
   at org.apache.shiro.web.servlet.IniShiroFilter.configure(IniShiroFilter.java:321)
   at org.apache.shiro.web.servlet.IniShiroFilter.init(IniShiroFilter.java:292)
   at org.apache.shiro.web.servlet.AbstractShiroFilter.onFilterConfigSet(AbstractShiroFilter.java:83)
   at org.apache.shiro.web.servlet.AbstractFilter.init(AbstractFilter.java:94)
   at org.mortbay.jetty.servlet.FilterHolder.doStart(FilterHolder.java:97)
   at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:50)
   at org.mortbay.jetty.servlet.ServletHandler.initialize(ServletHandler.java:713)
   at org.mortbay.jetty.servlet.Context.startContext(Context.java:140)
   at org.mortbay.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1282)
   at org.mortbay.jetty.handler.ContextHandler.doStart(ContextHandler.java:518)
   at org.mortbay.jetty.webapp.WebAppContext.doStart(WebAppContext.java:499)
   at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:50)
   at org.mortbay.jetty.handler.HandlerWrapper.doStart(HandlerWrapper.java:130)
   at org.mortbay.jetty.Server.doStart(Server.java:224)
   at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:50)
   at runjettyrun.Bootstrap.main(Bootstrap.java:97)
-------------------------------------------------
2013-12-10 19:42:46.499:INFO::Started SelectChannelConnector@0.0.0.0:8080
# Posted By Ça?r? | 12/10/13 8:48 AM
I updated tutorial 5 to now use the Tomcat 7 Maven plugin. Please visit tutorial 5 again and review the changes. There is a new download.

Bruce
# Posted By Bruce | 12/10/13 9:29 AM
Thanks for update Bruce. But although I added dependencies and configured all the changes needed, still I am getting error with additional TLD exception a little different than the old one. I think it is because of the Shiro filter. Here is the output:
-----------------------------------------------------------------
INFO: Server startup in 6840 ms
Ara 13, 2013 7:50:33 AM org.apache.jasper.compiler.TldLocationsCache tldScanJar
INFO: At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
org.apache.shiro.authc.AuthenticationException: Authentication failed for token submission [org.apache.shiro.authc.UsernamePasswordToken - bruce@hotmail.com, rememberMe=false]. Possible unexpected error? (Typical or expected login exceptions should extend from AuthenticationException).
   at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:214)
   at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106)
   at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:270)
   at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:256)
   at name.brucephillips.somesecurity.servlet.LoginUser.doPost(LoginUser.java:99)
   at javax.servlet.http.HttpServlet.service(HttpServlet.java:647)
   at javax.servlet.http.HttpServlet.service(HttpServlet.java:728)
   at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305)
   at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
   at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
   at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
   at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
   at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449)
   at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365)
   at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
   at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
   at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:383)
   at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362)
   at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
   at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
   at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
   at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222)
   at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123)
   at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
   at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
   at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:100)
   at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:953)
   at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
   at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
   at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1041)
   at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:603)
   at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:312)
   at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
   at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
   at java.lang.Thread.run(Thread.java:724)
Caused by: java.lang.IllegalStateException: Connection factory returned null from createConnection
   at org.apache.tomcat.dbcp.dbcp.PoolableConnectionFactory.makeObject(PoolableConnectionFactory.java:584)
   at org.apache.tomcat.dbcp.dbcp.BasicDataSource.validateConnectionFactory(BasicDataSource.java:1556)
   at org.apache.tomcat.dbcp.dbcp.BasicDataSource.createPoolableConnectionFactory(BasicDataSource.java:1545)
   at org.apache.tomcat.dbcp.dbcp.BasicDataSource.createDataSource(BasicDataSource.java:1388)
   at org.apache.tomcat.dbcp.dbcp.BasicDataSource.getConnection(BasicDataSource.java:1044)
   at org.apache.shiro.realm.jdbc.JdbcRealm.doGetAuthenticationInfo(JdbcRealm.java:215)
   at org.apache.shiro.realm.AuthenticatingRealm.getAuthenticationInfo(AuthenticatingRealm.java:568)
   at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:180)
   at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:267)
   at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)
   ... 34 more
-----------------------------------------------------------------
Thanks again.
# Posted By Ça?r? | 12/12/13 9:01 PM
double check the path to the securityDB file that is in context.xml to ensure it matches where you put the securityDB folder.
# Posted By Bruce | 12/13/13 7:13 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.9.1.002. Contact Blog Owner