Lingo (Spring Remoting) : Passing client credentials to the server

时间:2022-10-03 10:04:46

http://www.jroller.com/sjivan/entry/lingo_spring_remoting_passing_client

Lingo (Spring Remoting) : Passing client credentials to the server

Spring Remoting allows you to export a service interface which a remote client then then look up. Once the remote service interface is looked up, the client can work with this reference like its a local object reference. Well, kinda. There are certain limitations that are associated depending on the remoting transport protocol used.

The Spring distribution currently supports RMI, Spring's HTTP invoker, Hessian, Burlap and JAX-RPC. One of the biggest limitations in these transports is that none of them support passing a method argument by reference. This can be a show stopper for a several usecases such as callbacks. RMI as a transport protocol does support pass-by-reference however the Spring RMI Remoting Strategy does not support this yet. Regardless, RMI is a heavy weight protocol so I'm not a big fan of it.

This brings us to Lingo, an implementation of Spring Remoting over JMS which does support remote callbacks - the contract being that such arguments must either implement java.rmi.Remote or java.util.EventListener. Lingo also supports asynchronous one-way method invocations.

A common requirement in a client-server communication protocol is the propagation of the client's security context to the server during a remote method invocation. One could pass this information as an argument to every method call but that's a really really ugly way to go about it. The right way to do this would be to make such information available in as a ThreadLocal context variable on the server. Lingo does not support passing of client credentials to the server out of the box.

I examined the Lingo code found that adding support for this functionality was actually quite easy. I'll cover most of the details here. You can download the entire source for this as well.

Lingo uses Marshaller's on the client and server side which basically negotiates the underlying client-server JMS communication.

Lingo Marshaller's have a hook for adding and extracting custom JMS header messages. So I created a custom client Marshaller which adds the users Principal (user name) to the message header and a custom server Marshaller which reads this information and makes it available as a ThreadLocal context variable.

Here's the code for an Acegi based client marshaller.

public class AcegiClientMarshaller extends DefaultMarshaller {
protected void appendMessageHeaders(Message message, Requestor requestor,
LingoInvocation invocation) throws JMSException {
SecurityContext sc = SecurityContextHolder.getContext();
Authentication auth = sc.getAuthentication();
  //if you're using a custom acegi principal, update this code accordingly
String userName = null;
if (auth != null) {
Object principal = auth.getPrincipal();
if (principal instanceof net.sf.acegisecurity.UserDetails) {
userName = ((net.sf.acegisecurity.UserDetails) principal).getUsername();
} else if (principal instanceof String) {
userName = (String) principal;
} else {
throw new IllegalArgumentException("Invalid principal " + principal);
}
}  
//add user name info to the message header
message.setStringProperty(Constants.JMS_CLIENT_ID_PROPERTY, userName);
}
}

As you can see, I grab the principal (user name in our case) of the authenticated user and add it as a custom header property. (I have another simple implementation of a client Marshaller in the source distribution). Even if you're not using Acegi, you can basically follow a similar logic as above to add the client context info as a custom header message.

Now we need to create a server (un)Marshaller which extracts this information and puts it in a ThreadLocal context variable.

public class ServerMarshaller extends DefaultMarshaller {
 
private static final Log log = LogFactory.getLog(DefaultMarshaller.class);  
protected void handleInvocationHeaders(Message message) {
try {
String userName = message.getStringProperty(Constants.JMS_CLIENT_ID_PROPERTY);
ClientContextHolder.setContext(new ClientContextImpl(userName));
}
catch (JMSException e) {
log.error(e.getMessage());
throw new RuntimeException("An Unexpected error occured " + e.getMessage(), e);
}
}
}

Note that in this example we are just passing the user-name String as client context data, however you're not limited to using String's. You can use message.setObjectProperty(..) and message.getObjectProperty() and pass richer context information. The Object would need to be Serializable though.

Now the final step of hooking up the pieces in the client and server Spring ApplicationContext files.

The client configuration file looks like

<?xml version="1.0" encoding="UTF-8"?> 

<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans> <!-- client side --> <bean id="clientOne" class="org.logicblaze.lingo.jms.JmsProxyFactoryBean"> <property name="serviceInterface" value="org.sanjiv.lingo.test.ExampleService"/> <property name="connectionFactory" ref="jmsFactory"/> <property name="destination" ref="exampleDestination"/> <property name="marshaller" ref="acegiClientMarshaller"/> </bean>
<bean id="acegiClientMarshaller" class="org.sanjiv.lingo.client.AcegiClientMarshaller"/>
<!-- JMS ConnectionFactory to use --> <bean id="jmsFactory" class="org.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="vm://localhost"/> </bean></beans>

And the server configuration file looks like

<?xml version="1.0" encoding="UTF-8"?> 

<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans> <!-- the server side --> <bean id="serverImpl" class="org.sanjiv.lingo.test.ExampleServiceImpl" singleton="true"/>
<bean id="server" class="org.logicblaze.lingo.jms.JmsServiceExporter"> <property name="service" ref="serverImpl"/> <property name="serviceInterface" value="org.sanjiv.lingo.test.ExampleService"/> <property name="connectionFactory" ref="jmsFactory"/> <property name="destination" ref="exampleDestination"/> <property name="marshaller" ref="serverMarshaller"/> </bean>
<bean id="serverMarshaller" class="org.sanjiv.lingo.server.ServerMarshaller"/>
<!-- JMS ConnectionFactory to use --> <bean id="jmsFactory" class="org.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="vm://localhost"/> </bean>
<bean id="exampleDestination" class="org.activemq.message.ActiveMQQueue"> <constructor-arg index="0" value="test.org.sanjiv.lingo.example"/> </bean></beans>

