当前位置导航:炫浪网>>网络学院>>编程开发>>JAVA教程>>J2EE

GT4 开发:从头开始进行相互的身份验证

    网格安全基础设施(GSI)是 Java™ 通用安全服务(Generic Security Service,GSS-API)的实现。GSS 用来在互相通信的应用程序之间安全地交换消息,它在各种底层安全机制(例如 Kerberos)之上提供了对安全服务的一致访问。在本文中,您将学习如何使用 GSI/GSS-API 扩展和代理证书构建自己的客户机-服务器应用程序。这是网格中间件所使用的基本身份验证机制。
通过模仿进行身份验证

    代理证书 是一种模仿证书。模仿(Impersonation)是一种安全技术,允许实体 A 对另外一个实体(实体 B)进行授权,让 B 可以对其他实体进行验证,就仿佛 B 是实体 A 一样。换而言之,B 模仿了 A。

    因此为什么我们会希望使用模仿技术呢?这是因为它有助于解决分布式网络中关于身份验证的两个重要问题:

    单点登录 ?? 为什么单点登录这么重要呢?假设一个用户需要在多个资源上运行某个进程,使用单点登录,用户只需要一次身份验证,而不用对每个资源都进行身份验证。 
委托 ?? 之所以需要使用委托是因为进程需要代表用户进行身份验证。因此,必须向其委托所需的权力。 
为便于解释,假设一个用户在两个主机之间启动一个远程执行服务。这个服务需要代表用户在资源上使用单点登录进行身份验证,然后必须将自己的权力委托给这两个主机,这样它们就可以相互进行身份验证了。代理证书让您可以实现这种任务。

    相互进行身份验证

    当双方都具有数字证书并且都信任对这些证书进行签名的证书权威(CA)时,这两个实体可以执行相互身份验证从而彼此证明它们就是自己声称的那个实体。信任签名 CA 实际上意味着它们必须有一些 CA 证书的拷贝(包含公钥),并且信任这些证书确实来自这些 CA。两个实体(A 和 B)之间的相互身份验证过程如下所示:

    A 建立一个到 B 的连接。要启动身份验证过程, A 将自己的证书发送给 B。这个证书声明了 A 的身份、公钥和用来证明该证书的 CA。 
    B 通过检查 CA 的数字签名来确保该证书是有效的,从而确保该 CA 对这个证书进行了签名,并且这个证书并没有被篡改过,从而确保这个证书是有效的。(B 必须信任对 A 的证书进行签名的 CA)。 
    B 通过生成一条随机消息并将其发送给 A,请求 A 对其进行加密,从而确保 A 确实是证书所标识的人。    A 使用自己的私钥对这条消息进行加密,并将结果发回给 B。B 使用 A 的公钥对消息进行解密。如果解密后的消息与原来的随机消息相同,那么 B 就可确定 A 就是它声称的那个身份(B 信任 A 的身份)。 
第 3 个步骤中相同的操作必须按照相反的顺序再执行一次。 
现在,A 和 B 就已经相互进行了身份验证。

    代理证书

    代理证书包括一个新证书,其中包括一个新公钥以及一个新私钥。这个新证书包含一个修改过的所有者的身份,它是由所有者进行签名的,而不是由 CA 进行签名的。代理证书具有以下特性:

    有限的生命期 ?? 它具有一个特定的截止时间,之后这个代理就不再有效了。 
