Portlet 认证

对于受门户网站保护的资源,IBM® WebSphere® Portal 使用 CORBA 凭证和一个加密的 LTPA Cookie 来认证用户。然而,对于需要拥有自己的认证的后端系统,Portlet 需要提供某些认证表单来访问这些远程应用程序。为了提供单点登录用户体验,Portlet 必须能够存储和检索用于其特定关联应用程序的 Portlet。然后,Portlet 使用这些凭证代表用户登录。WebSphere Portal支持使用凭证保险库,用户和管理员可以在凭证保险库中安全地存储认证凭证。针对从保险库中解压缩用户凭证而编写的 Portlet 可以向用户隐藏登录提问。

凭证保险库正好提供了此功能。Portlet 可以通过凭证保险库 Portlet 服务 (CredentialVaultService) 来使用该功能。请参阅 Portlet 服务,以获取有关 Portlet 服务和如何访问它们的概述。下面提供了关于凭证保险库的更多信息。

凭证保险库组织

门户网站服务器的凭证保险库按如下所示进行组织:
  • 门户网站管理员可以将保险库分为几个保险库段。保险库段仅能由门户网站管理员创建和配置。
  • 一个保险库段包含一个或多个保险库槽。保险库槽是 Portlet 存储和检索用户凭证的“抽屉”。每个槽拥有一个凭证。
  • 保险库槽链接到保险库实现中的资源
    • 保险库实现是用户凭证的存储位置。保险库实现的示例包括缺省数据库保险库或 Security Access Management 锁定框。
    • 保险库实现中的资源与应用程序或需要拥有自己的认证的后端系统对应。资源的示例包括 Lotus Notes、个人记录或银行帐户。

保险库段

保险库段被标志为管理员管理或用户管理。当 Portlet(代表门户网站用户)可以设置和检索这两种类型的段中的凭证时,它们只能在用户管理的保险库段中创建保险库槽。下图显示如何在不同的保险库实现中分布管理员管理的保险库段。用户管理的保险库段只有一个,它位于 WebSphere Portal 所提供的缺省定制保险库中。

保险库实现图形。

保险库槽

WebSphere Portal 提供的凭证保险库可以区别四种不同类型的保险库槽:
  • 管理员管理的保险库段中的保险库槽:
    • 系统槽存储所有用户和所有 Portlet 之间共享实际保密信息的系统凭证。它是属于管理段的共享槽。通过在管理员管理的保险库段中添加新槽并将其标为共享槽,可以借助凭证保险库管理 Portlet 创建此槽类型。
    • 管理槽允许每个用户针对管理员定义的资源(例如,Lotus Notes)来存储保密信息。它是属于管理段的非共享槽。通过在管理员管理的保险库段中添加新槽但不将其标为共享槽,可以借助凭证保险库管理 Portlet 创建此槽类型。
  • 用户管理的保险库段中的保险库槽:
    • 共享用户槽存储用户的 Portlet 之间共享的用户凭证。它是属于用户段的共享槽。请使用“凭证保险库”Portlet 创建共享槽。将共享槽设置为 true
    • Portlet 专用槽存储 Portlet 之间不共享的用户凭证。它是属于用户段的非共享槽。请使用“凭证保险库”Portlet 创建共享槽。将共享槽设置为 false
下表显示了适用于可用保险库槽类型的约束:
表 1. 适用于可用保险库槽类型的约束
保险库槽类型 段类型 Shared 创建方式 保密信息共享
系统槽 管理员管理 true 凭证保险库管理 Portlet 每个系统一个保密信息 - 在所有用户和所有 Portlet 之间共享
管理槽 管理员管理 false 凭证保险库管理 Portlet 每个用户一个保密信息 - 在用户的所有 Portlet 之间共享
共享用户槽 用户管理 true 凭证保险库 Portlet 服务 每个用户一个保密信息 - 在用户的所有 Portlet 之间共享
Portlet 专用槽 用户管理 false 凭证保险库 Portlet 服务 每个用户和 Portlet 实体一个保密信息 - 在 Portlet 之间不共享

凭证对象

