注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

无线时代辐射无穷

抓紧生宝宝,小心辐射

 
 
 

日志

 
 

一个超级好的spring security范例  

2009-12-29 16:22:37|  分类: springsecurity |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
A Spring Security ACL tutorial for PostgreSQL
http://server.denksoft.com/wordpress/web-development/spring-security/

By Andrei Tudose and Ovidiu Gheorghies

In this article we show how to implement a simple yet meaningful Spring Security based web application using PostgreSQL as a database backend. The learning curve for effective Spring Security usage was pretty steep for us, and we now write the article we wish we would have had in front of our eyes from the very beginning. As die-hard fans of PostgreSQL for its ease of use, scalability and extensibility, we faced a few perks now and then, but having learned how to deal with them, we are now happy to share.

Most web applications use security to separate those who have to right to perform an action from those who don’t. In its simplest form, this may mean that registered users may post messages, unregistered users may only read them, while administrators can exercise their free will into editing, deleting, banning or generally annoying people. For more complex and realistic applications, one cannot simply use the user role to determine which areas within a web application are available to a certain class of users. For example, it is true that both registered users John and Jane may access the “Edit article” area of the application, yet it would be bad karma to allow John to post messages impersonating Jane, or to allow him to edit the messages of other users by entering in the URL bar a guessed or glimpsed message id. When the business logic of the application requires Jane being able to allow Sam, but not John, to edit her articles, things suddenly become a bit complicated for the application developer.

Luckily, Spring Security (formerly Acegi) comes to the rescue. With it, things do not become simpler than they actually are, yet developers are saved from reinventing and reimplementing the wheel, with all the bugs that this endeavor may bring.

Before proceeding, we remind that ACL stands for “access control list” [1], a list of (subject, action) pairs that can be attached to an object to specify that a given subject or user can perform the corresponding action.

To better explain things, we decided to create a BSD-licensed starter application (called SpringStarter) that exemplifies the concepts we explore, so that we have a source for shameless copy-pasting into our own real projects. The SpringStarter application is attached to this article ([5]), so you can download it and make your own experiments. But first, let us present its business logic.

Imagine an Internet banking application, in which we have two main types of users: customers and clerks. A customer has access to his bank accounts and each bank account contains a list of operations (such as deposit and withdrawal). A clerk can fully administer customer bank accounts (create, read, update, delete), yet he is limited to read-only access to bank account operations. To make things more interesting we decided to attach a different “personal information” type to a customer and a clerk.

The following UML diagram should be worth a thousand words ([6],[7]):

SpringStarter business logic: UML diagram

Here, DE<ClerkDE<, DE<CustomerDE<, DE<BankAccountDE< and DE<BankAccountOpperationDE< have their respective meaning. Each DE<ClerkDE< and DE<CustomerDE< correspond to precisely one DE<UserDE<, which has a list of DE<AuthorityDE<-s. Any DE<CustomerDE< has associated to it a number of DE<BankAccountDE<-s, each of which aggregates DE<BankAccountOperationDE<-s. To make the handling of security-related information uniform throughout the application, we derived DE<ClerkDE<, DE<CustomerDE<, DE<BankAccountDE< and DE<BankAccountOperationDE< from DE<AbstractSecureObjectDE< - an abstraction to which security information can be specified upon.

Let us dwell now on what needs to be done to make this business logic work with Spring Security. Spring Security uses a series of database tables to store security-related information, and we need to create them.

To spare you the gory theory, we take a hands-on approach: a working application comes with this article. In the archive, you will find the SQL code needed to create the security-related tables in the acl.sql file.

For the lazy, impatient, Linux-running programmers (our favorite kind), executing DE<`cat acl.sql | psql -U user_spring springstarter -h 127.0.0.1`DE< would do it, provided the database is properly configured and that the same password as the one in DE<hibernate.propertiesDE< is used. Then, it suffices to open and run the Idea project included in the archive (don’t forget to access the “Populate database” link in the web application though, so that you see the credentials to log in with).

Nevertheless, for the sake of respectability, in this article we’ll take it slowly, step by step. So, to make sure that we start off with a clean slate, we’ll drop (then create) these four tables:

    DROP TABLE ACL_ENTRY;      DROP TABLE ACL_OBJECT_IDENTITY;      DROP TABLE ACL_CLASS;      DROP TABLE ACL_SID;

