1. 安全Ice程序的设计模式

使用Ice设计安全应用程序的方法有很多。适用于传统Web应用程序的大多数原则,可以转换为Ice应用程序(例如,基于会话的身份验证或基于令牌的身份验证)。与使用非持久连接的HTTP不同,Ice也支持安全的持久连接。

我们将在这篇文章中探讨一些构建安全Ice应用程序的常用方法。

1.1. 基于令牌的安全

基于令牌的安全在Web应用程序中非常常见:客户端向服务器发送凭证,服务器依次使用令牌进行回复。该令牌对于客户端是不透明的,它包含服务器用来检查客户端是否有权访问资源的信息。这种方法的优点是它不需要服务器维护客户端的状态。生成的令牌可以用于任何服务器来验证访问资源的权限。

下面是一个Slice接口,它使用显式标记,来验证用户添加或删除购物车中物品的权限:

interface SecurityTokenFactory
{
    string createSecurityToken(string userId, string password);
}

exception PermissionDeniedException
{
    string reason;
}

interface CartManager
{ 
    void addItem(string itemId, int quantity, string token) throws PermissionDeniedException;
    void removeItem(string itemId, string token) throws PermissionDeniedException;
}

客户端应用程序先通过调用安全令牌工厂的createSecurityToken从服务器请求一个令牌。然后,使用返回的令牌调用addItemremoveItem来修改用户的购物车。在这两个方法中会实现检查令牌,以确保它是有效的。如果服务器中的令牌检查失败,则会抛出异常。

本示例中的身份验证基于用户标识和密码,但也可能基于任何其他类型的证书(证书通常会以字节序列的形式提供)。如果客户端使用X.509证书与服务器建立SSL/TLS连接,则身份验证可以使用客户端证书中包含的标识(专有名称)。

1.1.1. 服务端实现

CartManager的Java实现示例,如下所示:

class SecurityTokenFactoryImpl implements Demo.SecurityTokenFactory
{
    public String createSecurityToken(String userId, String password, com.zeroc.Ice.Current current)
    {       
        return generateToken(userId, password); // 检查用户ID和密码,然后生成令牌
    }
}

class CartManagerImpl implements Demo.CartManager
{
    public void addItem(String itemId, int quantity, String token, com.zeroc.Ice.Current current) throws PermissionDeniedException
    {
        checkToken(token); // 确保给定的令牌可用

        String userId = getUserIdFromToken(token);
        java.util.Map<String, Integer> cart = _carts.get(userId);
        if(cart == null)
        {
            cart = new java.util.HashMap<String, Integer>()
            _carts.put(userId, cart);
        }
        cart.put(itemId, quantity);
    }

    public void removeItem(String itemId, int quantity, String token, com.zeroc.Ice.Current current) throws PermissionDeniedException       
    {
        checkToken(token); // 确保给定的令牌可用

        java.util.Map<String, Integer> cart = _carts.get(getUserIdFromToken(token));
        if(cart != null)
        {
            cart.remove(itemId);
        }
    }

    // 用户ID到购物车的MAP映射,其中购物车是物品ID到数量的MAP映射
    private java.util.Map<String, java.util.Map<String, Integer>> _carts;
}

我们实现了Slice生成的接口Demo.SecurityTokenFactoryDemo.CartManagercreateSessionToken通过证书调用generateToken方法。如果用户证书有效,则此方法返回字符串令牌。令牌是各种数据的加密表示,例如:用户ID和限制令牌使用寿命的时间戳。服务器稍后通过解密令牌来检索这些数据。

展示令牌生成超出了本文的范围。令牌生成和验证通常由第三方库处理。

addItemremoveItem方法用于修改用户的购物车。令牌被明确地提供给每个调用,并且服务器调用checkToken以确保其有效性。同样,这里没有显示checkToken的实现,但通常这种方法用于解密令牌数据,验证它的有效性并检查用户的权限。由于令牌包含用户标识符,我们可以从令牌中提取它,用以检索用户的购物车并添加或删除项目。

下面是演示如何创建一个Ice communicator,Ice对象适配器,然后,注册两个服务的主要方法:

public static void main(String args[])
{
    com.zeroc.Ice.Communicator communicator = com.zeroc.Ice.Util.initialize(args);
    com.zeroc.Ice.ObjectAdapter adapter = communicator.createObjectAdapterWithEndpoints("CartAdapter", "ssl -p 10000");
    adapter.add(new SecurityTokenFactoryImpl(), new com.zeroc.Ice.Identity("SecurityTokenFactory", ""));
    adapter.add(new CartManagerImpl(), new com.zeroc.Ice.Identity("CartManager", ""));
    communicator.waitForShutdown();
}

SSL/TLS传输的其他配置(例如服务器证书)需要通过Ice配置文件提供。上面定义的端点允许客户端通过端口10000上的SSL/TLS传输访问服务器。由于不指定-h,服务器将监听主机上可用的所有网络接口。

1.1.2. 客户端实现

用Ice管理器服务很简单。

下面是一个简单的客户端,该客户端获取安全令牌,然后,通过调用CartManager的Ice对象,来添加和删除购物车中的物品:

public static void main(String args[])
{
    com.zeroc.Ice.Communicator com = com.zeroc.Ice.Util.initialize(args);
    Demo.SecurityTokenFactoryPrx factory = Demo.CartManagerPrx.uncheckedCast(com.stringToProxy("SecurityTokenFactory:ssl -p 10000 -h 127.0.0.1"));
    Demo.CartManagerPrx cartManager = Demo.CartManagerPrx.uncheckedCast(com.stringToProxy("CartManager:ssl -p 10000 -h 127.0.0.1"));

    try
    {
        String token = factory.createSecurityToken("foo", "dummy");

        cartManager.addItem("item1", 2, token);
        cartManager.removeItem("item1", token);
    }
    catch(PermissionDeniedException ex)
    {
       // 处理权限被拒
    }
    catch(com.zeroc.Ice.LocalException ex)
    {
       // 处理通讯失败
    }

    com.destroy();
}

客户端使用Ice对象标识CartManager和端点ssl -p 10000 -h 127.0.0.1创建一个代理,我们这里假设服务器和客户端在同一台机器上运行;如果客户端和服务器位于不同的主机上,我们将使用带-h选项服务器的IP地址或主机名。我们通常还会使用此字符串化的代理配置属性,而不是将其硬编码在客户端的源码中。

1.1.3. 改进

使用Ice上下文

CartManager中明确地传递令牌很麻烦,安全上下文不是应用程序的远程API应该关心的东西。更好的方法是,使用请求上下文在每个Slice方法签名中,没有明确token字符串参数的情况下,向每个调用发送安全令牌:

interface CartManager
{
    void addItem(string itemId, int quantity) throws PermissionDeniedException;
    void removeItem(string itemId) throws PermissionDeniedException;
}

客户端使用隐式上下文来传递令牌:

String token = cartManager.createSecurityToken("foo", "dummy");

// Setup an implicit context to provide the security token
java.util.Map<String, String> context = new java.util.HashMap<String, String>();
context.push("securityToken", token);
communicator.getImplicitContext().setContext(context);

cartManager.addItem("foo", "item1", 2);
cartManager.removeItem("foo", "item1");

Ice communicator也需要配置为使用Ice.ImplicitContext属性的隐式上下文。

隐式上下文的替代方案是在代理上设置上下文:

java.util.Map<String, String> context = new java.util.HashMap<String, String>();
context.push("securityToken", token);
cartManager = cartManager.ice_context(context);

现在,在这个新的CartManager代理上进行的调用将始终嵌入配置的上下文。如果你仅使用少量代理,此解决方案运作良好。

在服务器端,CartManager的服务,可以使用Ice的当前参数来检索上下文:

public void addItem(String userId, String itemId, int quantity, com.zeroc.Ice.Current current) throws PermissionDeniedException
{
    checkToken(current.ctx.get("securityToken")); // Ensures the given token is valid and generated for the given user
    ...
}

使用调度拦截器

我们可以做的另一个改进是从CartManager接口的实现中删除安全检查。我们可以使用Ice调度拦截器,来拦截CartManagerIce对象上的请求。令牌上的安全检查将不再在addItemremoveItem的实现中显式完成。相反,拦截器的实现会处理它:

class InterceptorImpl extends com.zeroc.Ice.DispatchInterceptor
{
    public InterceptorImpl(CartManagerImpl cartManager)
    {
        _cartManager = cartManager;
    }

    public java.util.concurrent.CompletionStage<com.zeroc.Ice.OutputStream> dispatch(Request request)
    {
        checkToken(request.getCurrent().ctx.get("securityToken"));
        return _cartManager.ice_dispatch(request);
    }

    private CartManagerImpl _cartManager;
}

1.2. 基于连接的安全

Ice支持与基于TCP传输(tcp,ssl,ws,wss)的持续连接,因此我们也可以通过与传入请求相关的连接,来跟踪经过身份验证的客户端。通过这种方法,客户端使用用户名/密码等凭据向服务器进行身份验证。然后,服务器检查这些凭据,并将当前网络连接与它们关联。当客户端向服务器发送请求时,服务器可以检查用于发送请求的连接,并查看连接是否已知。根据相关的凭据,它可以拒绝或接受请求。

我们来更新前面例子中的Slice接口,来使用连接代替令牌:

exception PermissionDeniedException
{
    string reason;
}

interface CartManager
{
    void login(string userId, string password) throws PermissionDeniedException;
    void logout();

    void addItem(string itemId, int quantity) throws PermissionDeniedException;
    void removeItem(string itemId) throws PermissionDeniedException;
}

1.2.1. 服务端实现

CartManager的Java实现示例,如下所示:

class CartManagerImpl implements Demo.CartManager
{
    synchronized public void login(String userId, String password, com.zeroc.Ice.Current current)
    {
        checkCredentials(userId, password);
        _connections.put(current.con, userId);
    }

    synchronized public void logout(com.zeroc.Ice.Current current)
    {
        _connections.remove(current.con);
    }

    synchronized public void addItem(String itemId, int quantity, String token, com.zeroc.Ice.Current current) throws PermissionDeniedException
    {
        String userId = checkConnectionAndGetUserId(current.con); // Ensures the connection is authenticated
        java.util.Map<String, Integer> cart = _carts.get(userId);
        if(cart == null)
        {
            cart = new java.util.HashMap<String, Integer>()
            _carts.put(userId, cart);
        }
        cart.put(itemId, quantity);
    }

    synchronized public void removeItem(String itemId, int quantity, String token, com.zeroc.Ice.Current current) throws PermissionDeniedException       
    {
        String userId = checkConnectionAndGetUserId(current.con); // Ensures the connection is authenticated
        java.util.Map<String, Integer> cart = _carts.get(userId);
        if(cart != null)
        {
            cart.remove(itemId);
        }
    }

    private String checkConnectionAndGetUserId(com.zeroc.Ice.Connection connection) throws PermissionDeniedException
    {
        String userId = _connections.get(connection);
        if(userId == null)
        {
            throw new PermissionDeniedException("unknown connection");
        }
        return userId;
    }

    // A map of userId -> Cart where Cart is  a map of item ID -> Quantity
    private java.util.Map<String, java.util.Map<String, Integer>> _carts;

    // A map of Ice.Connection to user ID
    private java.util.Map<com.zeroc.Ice.Connection, String> _connections;
}

正如你所看到的,与基于令牌的方法不同;现在,服务器需要使用新的用户ID连接映射来保持客户端状态。当客户端调用login方法时,此映射将被填充。该实现通过调用checkCredentials来验证凭证。我们在这里不展示这个方法的实现,但通常这个方法会执行数据库查找来检查密码。一旦证书检查完成,实现将从Ice当前对象获得的连接添加到Map,并将用户ID与连接相关联。 logout方法的实现很简单:它只是删除映射条目。

addItemremoveItem方法检索连接,并获取与连接关联的用户标识以检索用户的购物车。

1.2.2. 客户端实现

下面是客户端示例,该客户端使用上面定义的CartManager接口:

public static void main(String args[])
{
    com.zeroc.Ice.Communicator com = com.zeroc.Ice.Util.initialize(args);
    Demo.CartManagerPrx cartManager = Demo.CartManagerPrx.uncheckedCast(com.stringToProxy("CartManager:ssl -p 10000 -h 127.0.0.1"));
    try
    {
        cartManager.login("foo", "dummy");
        cartManager.addItem("item1", 2, token);
        cartManager.removeItem("item1", token);
        cartManager.logout();
    }
    catch(PermissionDeniedException ex)
    {
       // Handle permission denied
    }
    catch(com.zeroc.Ice.LocalException ex)
    {
       // Handle communication failure
    }
    com.destroy();
}