During execution of a remote method, you can extract the client context info by looking it up in the ThreadLocal context variable as illustrated by this example :

public class ExampleServiceImpl implements ExampleService {
 
public String whoAmI() {
String userName = ClientContextHolder.getUserName();
return userName != null ? userName : "annonymous";
}
}

The source for the ClientContextHolder class which manages the ThreadLocal variables is :

public class ClientContextHolder {
 
private static InheritableThreadLocal contextHolder = new InheritableThreadLocal();
 
/**
* Associates a new ClientContext with the current thread of execution.
*/ public static void setContext(ClientContext context) {
contextHolder.set(context);
}
 
/**
* Obtains the <code>ClientContext</code> associated with the current thread of execution.
*
* @return the current ClientContext
*/ public static ClientContext getContext() {
if (contextHolder.get() == null) {
contextHolder.set(new ClientContextImpl());
}
 
return (ClientContext) contextHolder.get();
}  
public static String getUserName() {
return ((ClientContext) contextHolder.get()).getUserName();
}
}

And there you have it. You can download the complete source for this here and check out the test case to get a better feel of how all the pieces hang together. JRoller doesn't allow uploading .zip files so I've uploaded the sample as a .jar file instead. The source distribution has a Maven 1.x project file. To build and run the tests, run "maven jar".

I like Lingo, it's simple and does the job well. I only wish there was a more active and responsive mailing list.

Feedback welcome.

Update 5/01/06 : I recently required to propagate the client's locale to the server as well and used a similar approach as above. Basically I created a composite ClientMarshaller and ServerMarshaller and added the Client and Server marshallers for propagating the principal (as shown above) and the client's locale declaratively in the Spring applicaiton context file. Here's the relevant code snippet : In LocaleClientMarshaller :

Locale locale = LocaleContextHolder.getLocale();
message.setStringProperty(OptimizerConstants.JMS_LOCALE_LANGUAGE_PROPERTY, locale.getLanguage());
message.setStringProperty(OptimizerConstants.JMS_LOCALE_COUNTRY_PROPERTY, locale.getCountry());

and in LocaleServerMarshaller

String language = message.getStringProperty(OptimizerConstants.JMS_LOCALE_LANGUAGE_PROPERTY);
String country = message.getStringProperty(OptimizerConstants.JMS_LOCALE_COUNTRY_PROPERTY);
if (language != null){
country = country == null ? "" : country;
Locale locale = new Locale(language, country);
LocaleContextHolder.setLocale(locale);
}

Note that I had previously mentioned that in addition to custom String headers, you could add any object that is serializable. Unfortunately this does not work.

org.activemq.message.ActiveMqMessage.setObjectMessage() only supports String and wrappers of primitive data type. And this behavior is specified in the JMS spec.

The setObjectProperty method accepts values of class Boolean, Byte, Short, Integer, Long, Float, Double and String. An attempt to use any other class must throw a JMSException. As a result I cannot pass the java.util.Locale object directly but instead pass the country and language as separate header properties.

Update 07/21/06 : I have commited this functionality into the Lingo codebase. This feature will be available in the Lingo 1.2 release which is due any day now.

随机推荐

  1. SQL知识整理一:触发器、存储过程、表变量、临时表

    触发器 触发器的基础知识 create trigger tr_name on table/view {for | after | instead of } [update][,][insert][,] ...

  2. Ubuntu 16&period;04 64位安装insight 6&period;8

    1. apt-get install insight已经不管用. 2. 编译源码死都有问题. 3. 拜拜,用KDBG.

  3. JS小记

    好记性不如烂笔头. 1.document.ElementFromPoint:根据坐标获得元素 2.有时候要操作DOM页面,但是得不到预期结果,很可能是因为页面还没加载完成,在console控制台可以看 ...

  4. iOS&colon;堆&lpar;heap&rpar;和栈&lpar;stack&rpar;的理解

    Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存是由你释放的,即release 栈由编译器管理自动释放的,在方法中(函数体)定义的变量通常是在栈内,因此如果你的变量要跨函数的话就 ...

  5. iis express 启动多个网站

      iis express一次只能运行一个网站,  执行iisexpress 不加参数. 将执行配置文件中的第一个网站. iis express一次只能运行一个 应用程序池. 可以使用这个特点实现一次 ...

  6. &dollar;&lpar;function&lpar;&rpar;&lbrace;&rcub;&rpar;与&dollar;&lpar;document&rpar;&period;ready&lpar;function&lpar;&rpar;&lbrace;&rcub;&rpar;

    $(function(){ //jq ready()的简写 }); $(document).ready(function(){ // }); 或者: $().ready(function(){ //j ...

  7. 在远程系统上开发 SharePoint 应用程序

    适用范围: apps for SharePoint | Office 365 | SharePoint Foundation 2013 | SharePoint Server 2013 使用远程安装的 ...

  8. CSS 去掉IE10中type&equals;password中的眼睛图标

    在IE10中,input[type=password],如果我们输入内容后,内容会变成圆点,这与以前一样,但后面多了一个眼睛图标,我们鼠标移过去按下会出现输入内容.有时我们想去掉这功能.IE10允许我 ...

  9. 原生js实现轮播

    1,效果图 https://ga20.github.io/slider/ 2,原理图 分三步: 将视口元素设置overflow:hidden 将其图片子元素设置float:left 让橙色的框(包裹) ...

  10. 使用Marshal&period;Copy把Txt行数据转为Struct类型值

    添加重要的命名空间: using System.Runtime.InteropServices; 先建立结构相同(char长度相同)的Struct类型用于转换: [StructLayout(Layou ...