The table DE<ACL_SIDDE< essentially lists all the users in our systems. In Spring Security, a “security id” (SID) is assigned to each user or role. This SID can be then used in an access control list (ACL) to specify which actions can the user with that SID perform on the desired objects. In fact, the SID may correspond to an user, a device or a system which can perform an action in the application, or it may correspond to a granted authority such as a role. The distinction between these two possibilities is made by the value store in the DE<principalDE< column of the DE<ACL_SIDDE< table: DE<trueDE< indicates that the DE<sidDE< is a user, DE<falseDE< means that the DE<sidDE< is a granted authority. Note that this table is not concerned with user names, passwords, or other user-related information, as it uses merely the id-s of the users.

    CREATE TABLE ACL_SID (          id BIGSERIAL NOT NULL PRIMARY KEY,          principal BOOLEAN NOT NULL,          sid VARCHAR(100) NOT NULL,          CONSTRAINT UNIQUE_UK_1 UNIQUE(sid,principal)      );

Having listed the users, we must also list the objects on which these users can operate. For this, we must inform Spring Security of the Java classes (table DE<ACL_CLASSDE<) and their instances (table DE<ACL_OBJECT_IDENTITYDE<) that are being secured. Let’s now define each of these needed tables.

Each class from the system whose objects we wish to secure must be registered in the DE<ACL_CLASSDE< table and uniquely identified by Spring Security by an id. The DE<classDE< field is the fully qualified Java name of the class, such as DE<com.denksoft.springstarter.BankAccountDE<.

    CREATE TABLE ACL_CLASS (          id BIGSERIAL NOT NULL PRIMARY KEY,          class VARCHAR NOT NULL,          CONSTRAINT UNIQUE_UK_2 UNIQUE(class)      );

Every secured object in the system is uniquely identified and registered in a single row of the table DE<ACL_OBJECT_IDENTITYDE<. Such row specifies for each object its class id (DE<object_id_classDE<), and its id in the table where objects of this type are stored (DE<object_id_identityDE<). Each object must have an owner, and the owner’s SID is stored in the DE<owner_sidDE< column. If the object was inherited, the fields DE<parent_objectDE< and DE<entries_inheritingDE< are used to give the due details.

    CREATE TABLE ACL_OBJECT_IDENTITY (          id BIGSERIAL NOT NULL PRIMARY KEY,          object_id_class BIGINT NOT NULL,          object_id_identity BIGINT NOT NULL,          owner_sid BIGINT,          parent_object BIGINT,          entries_inheriting BOOLEAN NOT NULL,          CONSTRAINT UNIQUE_UK_3 UNIQUE(object_id_class,object_id_identity),          CONSTRAINT FOREIGN_FK_1 FOREIGN KEY(parent_object) REFERENCES ACL_OBJECT_IDENTITY(id),          CONSTRAINT FOREIGN_FK_2 FOREIGN KEY(object_id_class) REFERENCES ACL_CLASS(id),          CONSTRAINT FOREIGN_FK_3 FOREIGN KEY(owner_sid) REFERENCES ACL_SID(id)      );

Finally, having listed all the users and all the secured objects (along with their corresponding class), we can now specify what actions can be performed on each of these objects by the desired users (table DE<ACL_ENTRYDE<). Obviously, each row in this table has to contain the reference to the object on which the rights apply (field DE<acl_object_identityDE<), and the user or role id which may perform the action (field DE<sidDE<). The permissions to be granted or denied are specified by using the field DE<maskDE< as a bitfield. For example, if DE<maskDE< is set to the 8-bit value DE<00000101DE<, then actions DE<0DE< and DE<2DE< are allowed, while action DE<1DE< is disallowed. Note that the meaning of these actions (such as read, write, delete) is application-dependent. The DE<grantingDE< field, if set to DE<trueDE<, indicates that the permissions indicated by DE<maskDE< are granted to the corresponding DE<sidDE<, otherwise they are revoked or blocked. Luckily, Spring Security takes care of setting the DE<ace_orderDE< field for us, so we have one less thing to comment upon.

    CREATE TABLE ACL_ENTRY(          id BIGSERIAL NOT NULL PRIMARY KEY,          acl_object_identity BIGINT NOT NULL,          sid BIGINT NOT NULL,          mask INTEGER NOT NULL,          ace_order INT NOT NULL,          granting BOOLEAN NOT NULL,          audit_success BOOLEAN NOT NULL,          audit_failure BOOLEAN NOT NULL,          CONSTRAINT UNIQUE_UK_4 UNIQUE(acl_object_identity,ace_order),          CONSTRAINT FOREIGN_FK_4 FOREIGN KEY(acl_object_identity) REFERENCES ACL_OBJECT_IDENTITY(id),          CONSTRAINT FOREIGN_FK_5 FOREIGN KEY(sid) REFERENCES ACL_SID(id)      );

You can simply copy-paste the SQL commands above into a psql console connected to your desired database, or you can import the DE<acl.sqlDE< from the application archive attached to this article into it.