采用这种方法,客户端与服务器的连接保持打开状态至关重要。如果连接已关闭,并且创建了新的连接,则客户端向服务器发送的进一步请求将失败,除非客户端调用login对新连接进行身份验证。

这种基于连接的方法需要对Ice连接管理有一个很好的理解。例如,在上面的客户端中,CartManager代理对象上的调用将用相同底层的Ice连接,因为它们是在一个接一个的在同一个代理上进行的。如果addItemremoveItem调用之间存在60秒的延迟(使用Ice的默认配置),则Ice活动连接管理将关闭连接。然后,removeItem调用将失败,因为它的调用在一个新的未认证的连接上。

1.2.3. 改进

使用ACM心跳

Ice提供配置属性,来保持Ice连接的开放和活跃,并避免空闲连接的自动关闭。

例如,你可以为客户端和服务端设置以下属性:

# Client
Ice.ACM.Close=0      # CloseOff
Ice.ACM.Heartbeat=3  # HeartbeatAlways
Ice.ACM.Timeout=30

# Server
Ice.ACM.Close=4      # CloseOnIdleForceful
Ice.ACM.Heartbeat=0  # HeartbeatOff
Ice.ACM.Timeout=30

有了这个配置:

  • 客户端在空闲时不会关闭连接,而是每隔15秒发送一次心跳消息来为连接保活,

  • 服务端将强制关闭连接,如果它在30秒内没有收到连接的活动。

有关这些配置属性的更多信息,请参阅Ice活动连接管理

使用连接回调

Ice还提供了一个回调机制,来通知应用程序的连接关闭。这样的关闭通知对于防止泄漏非常有用。在上面显示的服务器实现中,如果客户端没有调用logout(因为它崩溃或网络连接被丢弃),客户端连接的条目将不会从_connectionsMap中删除。这会在服务器中造成泄漏。

为防止这种泄漏,我们可以修改login方法,以确保在连接关闭时删除条目:

class CartManagerImpl implements Demo.CartManager
{
    synchronized public void login(String userId, String password, com.zeroc.Ice.Current current)
    {
        checkCredentials(userId, password);
        _connections.put(current.con, userId);
        current.con.setCloseCallback(_callback);
    }

    ...

    private com.zeroc.Ice.ConnectionCallback _callback = new com.zeroc.Ice.CloseCallback()
    {
        public void closed(com.zeroc.Ice.Connection con)
        {
            synchronized(CartManagerImpl.this)
            {
                _connections.remove(con);
            }
        }
    }
}

基于会话的安全性

在这种模式下,客户端与服务器建立会话。认证信息由客户端在会话建立时发送。如果认证成功,则服务器创建会话,并向客户端返回会话标识符。然后,客户端使用会话标识符在服务器应用程序上执行进一步的请求。服务器将会话信息保存在内存或数据库中。除了用户凭证之外,会话还存储特定应用程序的额外信息。例如,在购物车类型的应用程序中,会话可以存储用户的购物车。

以下是我们如何为这样的应用程序写入Slice接口:

module Demo
{

interface Session
{
    void addItem(string itemId, int quantity);
    void removeItem(string itemId, int quantity);
    void destroy();
}

interface SessionFactory
{
     Session* createSession(string username, string password);
}

}

Ice的面向对象特性使得理解客户端和服务器之间的交互变得容易。正如你所看到的,传输会话标识符也没有增加混乱。 会话标识符被createSession调用返回的会话代理隐式封装起来。

我们来看看这些接口如何实现和使用。

服务端实现

以下是会话接口的示例实现:

class SessionImpl implements Demo.Session
{
    public SessionImpl(String userId)
    {
       _userId = userId;
    }

    synchronized public void addItem(String itemId, int quantity, com.zeroc.Ice.Current current)
    {
        _cart.add(itemId, quantity);
    }

