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

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

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. Requirements

The following jar files are required to be configured as modules in application.xml to use Seam Security:

  • drools-compiler-3.0.5.jar

  • drools-core-3.0.5.jar

  • commons-jci-core-1.0-406301.jar

  • commons-jci-janino-2.4.3.jar

  • commons-lang-2.1.jar

  • janino-2.4.3.jar

  • stringtemplate-2.3b6.jar

  • antlr-2.7.6.jar

  • antlr-3.0ea8.jar

For web-based security, jboss-seam-ui.jar must also be included in the application's war file. Also, to make use of the security EL functions, SeamFaceletViewHandler must be used. Configure it in faces-config.xml like this:

      
    <application>
        <view-handler>org.jboss.seam.ui.facelet.SeamFaceletViewHandler</view-handler>
    </application>      
      
    

12.2. 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.2.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:

        
<components xmlns="http://jboss.com/products/seam/components"
            xmlns:core="http://jboss.com/products/seam/core"
            xmlns:security="http://jboss.com/products/seam/security"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation=
                "http://jboss.com/products/seam/core http://jboss.com/products/seam/core-1.1.xsd 
                 http://jboss.com/products/seam/components http://jboss.com/products/seam/components-1.1.xsd
                 http://jboss.com/products/seam/security http://jboss.com/products/seam/security-1.1.xsd">                
        
    <security:identity authenticate-method="#{authenticator.authenticate}"/>
    
</components>    
        
      

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.2.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 takes no parameters, and is expected to return a boolean indicating whether authentication is successful or not. The user's username and password can be obtained from Identity.instance().getUsername() and Identity.instance().getPassword(), respectively. Any roles that the user is a member of should be assigned using Identity.instance().addRole(). Here's a complete example of an authentication method inside a JavaBean component:

        
@Name("authenticator")
public class Authenticator {
   @In EntityManager entityManager;
   public boolean authenticate() {
      try
      {
         User user = (User) entityManager.createQuery(
            "from User where username = :username and password = :password")
            .setParameter("username", Identity.instance().getUsername())
            .setParameter("password", Identity.instance().getPassword())
            .getSingleResult();

         if (user.getRoles() != null)
         {
            for (UserRole mr : user.getRoles())
               Identity.instance().addRole(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.2.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.2.4. Simplified Configuration - 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.5. Advanced Authentication Features

This section explores some of the advanced features provided by the security API for addressing more complex security requirements.

12.2.5.1. Using your container's JAAS configuration

If you would rather not use the simplified JAAS configuration provided by the Seam Security API, you may instead delegate to the default system JAAS configuration by providing a jaasConfigName property in components.xml. For example, if you are using JBoss AS and wish to use the other policy (which uses the UsersRolesLoginModule login module provided by JBoss AS), then the entry in components.xml would look like this:

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

12.3. 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.3.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.3.2. Securing components

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

12.3.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() (see Inline Restrictions).

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 entirety of the security API.

Being an EL expression, the value of the @Restrict annotation may reference any 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.3.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.3.3. Security in 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.3.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.4. 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.4.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.4.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:

            
   <drools:rule-base name="securityRules">
       <drools:rule-files>/META-INF/security.drl</drools:rule-files>
   </drools:rule-base>    
        
      

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

12.4.3. Creating a security rules file

For this step you need to create a file called security.drl in the /META-INF directory 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.

12.5. Handling Security Exceptions

To prevent users from receiving the default error page in response to a security error, it's recommended that pages.xml is configured to redirect security errors to a more "pretty" page. The two main types of exceptions thrown by the security API are:

  • NotLoggedInException - This exception is thrown if the user attempts to access a restricted action or page when they are not logged in.

  • AuthorizationException - This exception is only thrown if the user is already logged in, and they have attempted to access a restricted action or page for which they do not have the necessary privileges.

Here's an example of a pages.xml file that redirects these security exceptions:

      
<pages>

  <exception class="org.jboss.seam.security.NotLoggedInException">
    <end-conversation/>
    <redirect view-id="/login.xhtml">
          <message>You must be logged in to perform this action</message>
    </redirect>
  </exception>
  
  <exception class="org.jboss.seam.security.AuthorizationException">
    <end-conversation/>
    <redirect view-id="/security_error.xhtml">
          <message>You do not have the necessary security privileges to perform this action.</message>
    </redirect>
  </exception>

</pages>