未加密的私钥 ?? 由于代理很长时间内都不是有效的,因此其私钥就不需要像所有者的私钥一样安全地进行保存。因此,可以使用一个未加密的私钥将其存储在临时空间中,只要存放私匙的文件权限阻止任何人查看该私匙。 
    一旦创建之后,用户就可以使用代理证书和私钥来进行身份验证,而不需要输入密码。

    代理证书是对 Globus 项目所创建的 Transport Later Security(TLS)协议的扩展。Globus 结合 Global Grid Forum 一起工作,使得代理成为 TLS 的一个标准扩展,这样 GSI 代理就可以与其他 TLS 软件一起使用了。

    构建自己的启用 GSI 的客户机服务器

    我们在这里要构建的应用程序名字是 Client 和 Server。它们是使用 Java 编程语言编写的,需要 Commodity Grid (CoG) Kit 所提供的 GSI 实现和支持库。构建自己的启用 GSI 的应用程序非常简单。Client 和 Server 的骨架可以分解为如下内容:

    读取命令行参数 
    在客户机和服务器之间建立 socket 连接来传输数据 
    加载代理证书 
    建立安全上下文 
    如果需要,安全地交换消息 
    清除工作 

    读取命令行参数

    Client 和 Server 的 main 方法需要做的第一件事也是最简单的事是读取命令行参数。

    Client 需要使用两个参数:主机名和要连接的端口。


    清单 1. Client 的主机名和端口
                
// load arguments
if (args.length < 2) 
{
    System.err.println("Usage: java {options} Client "
       + " {hostName} {port}");
    System.exit(-1);
}

String hostName = args[0];
int port         = Integer.parseInt(args[1]);
 


Server 需要一个参数:监听连接使用的端口号。


    清单 2. Server 端口号
                
// read the command-line arguments
if (args.length != 1) {
    System.err.println("Usage: java {options} Server {localPort}");
    System.exit(-1);
}

int localPort = Integer.parseInt(args[0]);

    建立 socket 连接

    Java GSS-API 为创建和解释标记(不透明的字节数据)提供了方法。这些标记包含双方之间安全交换的消息,不过实际进行标记传输的方法取决于交换双方。对于我们的目的来说,在客户机和服务器之间建立了一个 socket 连接,并使用从 socket 流和安全上下文中构造的流来交换数据。

    Client 需要建立一个到 Server 的 socket 连接,并从中提取输入/输出使用的流,如下所示:


    清单 3. Client 建立一个到 Server 的 socket 连接
                
Socket socket = new Socket(hostName, port);

DataInputStream inStream = 
    new DataInputStream(socket.getInputStream());

DataOutputStream outStream = 
    new DataOutputStream(socket.getOutputStream());

System.out.println("Client: Connected to server " 
    + socket.getInetAddress());

    服务器应用程序创建了一个 ServerSocket 来监听这个端口,使用下面的方式给出参数:

ServerSocket ss = new ServerSocket(localPort);

    ServerSocket 然后可以等待并接受一个来自客户机的连接,然后对 I/O 流进行初始化,以便以后与客户机进行数据交换。



    清单 4. ServerSocket 等待并接受来客户机的连接
                
Socket socket = ss.accept();

DataInputStream inStream =
    new DataInputStream(socket.getInputStream());

DataOutputStream outStream = 
    new DataOutputStream(socket.getOutputStream());

System.out.println("Got connection from client "
    + socket.getInetAddress());
 


    Socket 对象用来与客户机进行通信,它可以通过 ServerSocket 继续监听其他客户机的连接请求。这通常是使用一个循环实现的,例如:


    清单 5. Socket 对象与客户机进行通信
                
while (true) {

    Socket socket = ss.accept();

    // Get input and output streams for the connection
    // Create a context with the client 
    // Exchange messages with the client
    // Clean up
}
 


    这个循环一次只能处理一个客户机。然而,通过使用线程,可以对服务器进行修改,从而同时处理多个客户机。

    加载代理证书

    CoG Kit 是客户机的 API,它允许客户机应用程序开发人员和管理员从更高级的框架来使用、管理网格并对网格编程。CoG 提供了 API 来加载并创建代理证书和很多其他东西。例如,调用 CoGProperties.getProxyFile() 方法返回先前由 grid-proxy-init 创建的代理证书的路径。接下来,这个证书会被加载到一个字节缓冲区中,从而为 GSI 身份验证转换成一个 GSSCredential。

CoGProperties cog = CoGProperties.getDefault();
byte proxyBytes[] =  readBinFile( cog.getProxyFile() );
 


    下一个步骤是获得 ExtendedGSSManager 对象的实例。这个类是为其他重要 GSS-API 类提供工厂服务,它提供了有关所支持机制的信息。它可以创建实现下面这 3 个 GSS-API 接口的类实例:GSSName、GSSCredential 和 GSSContext。它还具有几个方法来查询可用机制列表和每种机制所支持的名称类型。默认 ExtendedGSSManager 子类的一个实例可以通过静态方法 getInstance 来获得。