    synchronized public void removeItem(String itemId, com.zeroc.Ice.Current current)
    {
        _cart.remove(itemId);
    }

    public void destroy(com.zeroc.Ice.Current current)
    {
        current.adapter.remove(current.id);
    }

    private String _userId;
    private java.util.Map<String, Integer> _cart = new java.util.Map<String, Integer>();
}

会话信息由SessionImpl对象的实例存储在内存中。每个客户端会话有一个这样对象的实例。Ice客户端调用addItemremoveItem方法来添加或删除购物车中的物品。客户端调用destroy方法来通知服务器不再对会话感兴趣。该方法的实现,从Ice对象适配器中取消注册Ice对象;此后,Ice不再接受对此会话对象的请求。

current.id是Ice对象的Ice对象标识。让我们来看看如何创建会话Ice对象,来更好地理解这个标识是什么:

class SessionFactoryImpl implements Demo.SessionFactory
{
    public Demo.SessionPrx createSession(String userId, String password, com.zeroc.Ice.Current current)
    {
        checkUserNameAndPassword(userId, password); // Perform authentication check on username/password
        return Demo.SessionPrx.uncheckedCast(current.adapter.addWithUUID(new SessionImpl(userId)));
    }
}

createSession方法的实现检查客户端提供的凭据。在这个例子中,客户端提供了一个用户标识符和密码。同样,它也可以提供另一种类型的证书,例如X.509证书。

然后,此方法将创建一个新的SessionImpl服务对象,并通过Ice对象适配器来注册这个服务,使用UUID作为对象标识。这个Ice对象的代理被传回给客户端。此代理嵌入了Ice对象标识和端点信息,允许客户端远程访问此Ice会话的对象。

该代理实际上是传统Web应用程序将传回客户端的会话标识符。用于Ice对象身份UUID的随机性,确保了其他客户端不能轻易猜测到它,保证客户端和服务器之间的安全通信,网络连接上的窃听也无法发现代理标识。

服务器的main方法类似于我们用于基于令牌模型的方法:

public static void main(String args[])
{
    com.zeroc.Ice.Communicator communicator = com.zeroc.Ice.Util.initialize(args);
    com.zeroc.Ice.ObjectAdapter adapter = communicator.createObjectAdapterWithEndpoints("SessionAdapter", "ssl -p 10000");
    adapter.add(new SessionFactoryImpl(), new com.zeroc.Ice.Identity("SessionFactory", ""));
    communicator.waitForShutdown();
}

客户端实现

下面是一个简单的客户端,创建一个会话,并更新会话的购物车。

public static void main(String args[])
{
    com.zeroc.Ice.Communicator communicator = com.zeroc.Ice.Util.initialize(args);
    com.zeroc.Ice.ObjectPrx proxy = communicator.stringToProxy("SessionFactory:ssl -p 10000 -h 127.0.0.1");
    Demo.SessionFactoryPrx factory = Demo.SessionFactoryPrx.uncheckedCast(proxy);

    try
    {
        Demo.SessionPrx session = factory.createSession("foo", "dummy");
        session.addItem("item1", 2);
        session.removeItem("item1");
        session.destroy();
    }
    catch(com.zeroc.Ice.LocalException ex)
    {
       // Handle communication failure
    }

    communicator.destroy();
}

客户端为会话工厂的Ice对象创建代理,并调用createSession来提供凭据和创建会话。然后,客户端可以使用会话代理来添加和删除购物车中的物品。最后,它会调用会话的destroy来确保服务器为会话分配的资源被销毁。

1.2.4. 改进

会话生命周期

在上面的服务器实现中,我们依靠客户端调用会话destroy()方法来释放分配给会话的资源。该实现从Ice对象适配器中删除服务。如果不调用destroy(),则服务将保持被Ice对象适配器引用,这将导致对象保留在内存中,直到服务器关闭时,Ice对象适配器才会被销毁。