凭证保险库 Portlet 服务以凭证对象的形式返回凭证。下面是所有凭证对象的基本接口类:
标准 Portlet API
com.ibm.portal.portlet.service.credentialvault.credentials.Credential
被动凭证对象是凭证保密信息的容器。使用被动凭证的 Portlet 需要从凭证中抽取保密信息,并与后端本身进行所有认证通信。被动凭证对象使用以下样本(伪)代码:
Object userSecret = credential.getUserSecret();

  < portlet connects to backend system authenticates using the user's secret >

  < portlet can use the connection to communicate with the backend application >

  < portlet takes care of logging at the backend >

门户网站中所有可用的凭证类型都在凭证类型注册表中进行注册。WebSphere Portal 提供了少量凭证类型,但您可以在此注册表中注册额外的凭证对象。

PassiveCredential 接口继承自 Credential 基本接口。下列主题描述了 WebSphere Portal 所提供的不同类型的被动凭证对象:

被动凭证对象:
SimplePassive
此凭证对象以可序列化 Java 对象的形式存储保密信息。由于保险库服务当前不支持二进制大对象(BLOB)保密信息,因此该服务仅供将来使用。
UserPasswordPassive
用户标识/密码对形式存储保密信息的凭证对象。
JaasSubjectPassive
此凭证对象以 javax.security.auth.Subject 对象的形式存储保密信息。另外,此类保密信息当前无法由保险库服务存储。它用作从用户会话获取的非持久瞬态凭证。
BinaryPassiveCredential
此凭证对象以字节列的形式存储保密信息。
将凭证对象存储在 PortletSession 中

凭证对象未实现 java.io.Serializable - 出于安全原因,不能将它们存储在 PortletSession 中。凭证类会将实际的凭证保密信息作为它们的某个专用属性进行存储。因此,任何可以访问应用程序服务器会话数据库的人员都可能会发现此保密信息。

但是,如果您确定不会在集群环境中对凭证对象进行序列化,那么可以将其存储在 PortletSession 中。实现此操作的一种方法是定义一个凭证容器类,该类会将实际凭证对象作为瞬态成员进行存储。然后,可以将此容器对象存储在 PortletSession 中,而不会产生任何问题。您必须确保检查序列化期间凭证对象是否丢失,并且如果丢失,那么重新从保险库中检索凭证对象。

图 1. 示例:凭证对象会话容器。
import com.ibm.portal.portlet.service.credentialvault.credentials.Credential;

public class CredentialSessionContainer implements java.io.Serializable
{
   private transient Credential credential = null;

   public void setCredential(Credential cred) {this.credential = cred;}

   public Credential getCredential() {return credential;}

   public boolean hasCredential() {return credential != null;}
}

凭证保险库使用方案

Portlet 需要凭证来完成其服务,它们有两个选项:
  1. 使用门户网站管理员在管理员管理的保险库段中定义的现有槽。
  2. 在用户管理的保险库段中创建槽。

您选择的选项取决于如何使用 Portlet。通常最好的解决方案是,向用户隐藏凭证保险库的技术详细信息。以下是某些使用槽的示例方案。

内部网 Lotus Notes 邮件 Portlet
  • 公司拥有内部网雇员门户网站。每个门户网站用户都拥有 Lotus Notes 邮件服务器帐户,并且 Lotus Notes 邮件 Portlet 将部署在员工的其中一个缺省门户网站页面上并进行预配置。
  • 设计解决方案。
  • Notes 邮件 Portlet 需要存储用户的 Notes 密码。由于大部分用户使用此 Portlet,因此管理员需要通过凭证保险库管理 Portlet 为它创建“Lotus Notes 凭证槽”。使用 Portlet 的配置方式,管理员可为所有 Portlet 条目设置保险库槽标识。Portlet 允许用户以编辑方式设置其个人 Notes 密码。Portlet 可以在凭证保险库中存储每个用户的密码。
  • 如果公司使用与门户网站使用同一单点登录域中的 Domino® 服务器,那么可以使用基于用户的 JAAS 主题的 LTPAToken 凭证。此凭证使您能通过已认证的连接(此连接重复使用用户的 LTPA 令牌)访问 domino 服务器。