ExtendedGSSManager manager =
    (ExtendedGSSManager)ExtendedGSSManager.getInstance();
 


    下一个步骤是调用一个工厂方法获得一组机制的凭证。


    清单 6. 调用工厂方法来获取凭证
                
    GSSCredential credential = manager.createCredential(
        proxyBytes,                   // proxy data
        ExtendedGSSCredential.IMPEXP_OPAQUE,
        GSSCredential.DEFAULT_LIFETIME, // default life time
        null,                    // OID Mechanism
        GSSCredential.INITIATE_AND_ACCEPT);
        
    System.out.println("Client Credential: " 
        + credential.getName()
        + " Remaining life time:" 
        + credential.getRemainingLifetime());
 


    参数有:

    一个使用诸如 grid-proxy-init 之类的命令创建的导出缓冲区或代理证书的字节数组。 
    ExtendedGSSCredential.IMPEXP_OPAQUE 意味着导出缓冲区是一个不透明的缓冲区,适合存储到内存或磁盘中,或者传递给其他进程。 
    一个生存期值 ?? 在本例中,使用的是默认值。 
    导出凭证希望使用的机制 ?? 可以为空,表示系统默认值。 
凭证使用标记 ?? 在本例中,INITIATE_AND_ACCEPT 请求用于上下文的初始化和接受。 

    建立安全上下文

    在两个应用程序可以使用 ava GSS-API 安全交换消息之前,它们必须使用自己的凭证建立一个联合的安全上下文。

    在 Client 上,ExtendedGSSManager.createContext 是在发起端创建上下文的工厂方法。第一个参数是目标端的名字。Null 表示底层身份验证机制所提供的默认值。第二个参数是这种机制的 Object ID(OID)(同样,null 表示使用默认值)。第三个参数是上一个步骤中的 GSS 凭证,最后一个参数是这个上下文的默认生存期。


    清单 7. 建立联合的安全上下文
                
    GSSContext context         = null;
    GSIGssOutputStream gssout     = null;
    GSIGssInputStream gssin     = null;

    context = manager.createContext(null,
        null,
        credential,
        GSSContext.DEFAULT_LIFETIME);
 


    在对上下文进行实例化之后,在与上下文接收者实际建立上下文之前,上下文的发起者可以选择设置不同的选项,确定所需要的安全上下文特性:

    相互身份验证 ?? 上下文的发起者始终要对接收者进行身份验证。如果发起者请求相互身份验证,那么接收者也可以对发起者进行身份验证。 
    机密性 ?? 请求机密性意味着您请求为上下文方法指定的封装进行加密。 
    完整性 ?? 将要求 wrap 和 getMIC 方法的完整性。在请求完整性时,在调用这些方法时会生成一个密码标记,称为 Message Integrity Code(MIC)。 
    context.requestCredDeleg(false);
    context.requestMutualAuth(true);
 


    在 Server 上,Server 端所需的唯一参数是凭证。必须获得 GSI I/O 流才能来回向客户机发送数据。

    GSSContext context = manager.createContext(credential);

    GSIGssOutputStream gssOut = new GSIGssOutputStream(outStream, context);
    GSIGssInputStream gssIn = new GSIGssInputStream(inStream, context);
 


    在 Client 实例化一个 GSSContext 并指定所需要的上下文选项之后,它就可以真正与 Server 建立安全上下文。每次交互都会使用一个循环来实现:

    调用上下文的 initSecContext 方法 ?? 如果是第一个调用,这个方法就会传入一个空标记符号。否则,它就会传入一个最近一次由 Server 发送给 Client 的标记(这个标记是由 Server 调用 acceptSecContext 而生成的)。 