Below we give an example of how to set up the most important fields of these security-related tables, and how they are connected to actual application business logic objects. In this example, customer John Doe can log in the application with the user name DE<John.DoeDE<. What we want to specify in Spring Security is that this customer has the right to read and write to his bank account, considering that bank accounts are always created by clerks. For this, we need to add a row to the DE<ACL_ENTRYDE< table, by referencing the desired secured object (via DE<acl_object_identityDE< field), the user (via the DE<sidDE< field) and giving the mask of the actions which are allowed (in our case 3, or binary 0011, means read and write access, but no delete or admin). The row in DE<ACL_OBJECT_IDENTITYDE< table states that the DE<BankAccountDE< with DE<object_id_identityDE< set to DE<10DE< (”DE<ROXX1212DE<“), whose type is DE<com.denksoft.springstarter.BankAccountDE< (field DE<object_id_classDE<), and which is thus stored in the DE<BankAccountDE< table, was created by the user DE<Clark.KentDE< (according to field DE<owner_sidDE<).

Spring Security configuration

Let us start the security configuration of the web application by writing the DE<security-config.xmlDE< file.

    <?xml version="1.0" encoding="UTF-8"?>        <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:p="http://www.springframework.org/schema/p"                   xsi:schemaLocation="http://www.springframework.org/schema/beans                   http://www.springframework.org/schema/beans/spring-beans-2.5.xsd                   http://www.springframework.org/schema/security                   http://www.springframework.org/schema/security/spring-security-2.0.1.xsd">             <global-method-security secured-annotations="enabled"/>             <authentication-provider user-service-ref="userDetailsServiceWrapper" />             <http>               <intercept-url pattern="/app/clerk/**" access="ROLE_CLERK"/>               <intercept-url pattern="/app/customer/**" access="ROLE_CUSTOMER"/>               <intercept-url pattern="/app/public/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />                 <anonymous/>               <form-login login-processing-url="/login" login-page="/login.jsp" default-target-url="/app/index.task" />               <logout logout-url="/logout" logout-success-url="/app/index.task" />           </http>      </beans:beans>

