站内搜索: 请输入搜索关键词
当前页面: 在线文档首页 > JBoss Seam 1.1.5 正式版英文参考手册

Chapter 12. Security - JBoss Seam 1.1.5 正式版英文参考手册

Chapter 12. Security

The Seam Security API is an optional Seam feature that provides authentication and authorization features for securing both domain and page resources within your Seam project.

12.1. Authentication

The authentication features provided by Seam Security are built upon JAAS (Java Authentication and Authorization Service), and as such provide a robust and highly configurable API for handling user authentication. However, for less complex authentication requirements Seam offers a much more simplified method of authentication that hides the complexity of JAAS.

12.1.1. Configuration

The simplified authentication method uses a built-in JAAS login module, SeamLoginModule, which delegates authentication to one of your own Seam components. This login module is already configured inside Seam as part of a default application policy and as such does not require any additional configuration files. It allows you to write an authentication method using the entity classes that are provided by your own application. Configuring this simplified form of authentication requires the identity component to be configured in components.xml:

        
    <security:identity authenticate-method="#{authenticator.authenticate}"/>          
        
      

The EL expression #{authenticator.authenticate} is a method binding indicating that the authenticate method of the authenticator component will be used to authenticate the user.

12.1.2. Writing an authentication method

The authenticate-method property specified for identity in components.xml specifies which method will be used by SeamLoginModule to authenticate users. This method is expected to conform to the following prototype:

        
  boolean (java.lang.String username, java.lang.String password, java.util.Set roles)
        
      

The first and second parameters should hopefully be self-explanatory. The third parameter (roles) is a Set object that should be populated with the user's roles only if authentication is successful. The return value should be set to true for a successful authentication, or false for an unsuccessful authentication. Here's a complete example of an authentication method inside a JavaBean component:

        
@Name("authenticator")
public class Authenticator {
   @In EntityManager entityManager;
   public boolean authenticate(String username, String password, Set<String> roles) {
      try
      {
         User user = (User) entityManager.createQuery(
            "from User where username = :username and password = :password")
            .setParameter("username", username)
            .setParameter("password", password)
            .getSingleResult();

         if (user.getRoles() != null)
         {
            for (UserRole mr : user.getRoles())
               roles.add(mr.getName());
         }

         return true;
      }
      catch (NoResultException ex)
      {
         FacesMessages.instance().add("Invalid username/password");
         return false;
      }
   }
        
      

In the above example, both User and UserRole are application-specific entity beans. The roles parameter is populated with the roles that the user is a member of, which should be added to the Set as literal string values, e.g. "admin", "user". In this case, if the user record is not found and a NoResultException thrown, the authentication method returns false to indicate the authentication failed.

12.1.3. Writing a login form

The Identity component provides both username and password properties, catering for the most common authentication scenario. These properties can be bound directly to the username and password fields on a login form. Once these properties are set, calling the identity.login() method will authenticate the user using the provided credentials. Here's an example of a simple login form:

        
    <div>
      <h:outputLabel for="name" value="Username"/>
      <h:inputText id="name" value="#{identity.username}"/>
    </div>

    <div>
      <h:outputLabel for="password" value="Password"/>
      <h:inputSecret id="password" value="#{identity.password}"/>
    </div>

    <div>
      <h:commandButton value="Login" action="#{identity.login}"/>
    </div>
        
      

Similarly, logging out the user is done by calling #{identity.logout}. Calling this action will clear the security state of the currently authenticated user.

12.1.4. Summary

So to sum up, there are the three easy steps to configure authentication:

  • Configure an authentication method in components.xml.

  • Write an authentication method.

  • Write a login form so that the user can authenticate.

12.2. Authorization

There are a number of authorization features provided by the Seam Security API for securing access to components, component methods, and pages. This section describes each of these.

12.2.1. Core concepts

Each of the authorization mechanisms provided by the Seam Security API are built upon the concept of a user being granted roles and/or permissions. A role is a group, or type, of user that may have been granted certain privileges for performing one or more specific actions within an application. A permission on the other hand is a privilege (sometimes once-off) for performing a single, specific action. It is entirely possible to build an application using nothing but permissions, however roles offer a higher level of convenience when granting privileges to groups of users.

Roles are simple, consisting of only a name such as "admin", "user", "customer", etc. Permissions consist of both a name and an action, and are represented within this documentation in the form name:action, for example customer:delete, or customer:insert.

12.2.2. Securing components

Let's start by examining the simplest form of authorization, component security, starting with the @Restrict annotation.

12.2.2.1. The @Restrict annotation

Seam components may be secured either at the method or the class level, using the @Restrict annotation. If both a method and it's declaring class are annotated with @Restrict, the method restriction will take precendence (and the class restriction will not apply). If a method invocation fails a security check, then an exception will be thrown as per the contract for Identity.checkRestriction().

An empty @Restrict implies a permission check of componentName:methodName. Take for example the following component method:

          
  @Name("account")
  public class AccountAction {
    @Restrict public void delete() {
      // code
    }
  }
          
        

In this example, the implied permission required to call the delete() method is account:delete. The equivalent of this would be to write @Restrict("#{s:hasPermission('account','delete',null)}"). Now let's look at another example:

          
  @Restrict @Name("account")
  public class AccountAction {
    public void insert() {
      // code
    }
    @Restrict("#{s:hasRole('admin')}") public void delete() {
      // code
    }
  }
          
        

This time, the component class itself is annotated with @Restrict. This means that any methods without an overriding @Restrict annotation require an implicit permission check. In the case of this example, the insert() method requires a permission of account:insert, while the delete() method requires that the user is a member of the admin role.

Before we go any further, let's address the #{s:hasRole()} expression seen in the above example. Both s:hasRole and s:hasPermission are EL functions, which delegate to the correspondingly named methods of the Identity class. These functions can be used within any EL expression throughout the entirity of the security API.

Being an EL expression, the value of the @Restrict annotation may reference objects that exist within a Seam context. This is extremely useful when performing permission checks for a specific object instance. Look at this example:

  @Name("account")
  public class AccountAction {
    @In Account selectedAccount;
    @Restrict("#{s:hasPermission('account','modify',selectedAccount)}")
    public void modify() {
      selectedAccount.modify();
    }
  }
        

The interesting thing to note from this example is the reference to selectedAccount seen within the hasPermission() function call. The value of this variable will be looked up from within the Seam context, and passed to the hasPermission() method in Identity, which in this case can then determine if the user has the required permission for modifying the specified Account object.

12.2.2.2. Inline restrictions

Sometimes it might be desirable to perform a security check in code, without using the @Restrict annotation. In this situation, simply use Identity.checkRestriction() to evaluate a security expression, like this:

          
  public void deleteCustomer() {
    Identity.instance().checkRestriction("#{s:hasPermission('customer','delete',selectedCustomer)}");
  }
          
        

If the expression specified doesn't evaluate to true, either 1) a NotLoggedInException exception is thrown if the user is not logged in, or 2) AuthorizationException is thrown if the user is logged in. It is also possible to call the hasRole and hasPermission methods directly:

          
  if (!Identity.instance().hasRole("admin"))
     throw new AuthorizationException("Must be admin to perform this action");

  if (!Identity.instance().hasPermission("customer", "create", null))
     throw new AuthorizationException("You may not create new customers");
          
        

12.2.3. Securing the user interface

One indication of a well designed user interface is that the user is not presented with options for which they don't have the necessary privileges to use. Seam Security allows conditional rendering of either 1) sections of a page or 2) individual controls, based upon the privileges of the user, using the very same EL expressions that are used for component security.

Let's take a look at some examples of interface security. First of all, let's pretend that we have a login form that should only be rendered if the user is not already logged in. Using the identity.isLoggedIn() property, we can write this:

        
  <h:form class="loginForm" rendered="#{not identity.loggedIn}">        
        
      

If the user isn't logged in, then the login form will be rendered - very straight forward so far. Now let's pretend there is a menu on the page that contains some actions which should only be accessible to users in the manager role. Here's one way that these could be written:

        
    <h:outputLink action="#{reports.listManagerReports}" rendered="#{s:hasRole('manager')}">
      Manager Reports
    </h:outputLink>]
        
      

This is also quite straight forward. If the user is not a member of the manager role, then the outputLink will not be rendered. The rendered attribute can generally be used on the control itself, or on a surrounding <s:div> or <s:span> control.

Now for something more complex. Let's say you have a h:dataTable control on a page listing records for which you may or may not wish to render action links depending on the user's privileges. The s:hasPermission EL function allows us to pass in an object parameter which can be used to determine whether the user has the requested permission for that object or not. Here's how a dataTable with secured links might look:

        
	<h:dataTable value="#{clients}" var="cl">
		<h:column>
			<f:facet name="header">Name</f:facet>
			#{cl.name}
		</h:column>
		<h:column>
			<f:facet name="header">City</f:facet>
			#{cl.city}
		</h:column>   
		<h:column>
			<f:facet name="header">Action</f:facet>
			<s:link value="Modify Client" action="#{clientAction.modify}" rendered="#{s:hasPermission('client','modify',cl)"/>
   		<s:link value="Delete Client" action="#{clientAction.delete}" rendered="#{s:hasPermission('client','delete',cl)"/>
		</h:column>
	</h:dataTable>
        
      

12.2.4. Securing pages

Page security requires that the application is using a pages.xml file, however is extremely simple to configure. Simply include a <restrict/> element within the page elements that you wish to secure. By default, if a value is not provided for the restrict element, an implied permission of {viewId}:render will be checked for whenever accessing that page. Otherwise the value will be evaluated as a standard security expression. Here's a couple of examples:

        
    <page view-id="/settings.xhtml">
      <restrict/>
    </page>
        
    <page view-id="/reports.xhtml">    
      <restrict>#{s:hasRole('admin')}</restrict>
    </page>        
        
      