将 initSecContext 所返回的标记(如果存在)发送给 Server ?? 第一次调用 initSecContext 通常会生成一个标记。最后一次调用可能不会返回标记。 
    检查上下文是否已经建立 ?? 如果没有,Client 就会从 Server 接收另外一个标记,然后开始下一个循环迭代。 
    initSecContext 所返回的任何标记或从 Server 接收到的任何标记都被放入一个字节数组中,客户机和服务器应该将其作为不透明的数据对待,在客户机和服务器之间进行传递,并由 Java GSS-API 方法进行解释。交换消息需要使用一组 GSI I/O 流。


    清单 8. GSI I/O 流
                
    gssout = new GSIGssOutputStream(outStream, context);
    gssin = new GSIGssInputStream(inStream, context);

    byte [] inToken = new byte[0];
    byte [] outToken = null;

    /*
     * Establish a security context
     *     1. Client: sends a secure handshake token 
     *     2. Server: receives handshake token
     *     3. Server: Sends hanshake token back to the client
     *     4. Client: receives handshake. Security context is established.
     */
    while( !context.isEstablished() ) {
        outToken = context.initSecContext(inToken, 0, inToken.length);
        
        if (outToken != null) {
            gssout.writeToken(outToken);
        }

        if (!context.isEstablished()) {
            inToken = gssin.readHandshakeToken();
        }
    }

    System.out.println("Client: Security context Established! ");
 


    另外一方面,服务器上下文循环会执行稍有不同的交互:

    从 Client 接收一个标记 ?? 这个标记是 Client initSecContext 调用的结果。 
    调用上下文的 acceptSecContext 方法,将刚才接收到的标记传递给它。 
    如果 acceptSecContext 返回一个标记,Server 将这个标记发送给 Client,如果上下文尚未建立,就开始下一个循环。 



    清单 9. Server 将标记发送给 Client 并开始下一次循环迭加
                
    while (!context.isEstablished()) 
    {
        token = gssIn.readHandshakeToken();
        token = context.acceptSecContext(token, 0, token.length);
            
        // Send a token to the peer if one was generated by
        // acceptSecContext
        if (token != null) {
            System.out.println("Server: Will send token of size "
                + token.length
                + " from acceptSecContext.");
            gssOut.writeToken(token);
        }
    }
            
    System.out.println("Server: Context Established! ");

    安全交换消息

    一旦在 Client 和 Server 之间建立一个安全上下文之后,它们就可以使用这个上下文来安全地交换消息了。有两种方法可以为安全交换准备消息:wrap 和 getMIC。实际上有两个 wrap 方法(和两个 getMIC 方法),二者之间的区别是输入消息的位置(一个字节数组或一个输入流)和输出消息的输出位置(一个字节数组返回值或一个输出流)的表示:

    wrap 是消息交换使用的主要方法。签名是 byte[] wrap (byte[] inBuf, int offset, int len, MessageProp msgProp),其中 (inBuf) 是要发送的消息, (offset) 是消息开始的位置,(len) 是消息的长度,(MessageProp) 用来说明所需要的 Quality-of-Protection(QOP),并指定是否需要进行加密。SOP 值选择密码完整性和要使用的加密算法(如果请求使用)。对应于各种 QOP 值的算法是由底层机制的提供者指定的。例如,Kerberos V5 使用的值是在 RFC 1964 中定义的。通常指定 0 作为 QOP 的值来请求默认的 QOP。 
    getMIC 用来获取一个包含所提供的消息使用的密码 MIC 的标记。它通常用来与您这一端确认双方都有相同的数据,方法仅仅是为数据传输一个 MIC,而不需要为双方传输数据本身。签名是 byte[] getMIC (byte[] inMsg, int offset, int len, MessageProp msgProp),其中 (inMsg) 是要发送的消息,(offset) 是消息开始的位置,而 (len) 是消息的长度。还传递了一个 (MessageProp),它用来说明所需要的 QOP。通常会指定 0 作为 QOP 的值来请求默认 QOP。 

    清除工作

    清除操作是由 Client 和 Server 来执行的,通过关闭 socket,并释放系统资源和存储在上下文对象中的密码信息,然后使这个上下文无效。


    清单 10. 关闭 socket 并释放系统资源和密码信息
                
    /*
     * If mutual authentication did not take place, then only the
     * client was authenticated to the server. Otherwise, both
     * client and server were authenticated to each other.
     */
    if (context.getMutualAuthState())
        System.out.println("Mutual authentication took place!");

    context.dispose();
    socket.close();


    测试新 GSI 应用程序

    在测试这个新的 GSI 客户机服务器之前,第一个步骤是使用启用了 GSI 的工具(例如 CoG Kit 或 Globus Toolkit)创建一个代理证书。