库存 Portlet

公司的购买部门运行一个集成不同旧应用程序的门户网站。其中一个应用程序是直接连接到供应商的系统的大型机订购应用程序。多个雇员使用订购 Portlet。然而,大型机应用程序由单个系统标识保护;它不支持多个用户帐户。

设计解决方案:订购 Portlet 需要使用系统标识访问订购应用程序。管理员配置会在 Portlet 部署期间配置保险库槽标识。因此,门户网站管理员在管理员管理的保险库段中创建保险库槽,并且把它标记为系统凭证。管理员使用凭证保险库 Portlet 在此槽中存储订购系统标识和密码。购买部门的雇员完全不必关心凭证。

因特网邮件联合 POP3 Portlet

因特网社区门户网站在其他功能中还提供了邮件联合 Portlet,门户网站用户可以使用它从许多 POP3 邮件帐户收集邮件。

设计解决方案:邮件联合 Portlet 只是社区门户网站的另一个功能,因此可能仅由某些门户网站用户使用。另外,不确定用户希望联合的帐户数。因此,门户网站管理员为该 Portlet 创建保险库槽是没有意义的。相反,Portlet 以编辑方式为用户提供适当的配置。用户可以按需添加许多 POP3 邮箱。Portlet 在用户管理的保险库段中为每个用户邮箱创建一个保险库槽。

理论上,用户可以在一个页面上配置两个 Portlet 实例,一个用于企业帐户,而另一个用于专用邮件帐户。出于上述原因,并且由于和其他 Portlet 共享用户的邮件凭证很可能没有意义,因此 Portlet 创建的保险库槽最好标记为 Portlet 专用。

凭证保险库样本

凭证保险库样本部分包含有关使用凭证保险库服务的样本代码。请参阅 Portlet API Javadoc 文档,以获取有关 CredentialVaultService 方法的信息。

Rational Application Developer 的标准 Portlet 示例

当您使用 Rational Application Developer 中的 Portlet 向导来创建使用凭证保险库 Portlet 服务的 Portlet 时,向导会为您生成处理常见任务的 SecretManager 类。向导生成的会话 Bean 包含用于保密信息类型和保险库槽名称的 getter 方法和 setter 方法。以下示例显示了可能为您生成的代码,具体取决于您在创建 JSR 168 Portlet 项目时进行的选择。

