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 |
admin |
secure |
|
jack |
staff |
secure |
|
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 ([email protected], [email protected], and [email protected]). When you're logged in as [email protected] 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 [email protected] 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
- An Introduction to Shiro (formerly JSecurity) – A Beginner's Tutorial Part 4, /blog/index.cfm/2009/4/5/An-Introduction-to-Ki-formerly-JSecurity--A-Beginners--Tutorial-Part-4
- An Introduction to Shiro (formerly JSecurity) – A Beginner's Tutorial Part 3, /blog/index.cfm/2009/4/5/An-Introduction-to-Ki-formerly-JSecurity--A-Beginners--Tutorial-Part-3
- Permission Security With Tags Example Application, /jsecurity_examples/permissionsecuritywithtags_mvn.zip
- Apache Shiro http://shiro.apache.org/
- Apache Shiro API, http://shiro.apache.org/static/current/apidocs/
- Apache Shiro Tags API, http://shiro.apache.org/static/current/apidocs/org/apache/shiro/web/tags/package-summary.html
- Apache Shiro Mailing Lists, http://shiro.apache.org/mailing-lists.html
- Shiro Custom Tags TLD, /jsecurity_examples/ki%20(jsecurity)%20tld.pdf
- Using Custom Tags, J2EE Tutorial, http://java.sun.com/javaee/5/docs/tutorial/doc/bnaiy.html
- Apache Derby, http://db.apache.org/derby/
- Apache Tomcat, http://tomcat.apache.org/
- Jetty, http://jetty.mortbay.org/jetty5/index.html tp://incubator.apache.org/projects/ki.html
- Maven: The Definitive Guide, http://www.sonatype.com/books/maven-book/reference/public-book.html
- Developing with Eclipse and Maven, http://www.sonatype.com/books/m2eclipse-book/reference/index.html
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/
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.
I don't have the DDL handy.
Bruce
***************************************
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 [email protected]:8080
Bruce
-----------------------------------------------------------------
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 - [email protected], 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.