图 1. 使用 CoG 工具包输出 grid-proxy-init 的调用结果
 


    本文中展示的应用程序可以作为一个 Eclipse Java 项目进行发布。将这个项目导入到您的工作空间中,然后:

    启动 Server 
    运行 Client 
    通过查看 Client 和 Server 的控制台输出结果来检验相互身份验证。 
    输出结果应该如下所示:


    清单 11. 输出结果
                
Client: Connected to server localhost/127.0.0.1
Client: Loaded proxy file: C:\DOCUME~1\Owner\LOCALS~1\Temp\x509up_u_owner
Client Credential: /C=US/L=Raleigh/O=ACME/OU=IT/CN=Vladimir Silva Remaining life time:9349
Client: Security context Established! 
Client is /C=US/L=Raleigh/O=ACME/OU=IT/CN=Vladimir Silva
Server is /C=US/L=Raleigh/O=ACME/OU=IT/CN=Vladimir Silva
Client: Mutual authentication took place!

Server Credential: /C=US/L=Raleigh/O=ACME/OU=IT/CN=Vladimir Silva
Server: Waiting for incoming connection...
Server: Got connection from client /127.0.0.1
Server: Will send token of size 1480 from acceptSecContext.
Server: Will send token of size 75 from acceptSecContext.
Server: Context Established! 
Client is /C=US/L=Raleigh/O=ACME/OU=IT/CN=Vladimir Silva
Server is /C=US/L=Raleigh/O=ACME/OU=IT/CN=Vladimir Silva
Server: Mutual authentication took place!
Server: Closing connection with client /127.0.0.1
Server: Waiting for incoming connection...
 
    如果事情并未预期进行,就请查看下面的故障检修一节的内容以查看问题的线索。

    故障检修

    当您有一个无效或过期的代理时,就会出现一个最简单的问题。这可能是由于 CoG/GT4 工具包安装、证书遭到破坏、I/O 错误等问题而引起的。常见的错误消息如下所示。


    清单 11. 错误消息
                
Got connection from client /127.0.0.1
GSSException: Expired credentials detected
 at org.globus.gsi.gssapi.GlobusGSSManagerImpl
    .createCredential(GlobusGSSManagerImpl.java:118)
 at org.globus.gsi.gssapi.GlobusGSSManagerImpl
    .createCredential(GlobusGSSManagerImpl.java:64)
 ...
 


    无效的安全上下文通常会由于上下文循环中的编码错误而产生 ?? 例如,交换使用 GSS 和 GSI 中的方法。记住 GSI 是对 GSS API 的一个扩展 ?? 因此 GSS 客户机会无法与 GSI 服务器建立一个上下文。


    清单 12. 错误消息
                
Waiting for incoming connection...
Got connection from client /127.0.0.1
unwrap failed. Caused by GSSException: 
    Security context init/accept not yet called or context deleted
 at org.globus.gsi.gssapi.GlobusGSSContextImpl
     .checkContext(GlobusGSSContextImpl.java:1283)
 at org.globus.gsi.gssapi.GlobusGSSContextImpl
     .unwrap(GlobusGSSContextImpl.java:831) 
 
    网格安全基础设施(GSI)是 Java Generic Security Services(GSS-API)的一个扩展。GSI 通过对安全服务提供一致的访问从而在两个应用程序之间安全地交换消息。本文的目标是通过构建一个简单的启用 GSI 的客户机/服务器应用程序来展示网格应用程序的基本身份验证机制。

相关内容
赞助商链接