This definition should be self-explanatory: we specify that actions in the web directory DE</app/clerk/**DE< can only be accessed by those who have the role DE<ROLE_CLERKDE<, actions in DE</app/customer/**DE< can only be accessed by those who have the role DE<ROLE_CUSTOMERDE<, while actions in DE</app/public/**DE< can be accessed by everybody, including non-authenticated users. Here, we also defined the actions that perform the login and logout operations, the login page, and the default targets the browser is sent to after a successful login and logout.

This should provide us with a basic security mechanism, in which a customer is not allowed to access actions which are specific to clerks. However, we must also ensure that no customer can access or modify the data belonging to another customer. For this, we will use security-specific annotations, with which methods of the service classes are adorned. In effect, we will specify who is allowed to run those methods and with what parameters.

Authorization configuration file

In the DE<securityAuthorizationContext.xmlDE< file, we define an interceptor called DE<objectManagerSecurityDE< which will be attached to the function calls we want to secure. This is the root point of the security mechanism.

    <bean id="objectManagerSecurity" class="org.springframework.security.intercept.method.aopalliance.MethodSecurityInterceptor" autowire="byType">          <property name="accessDecisionManager" ref="businessAccessDecisionManager"/>          <property name="afterInvocationManager" ref="afterInvocationManager"/>          <property name="objectDefinitionSource" ref="objectDefinitionSource"/>      </bean>        <bean id="objectDefinitionSource" class="org.springframework.security.annotation.SecuredMethodDefinitionSource" />

The DE<objectManagerSecurityDE< bean that we use extends DE<org.springframework.security.intercept.AbstractSecurityInterceptorDE<. You can refer to the Spring Security API to learn more about it, but for now it suffices to say that in our application this bean will intercept the call to methods that bear some security-specific annotations. For these annotations to be taken into account, we need to add this line to the DE<security-config.xmlDE< file:

    <global-method-security secured-annotations="enabled"/>

The access decision manager bean used by the DE<objectManagerSecurityDE<, namely DE<businessAccessDecisionManagerDE<, decides whether a given user has the right to perform a certain operation on a secured object. It does this by consulting a list of registered voters which cast their opinion on whether a particular access should be allowed or not.

    <bean id="businessAccessDecisionManager" class="org.springframework.security.vote.UnanimousBased">          <property name="allowIfAllAbstainDecisions" value="true"/>          <property name="decisionVoters">              <list>                  <ref local="roleVoter"/>                  <ref local="aclObjectReadVoter"/>                  <ref local="aclObjectWriteVoter"/>                  <ref local="aclObjectDeleteVoter"/>                  <ref local="aclObjectAdminVoter"/>              </list>          </property>      </bean>

A voter implementation returns an integer, whose possible values are given by the static fields DE<ACCESS_ABSTAINDE<, DE<ACCESS_DENIEDDE< and DE<ACCESS_GRANTEDDE<. Each voter has a list of permissions with which it can operate, and a reference to the DE<aclServiceDE< bean, which is used to retrieve the ACL-s. Based on these permission definitions (shown below), and the data returned by the DE<aclServiceDE<, the voter expresses its opinion on whether access should be granted or not, by returning one of these values. However, the final decision about granting or denying access is taken by the decision manager, typically by considering the values the registered voters return. We use here the DE<UnanimousBasedDE< bean, which is a simple concrete implementation of DE<org.springframework.security.AccessDecisionManagerDE< that requires all voters to either abstain or grant access if the access is to be granted. Having set the DE<allowIfAllAbstainDecisionsDE< property to true, access is granted even if all voters abstain (and obviously none denies the access).

An individual DE<AclEntryVoterDE< has three mandatory constructor arguments. The first one is a reference to the DE<aclServiceDE<, the second one is the action that is to be secured, and the third one consists in a list of permissions based on which the action can be carried out. We also need to set the property DE<processDomainObjectClassDE< with the class name of the object instances we want to secure. Since in our application we inherit all the objects we wish to secure from DE<AbstractSecureObjectDE<, we need one single such XML entry in the DE<securityAuthorizationContext.xmlDE< configuration file.

   <bean id="aclObjectReadVoter" class="org.springframework.security.vote.AclEntryVoter">          <constructor-arg ref="aclService"/>          <constructor-arg value="ACL_OBJECT_READ"/>          <constructor-arg>              <list>                  <ref local="administrationPermission"/>                  <ref local="readPermission"/>              </list>          </constructor-arg>          <property name="processDomainObjectClass" value="com.denksoft.springstarter.entity.AbstractSecureObject"/>      </bean>

In our application there are other three decision voters that are constructed in the same way. For example, if we want another voter to decide if it grants permission to write to object, simply replace DE<ACL_OBJECT_READDE< with DE<ACL_OBJECT_WRITEDE< and set the desired permissions in the constructor (DE<writePermissionDE<, DE<administrationPermissionDE<).

The full voter configuration in the SpringStarter application is defined in the file DE<securityAuthorizationContext.xmlDE<. Since all the classes whose objects we wish to secure (DE<CustomerDE<, DE<ClerkDE<, DE<BankAccountDE< and DE<BankAccountOperationDE<) are derived from DE<AbstractSecureObjectDE<, these four voters are the only ones we need to define for object-related security.

The generic role-based voter, DE<roleVoterDE<, is declared separetely. It reads the DE<ROLE_*DE< configuration settings from the current authenticated principal’s granted authorities (see DE<security-config.xmlDE<).

    <bean id="roleVoter" class="org.springframework.security.vote.RoleVoter"/>

We define now the ACL permissions that we will use throughout the application: administration, read, write, and delete. For this, we use the helper class DE<FieldRetrievingFactoryBeanDE< which retrieves these values from Spring Security’s default implementation of DE<org.springframework.security.acls.PermissionDE< interface.

     <bean id="administrationPermission" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">          <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/>      </bean>        <bean id="readPermission" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">           <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.READ"/>      </bean>        <bean id="writePermission" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">          <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.WRITE"/>      </bean>           <bean id="deletePermission" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">          <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.DELETE"/>      </bean>

An DE<AclServiceDE< provides support for working with ACL-secured instances. The DE<MutableAclServiceDE< is used for creating and storing such information in the database. The Spring Security default implementation DE<JdbcMutableAclServiceDE< contains a series of queries for this, but since these queries are not compatible with PostgreSQL and there are no setter methods for us to change them, we need to implement our own version of DE<MutableAclServiceDE<, which we called DE<PostgresqlJdbcMutableAclServiceDE<. Its full implementation is available in the SpringStarter application.

    <bean id="aclService" class="com.denksoft.springstarter.util.security.PostgresqlJdbcMutableAclService">          <constructor-arg ref="dataSource"/>          <constructor-arg ref="lookupStrategy"/>          <constructor-arg ref="aclCache"/>      </bean>

To configure the data source properties, we reuse the contents of the DE<hibernate.propertiesDE< file. In the XML snippet below, the DE<${...}DE< placeholders are set automagically to their corresponding values from the DE<hibernate.propertiesDE< file.

    <context:property-placeholder location="WEB-INF/classes/hibernate.properties"/>        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"            p:driverClassName="${hibernate.connection.driver_class}"            p:url="${hibernate.connection.url}"            p:username="${hibernate.connection.username}"            p:password="${hibernate.connection.password}"/>

A lookup strategy bean retrieves the ACLs from the database. We use the default Spring Security implementation called DE<BasicLookupStrategyDE< and we configure it to use the data source we defined earlier. In addition, we set a caching layer(DE<aclCacheDE<) and a logger (DE<ConsoleAuditLoggerDE<).

The DE<aclAuthorizationStrategyDE< is used to determine whether a principal is permitted to call administrative methods on the ACLs. In our case, the principal needs DE<ROLE_ADMINDE< to call methods that modify permissions.

All these settings come together in the configuration file DE<securityAuthorizationContext.xmlDE<. Here, DE<aclAuthorizationStrategyDE< has three required parameters: the first one is the authority needed to change ownership, the second one is the authority needed to modify auditing details, and the third one is the authority needed to change other ACL and ACE details.

    <bean id="lookupStrategy" class="org.springframework.security.acls.jdbc.BasicLookupStrategy">          <constructor-arg ref="dataSource"/>          <constructor-arg ref="aclCache"/>          <constructor-arg ref="aclAuthorizationStrategy"/>          <constructor-arg>              <bean class="org.springframework.security.acls.domain.ConsoleAuditLogger"/>          </constructor-arg>      </bean>        <bean id="aclCache" class="org.springframework.security.acls.jdbc.EhCacheBasedAclCache">          <constructor-arg>              <bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">                  <property name="cacheManager">                      <bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"/>                  </property>                  <property name="cacheName" value="aclCache"/>              </bean>          </constructor-arg>      </bean>        <bean id="aclAuthorizationStrategy" class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">          <constructor-arg>              <list>                  <bean class="org.springframework.security.GrantedAuthorityImpl">                      <constructor-arg value="ROLE_ADMIN"/>                  </bean>                  <bean class="org.springframework.security.GrantedAuthorityImpl">                      <constructor-arg value="ROLE_ADMIN"/>                  </bean>                  <bean class="org.springframework.security.GrantedAuthorityImpl">                      <constructor-arg value="ROLE_ADMIN"/>                  </bean>              </list>          </constructor-arg>      </bean>

Let us now see how we can put these definitions to good use. There are two main cases to consider: when the service method returns a single object, and when the service method returns a collection of objects. Let us start with a simple common service layer method that returns a single object:

    Customer getCustomer(long id);

In our application, only principals with permissions to read the given customer should be allowed to obtain it. To make this check, the DE<CustomerDE< instance is retrieved and passed to the DE<AclEntryAfterInvocationProviderDE<. If the authenticated object does not have the permission to read it, then the provider will throw DE<AccessDeniesExceptionDE<.

The bean below processes DE<AFTER_ACL_READDE< configuration settings:

    <bean id="afterAclRead" class="org.springframework.security.afterinvocation.AclEntryAfterInvocationProvider">          <constructor-arg ref="aclService"/>          <constructor-arg>              <list>                  <ref local="administrationPermission"/>                  <ref local="readPermission"/>                </list>          </constructor-arg>      </bean>

Let us look at what needs to be done to secure a service layer method that returns a collection of objects:

    public List getCustomers();

We use here the DE<afterAclCollectionReadDE< bean, which handles the DE<AFTER_ACL_COLLECTION_READDE< action:

    <bean id="afterAclCollectionRead" class="org.springframework.security.afterinvocation.AclEntryAfterInvocationCollectionFilteringProvider">          <constructor-arg ref="aclService"/>          <constructor-arg>              <list>                  <ref local="administrationPermission"/>                  <ref local="readPermission"/>              </list>          </constructor-arg>      </bean>

The following bean is an implementation of DE<afterInvocationManagerDE<, which handles the list of DE<AfterInvocationProviderDE<-s.

    <bean id="afterInvocationManager" class="org.springframework.security.afterinvocation.AfterInvocationProviderManager">          <property name="providers">              <list>                  <ref local="afterAclRead"/>                  <ref local="afterAclCollectionRead"/>              </list>          </property>      </bean>

Hierarchical roles and custom user details

In the SpringStarter application we use hierarchical roles (see [4]). In a role hierarchy, a user with a particular role has authorization to do everything that a role lower in the hierarchy is authorized to do. However, for this to happen, the higher role must be allowed to access the lower role.

In addition to using hierarchical roles, we will attach some application-specific data to authenticated users using Hibernate, so that each each role has its specific data type attached to it.

In older versions of Acegi Security, attaching application-specific user details to users was a challange (see [2]), but things have improved in Spring Security. The first step is to define our own custom user details wrapper (DE<CustomUserDetailsWrapperDE<), which contains the additional information on the user, by extending the DE<UserDetailsWrapperDE< from Spring Security.

    public class CustomUserDetailsWrapper extends UserDetailsWrapper {            private Object userInfo;            public CustomUserDetailsWrapper(UserDetails userDetails, RoleHierarchy roleHierarchy) {              super(userDetails, roleHierarchy);          }            public CustomUserDetailsWrapper(UserDetails userDetails, RoleHierarchy roleHierarchy, Object userInfo) {              super(userDetails, roleHierarchy);              this.userInfo = userInfo;          }            public Object getUserInfo() {              return userInfo;          }      }

We need to create a user details service wrapper that instantiates the newly defined DE<CustomUserDetailsWrapperDE<. For this, we wrap the default user details service from Spring Security so that authentication and authorization details are automatically retrieved, and use Hibernate to retrieve our custom data.

    public class CustomUserDetailsServiceWrapper extends HibernateDaoSupport implements UserDetailsService {            private UserDetailsService userDetailsService = null;          private RoleHierarchy roleHierarchy = null;          private Class[] userInfoObjectTypes;            public void setRoleHierarchy(RoleHierarchy roleHierarchy) {              this.roleHierarchy = roleHierarchy;          }            public void setUserDetailsService(UserDetailsService userDetailsService) {             this.userDetailsService = userDetailsService;          }            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {              UserDetails userDetails = userDetailsService.loadUserByUsername(username);                for(Class clazz: userInfoObjectTypes) {                  DetachedCriteria query = DetachedCriteria.forClass(clazz).add(Restrictions.eq(”user.username”, username));                  try {                      Object info = getHibernateTemplate().findByCriteria(query).get(0);                      return new CustomUserDetailsWrapper(userDetails, roleHierarchy, info);                  } catch (IndexOutOfBoundsException ex) {                      //do nothing                    }              }              return new CustomUserDetailsWrapper(userDetails, roleHierarchy);          }            public UserDetailsService getWrappedUserDetailsService() {              return userDetailsService;          }            public void setUserInfoObjectTypes(Class[] userInfoObjectTypes) {              this.userInfoObjectTypes = userInfoObjectTypes;          }      }

The user detail service is wired into the authentication provider in the DE<security-config.xmlDE< file.

    <bean id="userDetailsServiceWrapper"            class="com.denksoft.springstarter.util.security.CustomUserDetailsServiceWrapper"            p:roleHierarchy-ref="roleHierarchy"            p:userDetailsService-ref="usersDetailServiceJdbc"            p:sessionFactory-ref="sessionFactory">          <property name="userInfoObjectTypes" >              <list>                  <value>com.denksoft.springstarter.entity.Clerk                  <value>com.denksoft.springstarter.entity.Customer              </list>          </property>       </bean>         <bean id="roleHierarchy"            class="org.springframework.security.userdetails.hierarchicalroles.RoleHierarchyImpl">          <property name="hierarchy">              <value>                  ROLE_CLERK > ROLE_CUSTOMER              </value>          </property>      </bean>        <bean id="usersDetailServiceJdbc"            class="org.springframework.security.userdetails.jdbc.JdbcDaoImpl"            p:dataSource-ref="dataSource"            p:authoritiesByUsernameQuery="select users_username as username, authority from users_authorities inner join authorities on authorities_id=id where users_username = ?"/>        <bean id="hibernateProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean"            p:location="WEB-INF/classes/hibernate.properties"/>        <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"            p:hibernateProperties-ref="hibernateProperties"            p:configLocation="classpath:hibernate.cfg.xml"/>

We give a few additional details on the properties used for the definition of the user details service DE<userDetailsServiceWrapperDE<:

  • DE<roleHierarchyDE< - Spring Security’s implementation of roles hierarchy, in our case DE<ROLE_CLERKDE< includes the rights of DE<ROLE_CUSTOMERDE<, but not viceversa.
  • DE<userDetailsServiceDE< - The default Jdbc implementation of the user details. Because our table structure is slightly different from the default one, we overwrite the DE<authoritiesByUsernameQueryDE<.
  • DE<userInfoObjectTypesDE< - A list of entities containing application specific information that we want to attach to authenticated users.
  • DE<sessionFactoryDE< - Hibernate session factory using Java 5 annotations. The session properties are loaded from a properties file by PropertiesFactoryBean.

The DE<hibernate.cfg.xmlDE< file contains only the entity classes used by the session factory.

    <?xml version='1.0' encoding='UTF-8'?>      <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">        <hibernate-configuration>            <session-factory>              <mapping class="com.denksoft.springstarter.entity.Clerk" />              <mapping class="com.denksoft.springstarter.entity.security.User" />              <mapping class="com.denksoft.springstarter.entity.security.Authority" />              <mapping class="com.denksoft.springstarter.entity.BankAccount" />              <mapping class="com.denksoft.springstarter.entity.AbstractSecureObject" />              <mapping class="com.denksoft.springstarter.entity.BankAccountOperation" />              <mapping class="com.denksoft.springstarter.entity.Customer" />          </session-factory>      </hibernate-configuration>

ACL management

Suppose that we wish to grant a certain permission to a given user for a certain object. How do we do it? Obviously, we need to add a row in the DE<ACL_ENTRYDE< table, detailing this permission. To avoid having to do this by hand, we created a proxy interface to the DE<aclSecurityUtilDE< bean, that automates this process and offers an easy-to-use API.

Let us list the XML definitions first, explanations follow.

    <bean id="aclSecurityUtil" class="org.springframework.aop.framework.ProxyFactoryBean">          <qualifier value="aclSecurity"/>          <property name="proxyInterfaces" value="com.denksoft.springstarter.util.security.AclSecurityUtil"/>          <property name="interceptorNames">              <list>                  <idref local="transactionInterceptor"/>                  <idref local="aclSecurityUtilTarget"/>              </list>          </property>      </bean>        <bean id="aclSecurityUtilTarget" class="com.denksoft.springstarter.util.security.AclSecurityUtilImpl" p:mutableAclService-ref="aclService"/>        <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"            p:transactionManager-ref="jdbcTransactionManager">          <property name="transactionAttributeSource">              <value>                  com.denksoft.springstarter.util.security.AclSecurityUtil.deletePermission=PROPAGATION_REQUIRED                  com.denksoft.springstarter.util.security.AclSecurityUtil.addPermission=PROPAGATION_REQUIRED              </value>          </property>      </bean>        <bean id="jdbcTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource"/>

The DE<aclSecurityUtilDE< bean is an AOP proxy to DE<com.denksoft.springstarter.util.security.AclSecurityUtilDE< interface implementation, and it has with two interceptors:

  • the DE<transactionInterceptorDE<, since Spring Security uses JDBC to retrieve ACL-s;
  • the DE<aclSecurityUtilTargetDE<, which is the bean that will be proxied.

The DE<AclSecurityUtilDE< service interface defines the following reasonable-looking methods:

    public interface AclSecurityUtil {          public void addPermission(AbstractSecureObject securedObject, Permission permission, Class clazz);          public void addPermission(AbstractSecureObject securedObject, Sid recipient, Permission permission, Class clazz);          public void deletePermission(AbstractSecureObject securedObject, Sid recipient, Permission permission);      }

Its corresponding implementation is listed below:

    public class AclSecurityUtilImpl implements AclSecurityUtil {            private static Log logger = LogFactory.getLog(AclSecurityUtil.class);            private MutableAclService mutableAclService;            public void setMutableAclService(MutableAclService mutableAclService) {              this.mutableAclService = mutableAclService;          }            public void addPermission(AbstractSecureObject secureObject, Permission permission, Class clazz) {                addPermission(secureObject, new PrincipalSid(getUsername()), permission, clazz);          }            public void addPermission(AbstractSecureObject securedObject, Sid recipient, Permission permission, Class clazz) {              MutableAcl acl;              ObjectIdentity oid = new ObjectIdentityImpl(clazz.getCanonicalName(), securedObject.getId());                try {                  acl = (MutableAcl) mutableAclService.readAclById(oid);              } catch (NotFoundException nfe) {                  acl = mutableAclService.createAcl(oid);              }                       acl.insertAce(acl.getEntries().length, permission, recipient, true);              mutableAclService.updateAcl(acl);                if (logger.isDebugEnabled()) {                  logger.debug("Added permission " + permission + " for Sid " + recipient + " securedObject " + securedObject);              }          }            public void deletePermission(AbstractSecureObject securedObject, Sid recipient, Permission permission, Class clazz) {              ObjectIdentity oid = new ObjectIdentityImpl(clazz.getCanonicalName(), securedObject.getId());              MutableAcl acl = (MutableAcl) mutableAclService.readAclById(oid);                // Remove all permissions associated with this particular recipient (string equality used to keep things simple)              AccessControlEntry[] entries = acl.getEntries();                for (int i = 0; i < entries.length; i++) {                  if (entries[i].getSid().equals(recipient) && entries[i].getPermission().equals(permission)) {                      acl.deleteAce(i);                  }              }                mutableAclService.updateAcl(acl);                if (logger.isDebugEnabled()) {                  logger.debug(”Deleted securedObject ” + securedObject + ” ACL permissions for recipient ” + recipient);              }          }            protected String getUsername() {              Authentication auth = SecurityContextHolder.getContext().getAuthentication();                if (auth.getPrincipal() instanceof UserDetails) {                  return ((UserDetails) auth.getPrincipal()).getUsername();              } else {                  return auth.getPrincipal().toString();              }          }      }

The DE<addPermission(AbstractSecureObject securedObject, Sid recipient, Permission permission, Class clazz)DE< function creates or retrieves the ACL of the DE<securedObjectDE< and adds a new DE<permissionDE< to the desired DE<recipientDE<, while a call to DE<addPermission(AbstractSecureObject securedObject, Permission permission, Class clazz)DE< adds permissions to the current authenticated user. The DE<deletePermission(AbstractSecureObject securedObject, Sid recipient, Permission permission, Class clazz)DE< method removes permissions for the desired DE<recipientDE<. The DE<sidDE< can be a DE<PrincipalDE<, DE<SidDE<, or DE<GrantedAuthoritySidDE<.

We show now how this API can be used in the application logic. Since we wish to know exactly the point where ACL operations are performed (as opposed to having these operations scattered and copy-pasted throughout the code), we define a DE<SecurityServiceDE< interface which handles permission management. In its implementation, we have autowired the DE<aclSecurityUtilDE< bean discussed above, using the qualifier name property from its bean declaration ().

    public class SecurityServiceImpl implements SecurityService {            @Autowired          @Qualifier("aclSecurity")          private AclSecurityUtil aclSecurityUtil;            public void setCustomerPermissions(Customer customer) {              Sid sid = new PrincipalSid(customer.getUser().getUsername());              Sid sidAdmin = new GrantedAuthoritySid("ROLE_CLERK");                aclSecurityUtil.addPermission(customer, sid, BasePermission.ADMINISTRATION, Customer.class);              aclSecurityUtil.addPermission(customer, sidAdmin, BasePermission.ADMINISTRATION, Customer.class);          }      }

This piece of code code adds administration permissions to the registered customer and to any principal with the role DE<ROLE_CLERKDE<.

Let us now consider an application-level service interface, DE<CustomerServiceDE<. To secure it, we must attach to it the DE<objectManagerSecurityDE< interceptor in the DE<applicationContext.xmlDE< file. Again, we will use the DE<ProxyFactoryBeanDE< class to build an AOP proxy around our service implementation.

    <bean id="customerServiceTarget" class="com.denksoft.springstarter.service.impl.CustomerServiceImpl"/>        <bean id="customerService" class="org.springframework.aop.framework.ProxyFactoryBean">          <qualifier value="customerService"/>          <property name="proxyInterfaces" value="com.denksoft.springstarter.service.CustomerService"/>          <property name="interceptorNames">              <list>                  <idref bean="objectManagerSecurity"/>                  <idref local="customerServiceTarget"/>              </list>          </property>      </bean>

The interface is adorned with Java 5 annotations:

    public interface CustomerService {          @Secured({"ROLE_CUSTOMER","AFTER_ACL_READ"})          public Customer getCustomer(long id);            @Secured({"ROLE_CUSTOMER","ACL_OBJECT_ADMIN"})          public void modifyBankAccount(BankAccount bankAccount);            @Secured({"ROLE_CUSTOMER","AFTER_ACL_COLLECTION_READ"})          public Collection getCustomerBankAccounts();      }

After this definition is in place, in order to execute a call to method DE<getCustomerDE<, the calling principal must first have the role DE<ROLE_CUSTOMERDE<. Once the object is retrieved, the DE<objectManagerSecurityDE< kicks in, and its DE<afterInvocationManagerDE< method checks if the authenticated user has the rights to read this object before returning it. If the user has the required rights then the object is returned to the user, otherwise, the method throws an exception.

If the user wishes to modify a bank account, he must have the role DE<ROLE_CUSTOMERDE< and also the administration role for the BankAccount instance involved, which is given as function parameter to DE<modifyBankAccountDE<. Note the difference between the pairs DE<AFTER_ACL_READDE<, DE<AFTER_ACL_COLLECTION_READDE< and DE<ACL_OBJECT_READDE<, DE<ACL_OBJECT_ADMINDE<: the rights in the first pair are used for function calls returning secured objects, while the ones in the second pair are used for function calls that have parameters of secured object type.

Conclusions

Setting up a Spring Security application for effective use is no walk in the park. However, when considering doing these things by hand, with all the bugs and productivity loss that this approach may bring, Spring Security certainly appears as an attractive option, even for relatively simple applications. Once in place, a system like the one we configured gives confidence to the development team that a carefully developed and tested security backbone is in place, allowing for the use of more complex security scenarios for the future, should such needs arise.

We look forward to receiving your feedback on improving the SpringStarter application and on making the explanations here easier to understand.

References


[1] http://en.wikipedia.org/wiki/Access_Control_List
[2] http://www.javalobby.org/java/forums/t91426.html
[3] http://springframework.org/spring-security/site/reference/html/springsecurity.html
[4] http://jira.springframework.org/secure/attachment/12872/HierarchicalRoles.pdf
[5] SpringStarter 0.0.1 application
[6] http://metauml.sourceforge.net/
[7] http://en.wikipedia.org/wiki/Class_diagram

  评论这张
 
阅读(1969)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017