由于不能保证客户端会调用destroy()(客户端可能会崩溃,网络连接可能会丢失,...),所以当我们检测到客户端放弃了会话时,我们需要添加一个机制来删除会话调用destroy()。Ice服务器如何检测到这个?有几个解决方案:

  • 服务器要求客户端定期显式ping或保持活动会话。服务器跟踪来自客户端的ping,收集一段时间内没有收到ping消息的会话。这可以通过在客户端设置一个定时器来调用代理上的ice_ping()方法,并覆盖Demo.SessionImpl类中的ice_ping服务方法来实现。服务器会定期检查哪些会话,可以根据最后一次收到ping的情况收回。

  • 应用程序可以将会话绑定到Ice连接(表示客户端和服务器之间的持久网络连接),并依靠连接关闭回调来销毁会话。这种方法要求客户端使用单个连接在服务器上调用。除非客户明确请求使用单独的连接,或使用不同的连接超时或端点。

将会话绑定到客户端连接是最简单的解决方案。以下是我们如何修改会话工厂,通过设置连接回调来清理连接关闭时的会话:

class SessionFactoryImpl implements Demo.SessionFactory
{
    public Demo.SessionPrx createSession(String userId, String password, com.zeroc.Ice.Current current)
    {
        checkUserNameAndPassword(userId, password); // Perform authentication check on username/password

        Demo.SessionPrx sessionPrx = Demo.SessionPrx.uncheckedCast(current.adapter.addWithUUID(new SessionImpl(userId)));
        current.con.setCloseCallback(new com.zeroc.Ice.CloseCallback()
        {
            public void closed(com.zeroc.Ice.Connection con)
            {
                current.adapter.remove(sessionPrx.ice_getIdentity());
            }
        });
        return sessionPrx;
    }
}

还有一个额外的考虑:closed的回调被调用的速度有多快。如果操作系统的TCP/IP堆栈未及时检测到连接故障,则可能在连接失败几个小时后,调用回调closed方法。当客户端与服务器的连接经过多个路由器,并且由于连接路径上预期的故障而导致连接失败时,通常情况就是如此。为了使服务器及时释放会话资源,快速检测到连接丢失,可以在连接上启用保活。在Ice手册的活动连接管理部分,提供了有关该主题的更多信息。

增加安全性

上面的方法依赖于,其他客户端不能发现,用于会话的Ice对象身份的UUID。在上面的改进中,我们看到将会话绑定到连接是在客户端消失时,清除会话资源的简单方法。为了提供更高的安全性,我们还可以依靠Ice连接对象,来检查会话上的调用是否仅由创建会话的客户端执行。这需要跟踪会话中的Ice连接对象,并在每个调用中检查这个连接:

class SessionImpl implements Demo.Session
{
    public SessionImpl(String userId, com.zeroc.Ice.Connection connection)
    {
       _userId = userId;
       _connection = connection;
    }
    synchronized public void addItem(String itemId, int quantity, com.zeroc.Ice.Current current)
    {
        checkConnection(current);
        _cart.add(itemId, quantity);
    }

    synchronized public void removeItem(String itemId, com.zeroc.Ice.Current current)
    {
        checkConnection(current);
        _cart.remove(itemId);
    }

    void destroy(com.zeroc.Ice.Current current)
    {
        checkConnection(current);
        current.adapter.remove(current.id);
    }

    private void checkConnection(com.zeroc.Ice.Current current)
    {
        //
        // If the connection doesn't match the one used to create the session, we return
        // ObjectNotExistException to the client.
        //
        if(current.con != _connection)
        {
            throw new com.zeroc.Ice.ObjectNotExistException(current.id, current.facet, current.operation);
        }
   }

    private String _userId;
    private com.zeroc.Ice.Connection _connection;
    private java.util.Map<String, Integer> _cart = new java.util.Map<String, Integer>();
}

使用调度拦截器

就像基于令牌的模型一样,我们可以进一步改进上面的会话实现,并将checkConnection调用移动到调度拦截器:

class InterceptorImpl extends com.zeroc.Ice.DispatchInterceptor
{
    InterceptorImpl(SessionImpl session)
    {
        _session = session;
    }

    public java.util.concurrent.CompletionStage<com.zeroc.Ice.OutputStream> dispatch(com.zeroc.Ice.Request request)
    {
        _session.checkConnection(current);
        return _session.ice_dispatch(request);
    }

    private SessionImpl _session;
}
Copyright © github.com/weiofcn 2017 all right reserved,powered by GitbookLast modified time: 2017-12-27 02:20:43

results matching ""

    No results matching ""