In the above example, the first page has an implied permission restriction of /settings.xhtml:render, while the second one checks that the user is a member of the admin role.

12.3. Writing Security Rules

Up to this point there has been a lot of mention of permissions, but no information about how permissions are actually defined or granted. This section completes the picture, by explaining how permission checks are processed, and how to implement permission checks for a Seam application.

12.3.1. Permissions Overview

So how does the security API know whether a user has the customer:modify permission for a specific customer? Seam Security provides quite a novel method for determining user permissions, based on JBoss Rules. A couple of the advantages of using a rule engine are 1) a centralized location for the business logic that is behind each user permission, and 2) speed - JBoss Rules uses very efficient algorithms for evaluating large numbers of complex rules involving multiple conditions.

12.3.2. Configuring a rules file

Seam Security expects to find a RuleBase component called securityRules which it uses to evaluate permission checks. This is configured in components.xml as follows:

        
    <component class="org.jboss.seam.drools.RuleBase" name="securityRules">
        <property name="ruleFiles">/META-INF/security-rules.drl</property>
    </component>        
        
      

Once the RuleBase component is configured, it's time to write the security rules.

12.3.3. Creating a security rules file

For this step you need to create a file called security-rules.drl in the /META-INF of your application's jar file. In actual fact this file can be called anything you want, and exist in any location as long as it is configured appropriately in components.xml.

So what should the security rules file contain? At this stage it might be a good idea to at least skim through the JBoss Rules documentation, however to get started here's an extremely simple example:

        
package MyApplicationPermissions;

import org.jboss.seam.security.PermissionCheck;
import org.jboss.seam.security.Role;

rule CanUserDeleteCustomers
when
  c: PermissionCheck(name == "customer", action == "delete")
  Role(name == "admin")
then
  c.grant()
end;        
        
      

Let's break this down. The first thing we see is the package declaration. A package in JBoss Rules is essentially a collection of rules. The package name can be anything you want - it doesn't relate to anything else outside the scope of the rule base.

The next thing we can notice is a couple of import statements for the PermissionCheck and Role classes. These imports inform the rules engine that we'll be referencing these classes within our rules.

Finally we have the code for the rule. Each rule within a package should be given a unique name (usually describing the purpose of the rule). In this case our rule is called CanUserDeleteCustomers and will be used to check whether a user is allowed to delete a customer record.

Looking at the body of the rule definition we can notice two distinct sections. Rules have what is known as a left hand side (LHS) and a right hand side (RHS). The LHS consists of the conditional part of the rule, i.e. a list of conditions which must be satisfied for the rule to fire. The LHS is represented by the when section. The RHS is the consequence, or action section of the rule that will only be fired if all of the conditions in the LHS are met. The RHS is represented by the then section. The end of the rule is denoted by the end; line.

If we look at the LHS of the rule, we see two conditions listed there. Let's examine the first condition:

        
  c: PermissionCheck(name == "customer", action == "delete")        
        
      

In plain english, this condition is stating that there must exist a PermissionCheck object with a name property equal to "customer", and an action property equal to "delete" within the working memory. What is the working memory? It is a session-scoped object that contains the contextual information that is required by the rules engine to make a decision about a permission check. Each time the hasPermission() method is called, a temporary PermissionCheck object, or Fact, is asserted into the working memory. This PermissionCheck corresponds exactly to the permission that is being checked, so for example if you call hasPermission("account", "create", null) then a PermissionCheck object with a name equal to "account" and action equal to "create" will be asserted into the working memory for the duration of the permission check.

So what else is in the working memory? Besides the short-lived temporary facts asserted during a permission check, there are some longer-lived objects in the working memory that stay there for the entire duration of a user being authenticated. These include any java.security.Principal objects that are created as part of the authentication process, plus a org.jboss.seam.security.Role object for each of the roles that the user is a member of. It is also possible to assert additional long-lived facts into the working memory by calling Identity.instance().getSecurityContext().assertObject(), passing the object as a parameter.

Getting back to our simple example, we can also notice that the first line of our LHS is prefixed with c:. This is a variable binding, and is used to refer back to the object that is matched by the condition. Moving onto the second line of our LHS, we see this:

        
  Role(name == "admin")   
        
      

This condition simply states that there must be a Role object with a name of "admin" within the working memory. As mentioned, user roles are asserted into the working memory as long-lived facts. So, putting both conditions together, this rule is essentially saying "I will fire if you are checking for the customer:delete permission and the user is a member of the admin role".

So what is the consequence of the rule firing? Let's take a look at the RHS of the rule:

        
  c.grant()        
        
      

The RHS consists of Java code, and in this case is invoking the grant() method of the c object, which as already mentioned is a variable binding for the PermissionCheck object. Besides the name and action properties of the PermissionCheck object, there is also a granted property which is initially set to false. Calling grant() on a PermissionCheck sets the granted property to true, which means that the permission check was successful, allowing the user to carry out whatever action the permission check was intended for.