注: 此代码只是您可以如何使用凭证保险库的一个演示。您必须针对特定应用程序需求进一步定制任何要生成的代码。
  1. 监视凭证保险库 Portlet 服务。
    此代码放在 SecretManager 的 init() 方法中,并从主 Portlet 类的 init() 进行调用。请参阅访问 Portlet 服务以获取有关如何使用 JNDI 检索 Portlet 服务的一般信息。
    图 2. 检索凭证保险库服务
    public static void init(PortletConfig config) throws PortletException {
      try {
        if( vaultService == null ) {
          Context ctx = new InitialContext();
          PortletServiceHome cvsHome = 
            (PortletServiceHome)ctx.lookup("portletservice/com.ibm.portal.portlet.
               service.credentialvault.CredentialVaultService"); 
          if (cvsHome != null) {
            vaultService = (CredentialVaultService)cvsHome.getPortletService
              (CredentialVaultService.class);
          }
        }
      } catch (Exception e) {
        throw(new PortletException("Error on init()", e)); 
      }
    }
  2. 设置凭证。
    Portlet 的 processAction() 方法从来自编辑 JSP 的操作请求中获取 USERIDPASSWORD 参数。如果这两个参数都不为 Null,那么可使用它们来设置凭证。
    图 3. 获取有关操作请求的凭证
    if( request.getParameter(USER_SUBMIT) != null ) {
       // Set userId/password text in the credential vault
       PortletSessionBean sessionBean = getSessionBean(request);
       if( sessionBean!=null ) {
          String userID   = request.getParameter(USERID);
          String password = request.getParameter(PASSWORD);
          // save only if both parameters are set
          if(userID!=null && password!=null && !userID.trim().equals("") && 
            !password.trim().equals("")) {
             try {
                SecretManager.setCredential(request,sessionBean,userID,password);
             }
             catch (Exception e) {
                //Exception Handling
             }
          }
       }
    }

    SecretManager 类中的 setCredential() 方法可确定 Portlet 是否可以写入槽以及槽标识是否有内容。如果可以写入槽且槽标识有内容,那么此 setCredential() 方法会使用凭证保险库服务的 setCredentialSecretUserPassword() 方法在槽中设置凭证。

    图 4. 设置凭证
    public static boolean setCredential(PortletRequest portletRequest,
                                        PortletSessionBean sessionBean,
                                        String userID,
                                        String password)  throws PortletException {
    try {
       if( isWritable(sessionBean) ) {
          String slotId = getSlotId(portletRequest,sessionBean,true); // create
            slot if necessary
          if( slotId != null ) {
             vaultService.setCredentialSecretUserPassword(slotId,userID,password.
            toCharArray(),portletRequest);
             return true;
          }
       }
    }
    catch( CredentialVaultException e) {
    			//Exception Handling
    }
    return false;
    }
  3. 获取用于存储用户凭证的槽。
    setCredential() 方法调用此方法来创建新槽或使用现有可访问的槽。
    • 槽名称设置为 Portlet 首选项,因此管理员可以将它更改为使用门户网站管理接口创建的任何槽。
    • 对于 Portlet 专用槽,如果 Portlet 首选项中的槽名称为 null,那么此方法会创建一个新槽。createNewSlot() 方法是 SecretManager 的另一个定制方法,它用于凭证保险库服务的 createCredentialSlot() 方法。此方法可以创建非系统槽,并返回 CredentialSlotConfig 对象。新槽的 slotID 存储在 Portlet 的首选项中,并用于更新和获取 Portlet 专用凭证。
    • 对于共享槽,此方法可从可访问的槽中搜索共享资源名称。如果共享槽不可用,那么此方法会创建新槽。
    图 5. 创建或使用现有槽
    private static String getSlotId(PortletRequest portletRequest, 
                                    PortletSessionBean sessionBean, 
                                    boolean bCreate)  throws PortletException {
       String slotId = null;
       String slotName = sessionBean.getVaultSlotName();
       switch( sessionBean.getSecretType() ) {
          case SECRET_PORTLET_PRIVATE_SLOT:
             PortletPreferences prefs = portletRequest.getPreferences();
             String prefsKey = ".slot."+portletRequest.getRemoteUser()+".
               "+slotName;
             slotId = prefs.getValue(prefsKey,null);
             if( slotId==null && bCreate ) {
                slotId = createNewSlot(portletRequest,slotName, true);  // 
                  create private slot
                if( slotId != null ) {
                   try {
                      prefs.setValue(prefsKey,slotId);
                      prefs.store();
                   }
                   catch( Exception e ) {
                      throw(new PortletException("Error on PortletPreferences.
                        store()", e));
                   }
                }
             }
             break;
          case SECRET_SHARED_SLOT:
             try {
                Iterator it = vaultService.getAccessibleSlots(portletRequest);
                while( it.hasNext() ) {
                   CredentialSlotConfig config = (CredentialSlotConfig)it.next() ;
                   //searches for shared resource name
                   if( config.getResourceName().startsWith(slotName ) ) {
                      slotId = config.getSlotId();
                      break;
                   }
                }
                if( slotId==null && bCreate )
                    slotId = createNewSlot(portletRequest,slotName,false);  
                      // create shared slot
             }
             catch( CredentialVaultException e) {
             // exception handling goes here
             }
             break;
          default:
             slotId = slotName;
             break;
       }
       return slotId;
    }
  4. 检索凭证
    doView() 和 doEdit 方法可从 SecretManager 中调用 getCredential()。对于被动凭证,此 Portlet 将通过此方法返回的 USERIDPASSWORD 设置为请求的属性。
    图 6. 设置被动凭证
    StringBuffer userId = new StringBuffer("");
    StringBuffer password = new StringBuffer("");
    try {
       SecretManager.getCredential(request,sessionBean,userId, password);
    }
    catch( Exception e ) {
       getPortletContext().log("Exception on SecretManager.getCredential(): "+e.getMessage());
    }
    request.setAttribute(USERID,userId.toString());
    request.setAttribute(PASSWORD,password.toString());
    SecretManager 的 getCredential() 方法可处理 UserPasswordPassiveCredential 类型。此凭证类型在 Portlet 首选项中被设置为整数。凭证保险库服务的 getCredential() 方法用于获取槽中存储的凭证。
    图 7. getCredential() 方法
    public static void getCredential(PortletRequest portletRequest,
                                     PortletSessionBean sessionBean,
                                     StringBuffer userid, 
                                     StringBuffer password) throws PortletException {
       try {
          String slotId = getSlotId(portletRequest,sessionBean,false);
             if( slotId != null ) {
                UserPasswordPassiveCredential credential = 
                   (UserPasswordPassiveCredential)vaultService.getCredential
                      (slotId,CredentialTypes.USER_PASSWORD_PASSIVE,new 
                        HashMap(),portletRequest);
                if( credential != null) {
                   userid.append(credential.getUserId());
                   password.append(String.valueOf(credential.getPassword()));
                }
             }
       }
       catch( CredentialVaultException e) {
       // exception handling goes here
       }
    }

更改凭证保险库加密

WebSphere Portal 支持插入用于存储和检索凭证的不同保险库适配器。WebSphere Portal 随附的缺省保险库适配器将用户凭证存储在门户网站配置数据库中。缺省情况下,仅混乱密码,而不加密密码。存在一个加密插入点,可在此使用定制加密对密码进行加密。下列各节提供定制构建加密逻辑,此逻辑可以插入到现有的凭证保险库基础结构中。

定制加密

要为缺省保险库适配器编写定制加密出口,必须实现 WebSphere Portal 所提供的 EncryptionExit 接口,然后部署此接口。

要创建并使用定制加密出口,请执行以下操作:
  1. 编写实现了以下公共 SPI 接口的类:com.ibm.portal.portlet.service.credentialvault.spi.EncryptionExit。
    图 8. 加密出口接口包 com.ibm.portal.portlet.service.credentialvault.spi;公共接口 EncryptionExit
    {
    /**This method is called during portal start up*/
    public void init() throws CredentialVaultException;
    /**This method is called during portal shut down*/
    public void destroy();
    /**Encrypts the password. The password is only stored encrypted. After
    *getting it from the store it needs to be decrypted.*/
    public char[] encryptPassword(char[] password) throws CredentialVaultException;
    /**Decrypts the password as a char[]*/
    public char[] decryptPassword(char[] password)throws CredentialVaultException;
    }
  2. 要将新类部署到 WebSphere Portal,请创建包含新的加密出口类的 JAR 文件。
  3. 在此文件中设置适配器文件属性的名称:将 <wp_root>/shared/app/config/services/VaultService.properties 设置为 default.config=defaultvault.properties
  4. 创建包含以下内容的 defaultvault.properties 文件:encryptionExit=com.yourcompany.YourEncryptionExit
  5. 将代码部署到 WebSphere Portal。有关如何将代码部署到 WebSphere Portal 的更多信息,请参阅“扩展 WebSphere Portal 类路径”
  6. 重新启动 WebSphere Portal
    重新启动门户网站服务器后,缺省保险库适配器将读取更改后的配置,并使用 init() 方法将新的加密出口类实例化。对缺省保险库适配器所处理的凭证保险库进行的所有读写访问现在都将定制加密出口用于用户标识/密码凭证中的密码。不会对二进制凭证进行加密。
    要点: 如果您更改了门户网站的加密逻辑,那么已存储在凭证保险库中的现有密码将变为不可用,这是因为这些密码仍使用缺省逻辑进行编码。因此,最好先插入定制加密逻辑,然后才在生产环境中使用系统。门户网站用户必须手动地重新输入现有密码才能采用新的加密逻辑。重新输入密码后,该密码将使用定制加密出口,这使您能够再次正确地检索密码。