顯示具有 Spring 標籤的文章。 顯示所有文章
顯示具有 Spring 標籤的文章。 顯示所有文章

2013年2月1日 星期五

The Lost Part of Multi-tenancy in Hibernate

在Hibernate官方網站Chapter 16. Multi-tenancy中提到三種實現multi-tenancy的方法,分別是:
  1. Separate database
  2. Separate schema
  3. Partitioned (discriminator) data
前兩項在Hibernate 4已經有支援,但是最後一項discriminator很遺憾官方有說明如下:
Correlates to the partitioned (discriminator) approach. It is an error to attempt to open a session without a tenant identifier using this strategy. This strategy is not yet implemented in Hibernate as of 4.0 and 4.1. Its support is planned for 5.0.
所以在Hibernate 4之前我們得自己實現這樣的approach,深入思考一下,如果可以依照登入的使用者取得tenant ID,然後這個使用者產生的所有資料都自動填入tenant ID,而讀取資料的任何query都自動加上tenant ID這個condition,那麼不就完成了我們所想要的multi-tenancy機制嗎?所以我們將三種技術結合就可以達成這個目的:
  1. Spring Security
  2. Hibernate Filter
  3. Hibernate Interceptor
可以參考:
目前我們已經在部分系統中成功地實現這樣的概念。

Update Specific Columns Automatically by Hibernate Interceptor

如果我們想要在某些時機點固定更新某些欄位,但是又不想讓這種邏輯被寫死在DAO裡面,就可以利用Hibernate Interceptor這個功能完成這件事情。

舉例來說,如果我們想要記錄每筆資料是誰在什麼時間產生,又是誰在什麼時間做了修改,如果在每個DAO中去處理這件事情就需要花相當的力氣,而Hibernate Interceptor會是個不錯的選擇。

以下我們就實作這樣的例子,程式分為三大部分:
  1. 一組攔截generic property的Hibernate Interceptor API
  2. 記錄誰在什麼時間更新資料的API
  3. 設定與程式套用範例
Part I
EmbeddablePropertyInterceptor
package com.gss.gmo.cao.hibernate.interceptor;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;

import lombok.EqualsAndHashCode;
import lombok.SneakyThrows;
import lombok.ToString;

import org.hibernate.EmptyInterceptor;
import org.hibernate.type.Type;
import org.springframework.util.ReflectionUtils;

/**
 * @author linus_chien
 *
 * @param <Embeddable>
 */
@EqualsAndHashCode(of = "embeddableClass", callSuper = false)
@ToString(of = "embeddableClass")
public abstract class EmbeddablePropertyInterceptor<Embeddable> extends EmptyInterceptor {

    private static final long serialVersionUID = 1L;

    private Class<Embeddable> embeddableClass;

    @SuppressWarnings("unchecked")
    public EmbeddablePropertyInterceptor() {
        java.lang.reflect.Type t = getClass().getGenericSuperclass();
        ParameterizedType pt = (ParameterizedType) t;
        embeddableClass = (Class<Embeddable>) pt.getActualTypeArguments()[0];
    }

    /**
     * @return new Embeddable instance.
     */
    @SneakyThrows
    private Embeddable newInstance() {
        return embeddableClass.newInstance();
    }

    @SuppressWarnings("unchecked")
    private Embeddable findEmbeddableProperty(Object entity, Object[] state, String[] propertyNames) {
        for (int i = 0; i < propertyNames.length; i++) {
            String propertyName = propertyNames[i];
            Field auditInfoField = ReflectionUtils.findField(entity.getClass(), propertyName, embeddableClass);
            if (auditInfoField != null) {
                if (state[i] == null) {
                    state[i] = newInstance();
                }
                return (Embeddable) state[i];
            }
        }
        return null;
    }

    @Override
    public final boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        Embeddable embeddable = findEmbeddableProperty(entity, state, propertyNames);
        if (embeddable != null) {
            takeCreateEvent(embeddable);
            return true;
        } else {
            return false;
        }
    }

    protected abstract void takeCreateEvent(Embeddable embeddable);

    @Override
    public final boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
        Embeddable embeddable = findEmbeddableProperty(entity, currentState, propertyNames);
        if (embeddable != null) {
            takeModifiedEvent(embeddable);
            return true;
        } else {
            return false;
        }
    }

    protected abstract void takeModifiedEvent(Embeddable embeddable);

}
EmbeddablePropertyInterceptorChainProxy
package com.gss.gmo.cao.hibernate.interceptor;

import java.io.Serializable;
import java.util.LinkedHashSet;
import java.util.Set;

import lombok.extern.apachecommons.CommonsLog;

import org.hibernate.EmptyInterceptor;
import org.hibernate.type.Type;

@CommonsLog
public class EmbeddablePropertyInterceptorChainProxy extends EmptyInterceptor {

    private static final long serialVersionUID = 5566719359520070978L;

    private Set<EmbeddablePropertyInterceptor<?>> interceptorChain = new LinkedHashSet<EmbeddablePropertyInterceptor<?>>();

    public void setInterceptorChain(Set<EmbeddablePropertyInterceptor<?>> interceptorChain) {
        this.interceptorChain.addAll(interceptorChain);
    }

    public void addInterceptorChain(EmbeddablePropertyInterceptor<?> interceptor) {
        interceptorChain.add(interceptor);
    }

    @Override
    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
        boolean isStateChanged = false;
        for (EmbeddablePropertyInterceptor<?> interceptor : interceptorChain) {
            log.debug("invoke onFlushDirty of " + interceptor.toString());

            if (interceptor.onFlushDirty(entity, id, currentState, previousState, propertyNames, types)) {
                isStateChanged = true;
            }
        }
        return isStateChanged;
    }

    @Override
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        boolean isStateChanged = false;
        for (EmbeddablePropertyInterceptor<?> interceptor : interceptorChain) {
            log.debug("invoke onSave of " + interceptor.toString());

            if (interceptor.onSave(entity, id, state, propertyNames, types)) {
                isStateChanged = true;
            }
        }
        return isStateChanged;
    }

}
小結:
在EmbeddablePropertyInterceptor裡面我們留了兩個abstract method,讓developer各自實作。
EmbeddablePropertyInterceptorChainProxy是為了可以攔截不同的property,但限制是type不能重複。

Part II
AuditInterceptor
package com.gss.gmo.cao.hibernate.audit;

import java.util.Date;

import org.springframework.beans.factory.annotation.Required;

import com.gss.gmo.cao.hibernate.interceptor.EmbeddablePropertyInterceptor;

/**
 * Auto-set values of createUserId, createDate, lastModifiedUserId and
 * lastModifiedDate.
 *
 * @author linus_chien
 *
 */
public class AuditInterceptor extends EmbeddablePropertyInterceptor<AuditInfo> {

    private static final long serialVersionUID = 1L;

    /**
     * Provide audit user id.
     */
    private AuditUserIdAware auditUserIdAware;

    @Required
    public void setAuditUserIdAware(AuditUserIdAware auditUserIdAware) {
        this.auditUserIdAware = auditUserIdAware;
    }

    @Override
    protected void takeCreateEvent(AuditInfo auditInfo) {
        auditInfo.setCreateUserId(auditUserIdAware.getAuditUserId());
        auditInfo.setCreateUserName(auditUserIdAware.getAuditUserName());
        auditInfo.setCreateUserEnglishName(auditUserIdAware.getAuditUserEnglishName());
        auditInfo.setCreateDate(new Date());
    }

    @Override
    protected void takeModifiedEvent(AuditInfo auditInfo) {
        auditInfo.setLastModifiedUserId(auditUserIdAware.getAuditUserId());
        auditInfo.setLastModifiedUserName(auditUserIdAware.getAuditUserName());
        auditInfo.setLastModifiedUserEnglishName(auditUserIdAware.getAuditUserEnglishName());
        auditInfo.setLastModifiedDate(new Date());
    }

}
AuditUserIdAware
package com.gss.gmo.cao.hibernate.audit;

import java.io.Serializable;

/**
 * @author linus_chien
 *
 */
public interface AuditUserIdAware extends Serializable {

    /**
     * @return user id for auditing.
     */
    String getAuditUserId();

    /**
     * @return user name for auditing.
     */
    String getAuditUserName();

    /**
     * @return user English name for auditing.
     */
    String getAuditUserEnglishName();

}
AuditInfo
package com.gss.gmo.cao.hibernate.audit;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Embeddable;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

/**
 * @author linus_chien
 *
 */
@Getter
@Setter
@ToString
@Embeddable
public class AuditInfo implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * column length.
     */
    private static final int LENGTH = 50;

    /**
     * create user id.
     */
    @Column(name = "CREATE_USER_ID", length = LENGTH)
    private String createUserId;

    /**
     * create user name.
     */
    @Column(name = "CREATE_USER_NAME", length = LENGTH)
    private String createUserName;

    /**
     * create user English name.
     */
    @Column(name = "CREATE_USER_ENGLISH_NAME", length = LENGTH)
    private String createUserEnglishName;

    /**
     * create date.
     */
    @Column(name = "CREATE_DATE")
    private Date createDate;

    /**
     * last modified user id.
     */
    @Column(name = "LAST_MODIFIED_USER_ID", length = LENGTH)
    private String lastModifiedUserId;

    /**
     * last modified user name.
     */
    @Column(name = "LAST_MODIFIED_USER_NAME", length = LENGTH)
    private String lastModifiedUserName;

    /**
     * last modified English name.
     */
    @Column(name = "LAST_MODIFIED_USER_ENGLISH_NAME", length = LENGTH)
    private String lastModifiedUserEnglishName;

    /**
     * last modified date.
     */
    @Column(name = "LAST_MODIFIED_DATE")
    private Date lastModifiedDate;

}
小結:
讓Hibernate Interceptor幫我們攔截AuditInfo;AuditUserIdAware是為了loose coupling,不讓取得使用者資訊的程式碼出現在AuditInterceptor裡面。

Part III
Person
package com.gss.gmo.cao.hibernate.impl;

import java.util.List;

import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.Id;

import org.hibernate.annotations.Filter;
import org.hibernate.annotations.Type;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import com.gss.gmo.cao.base.BaseDto;
import com.gss.gmo.cao.hibernate.audit.AuditInfo;

/**
 * @author linus_chien
 *
 */
@Getter
@Setter
@EqualsAndHashCode(callSuper = false, of = { "id" })
@ToString
@Entity
@Filter(name = "personFilter", condition = "name like :name and address is null")
public class Person extends BaseDto {

    private static final long serialVersionUID = 1L;

    @Id
    private String id;

    @Column
    private String name;

    @Column
    private String address;

    @Embedded
    private AuditInfo auditInfo;

    @Type(type = "listType")
    private List<String> emails;

}
LoginUserIdProvider
package com.gss.gmo.cao.hibernate.impl;

import com.gss.gmo.cao.hibernate.audit.AuditUserIdAware;

public class LoginUserIdProvider implements AuditUserIdAware {

    private static final long serialVersionUID = -5133132384619280103L;

    @Override
    public String getAuditUserId() {
        return "linus_chien";
    }

    @Override
    public String getAuditUserName() {
        return "Cheng-Ming Chien";
    }

    @Override
    public String getAuditUserEnglishName() {
        return "Linus";
    }

}
SpringConfig
package com.gss.gmo.cao.hibernate.impl;

import org.hibernate.Interceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.gss.gmo.cao.hibernate.audit.AuditInterceptor;
import com.gss.gmo.cao.hibernate.interceptor.EmbeddablePropertyInterceptorChainProxy;

@Configuration
public class SpringConfig {

    @Bean
    public Interceptor embeddablePropertyInterceptorChainProxy() {
        EmbeddablePropertyInterceptorChainProxy interceptor = new EmbeddablePropertyInterceptorChainProxy();
        AuditInterceptor auditInterceptor = new AuditInterceptor();
        auditInterceptor.setAuditUserIdAware(new LoginUserIdProvider());
        interceptor.addInterceptorChain(auditInterceptor);
        return interceptor;
    }

}
Spring XML
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource">
        <ref bean="dataSource" />
    </property>
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
            <prop key="hibernate.hbm2ddl.auto">create</prop>
            <prop key="hibernate.show_sql">true</prop>
            <prop key="hibernate.format_sql">true</prop>
        </props>
    </property>
    <property name="packagesToScan" value="com.gss.gmo.cao.hibernate.impl" />
    <property name="annotatedPackages" value="com.gss.gmo.cao.hibernate.impl" />
    <property name="entityInterceptor" ref="embeddablePropertyInterceptorChainProxy" />
</bean>
小結:
LoginUserIdProvider裡面可以和Spring Security整合,取得目前登入的使用者資訊。

2013年1月31日 星期四

Unit Test to Struts2 Action with Spring

我將展示使用JUnit3和JUnit4兩種測試Struts2 Action的寫法,兩種都會enable Spring以及所有Struts2 plugins。

JUnit3:
TestActionJUnit3Test
import static org.apache.commons.lang.StringUtils.replace;
import lombok.SneakyThrows;
import org.apache.struts2.StrutsSpringTestCase;
import com.opensymphony.xwork2.ActionProxy;

public class TestActionJUnit3Test extends StrutsSpringTestCase {

    @SneakyThrows
    public void test() {
        ActionProxy proxy = getActionProxy("/test/test.action");
        String result = proxy.execute();
        assertEquals("success", result);
        assertEquals("Test by Linus", ((TestAction) proxy.getAction()).getMessage());

        executeAction("/test/test");
        assertFalse(((TestAction) findValueAfterExecute("action")).hasFieldErrors());
        assertEquals("Test by Linus", findValueAfterExecute("message"));
    }

    @Override
    protected String[] getContextLocations() {
        return new String[] { "classpath:" + replace(getClass().getName(), ".", "/") + "-context.xml" };
    }

}
上面我將預設讀取的Spring XML路徑改成和@ContextConfiguration一致。

JUnit4:
TestActionJUnit4Test
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import org.apache.struts2.StrutsSpringJUnit4TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.opensymphony.xwork2.ActionProxy;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TestActionJUnit4Test extends StrutsSpringJUnit4TestCase<TestAction> {

    @Override
    protected String getConfigPath() {
        return "struts-plugin.xml";
    }

    @Test
    public void testExecute() throws Exception {
        ActionProxy proxy = getActionProxy("/test/test.action");
        String result = proxy.execute();
        assertEquals("success", result);
        assertEquals("Test by Linus", ((TestAction) proxy.getAction()).getMessage());

        executeAction("/test/test");
        assertFalse(getAction().hasFieldErrors());
        assertEquals("Test by Linus", findValueAfterExecute("message"));
    }

}
上面getConfigPath()可以回傳其它Struts2的XML檔,利用這個method可以啟動其它plugins,但JUnit3的寫法不需要這個動作。

2013年1月24日 星期四

Enable Hibernate Filter Dynamically

Hibernate Filter功能的主要目的是預先設定好filter definition和filter condition,然後在操作Hibernate session時決定要不要enable filter並提供要傳入condition的變數,將一些公用的condition抽離到filter中,Hibernate會在符合filter設定的任何查詢上都補上定義好的condition。

但這樣還不夠好,原因是這樣的使用方式會讓DAO裡面充滿enable filter的程式碼,developer必須了解這些filter的規範才能正確使用,如果我們會依照使用者的身份決定condition的值,那麼與security有關的API就會汙染DAO,為此,我們利用Spring AOP的功能來完成enable filter dynamically的機制,讓interceptor來決定目前被攔截的DAO是否需要enable filter並傳入condition變數。

以下的程式碼分為兩大部分,interceptor and test case。

Interceptor:
EnableFilterInterceptor
package com.gss.gmo.cao.hibernate.filter;

import java.lang.reflect.Field;
import java.util.Map;
import java.util.Set;

import lombok.SneakyThrows;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.hibernate.Filter;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.util.ReflectionUtils;

import com.gss.gmo.cao.hibernate.HibernateGenericDao;
import com.gss.gmo.cao.hibernate.HibernateGenericPagingDao;

/**
 * @author linus_chien
 *
 */
public class EnableFilterInterceptor implements MethodInterceptor {

    /**
     * Provide filter name and parameters to enable.
     */
    private EnableFilterInfoAware enableFilterInfoAware;

    /**
     * @param enableFilterInfoAware
     */
    @Required
    public void setEnableFilterInfoAware(EnableFilterInfoAware enableFilterInfoAware) {
        this.enableFilterInfoAware = enableFilterInfoAware;
    }

    @Override
    @SneakyThrows
    public Object invoke(MethodInvocation invocation) {
        Object target = invocation.getThis();
        if (target instanceof HibernateGenericDao || target instanceof HibernateGenericPagingDao) {
            SessionFactory sessionFactory = getSessionFactory(target);
            Session session = sessionFactory.getCurrentSession();
            @SuppressWarnings("unchecked")
            Set<String> filterNames = sessionFactory.getDefinedFilterNames();
            Map<String, Map<String, Object>> filterInfo = enableFilterInfoAware.getEnableFilterInfo();
            for (String filterName : filterNames) {
                if (filterInfo.containsKey(filterName)) {
                    Filter filter = session.getEnabledFilter(filterName);
                    if (filter == null) {
                        filter = session.enableFilter(filterName);
                        Map<String, Object> parameters = filterInfo.get(filterName);
                        for (String parameterName : parameters.keySet()) {
                            Object parameterValue = parameters.get(parameterName);
                            filter.setParameter(parameterName, parameterValue);
                        }
                        filter.validate();
                    }
                }
            }
        }

        Object result = invocation.proceed();
        return result;
    }

    /**
     * @param target
     * @return SessionFactory
     */
    private SessionFactory getSessionFactory(Object target) {
        Field sessionFactoryField = ReflectionUtils.findField(target.getClass(), "sessionFactory", SessionFactory.class);
        ReflectionUtils.makeAccessible(sessionFactoryField);
        return (SessionFactory) ReflectionUtils.getField(sessionFactoryField, target);
    }

}
EnableFilterInfoAware
package com.gss.gmo.cao.hibernate.filter;

import java.util.Map;

/**
 * @author linus_chien
 *
 */
public interface EnableFilterInfoAware {

    /**
     * Key: filter name, Value: parameters (Key: parameter name, Value: value).
     *
     * @return
     */
    Map<String, Map<String, Object>> getEnableFilterInfo();

}
EnableFilterInfoAware的用意事實上就是Strategy Pattern的一種應用,再一次將怎麼enable filter這件事交給strategy object負責。

Test Case包含四小部分:
  1. Filter Definition:package-info.java
  2. Filter:Person
  3. Implement Strategy:EnableFilterInfoProvider
  4. Setup AOP:Spring XML
package-info.java
/**
 * @author linus_chien
 *
 */
@FilterDef(name = "personFilter", parameters = { @ParamDef(name = "name", type = "string"), @ParamDef(name = "address", type = "string") })
@TypeDef(name = "listType", typeClass = ListJsonType.class)
package com.gss.gmo.cao.hibernate.impl;

import org.hibernate.annotations.FilterDef;
import org.hibernate.annotations.ParamDef;
import org.hibernate.annotations.TypeDef;
Person
package com.gss.gmo.cao.hibernate.impl;

import java.util.List;

import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.Id;

import org.hibernate.annotations.Filter;
import org.hibernate.annotations.Type;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import com.gss.gmo.cao.base.BaseDto;
import com.gss.gmo.cao.hibernate.audit.AuditInfo;

/**
 * @author linus_chien
 *
 */
@Getter
@Setter
@EqualsAndHashCode(callSuper = false, of = { "id" })
@ToString
@Entity
@Filter(name = "personFilter", condition = "name like :name and address is null")
public class Person extends BaseDto {

    private static final long serialVersionUID = 1L;

    @Id
    private String id;

    @Column
    private String name;

    @Column
    private String address;

    @Embedded
    private AuditInfo auditInfo;

    @Type(type = "listType")
    private List<String> emails;

}
EnableFilterInfoProvider
package com.gss.gmo.cao.hibernate.impl;

import java.util.HashMap;
import java.util.Map;

import com.gss.gmo.cao.hibernate.filter.EnableFilterInfoAware;

public class EnableFilterInfoProvider implements EnableFilterInfoAware {

    @Override
    public Map<String, Map<String, Object>> getEnableFilterInfo() {
        Map<String, Map<String, Object>> enableFilterInfo = new HashMap<String, Map<String, Object>>();
        Map<String, Object> parameter = new HashMap<String, Object>();
        parameter.put("name", "%Linus%");
        parameter.put("address", "abc");
        enableFilterInfo.put("personFilter", parameter);
        return enableFilterInfo;
    }

}
Spring XML
<aop:config>
    <aop:pointcut id="daoPointcut" expression="execution(* com.gss.gmo.cao.hibernate.*Dao.*(..))" />
    <aop:advisor advice-ref="daoAdvice" pointcut-ref="daoPointcut" />
</aop:config>

<bean id="daoAdvice" class="com.gss.gmo.cao.hibernate.filter.EnableFilterInterceptor">
    <property name="enableFilterInfoAware">
        <bean class="com.gss.gmo.cao.hibernate.impl.EnableFilterInfoProvider" />
    </property>
</bean>
這樣只要是被攔截到的DAO就會enable filter,然後只要查詢Person就會補上額外的condition,而相同的filter definition套在每個entity都能有不同的condition。想像一下,如果今天要做到個人只能看個人的資料,而主管可以看到部門內所有人的資料,利用這個方式就不需要在DAO裡面implement這樣的邏輯,而是交給Hibernate Filter和Strategy處理了。

最後我們看一下Hibernate實際下的SQL statement:
Show SQL
Hibernate:
    select
        count(*) as y0_
    from
        Person this_
    where
        this_.name like ? 
        and this_.address is null
        and (
            this_.name=?
        )

2013年1月23日 星期三

Applying Bean Validation (JSR303) for Design by Contract

原本method level validation應該在Bean Validation 1.1 (JSR349)才有,但是Hibernate Validator和Spring已經搶先提供這個功能,所以我們現在可以用method validation輕易做到design by contract的precondition和postcondition。

它的運作原理很簡單,Spring利用AOP攔截需要進行validation的method,在method執行的前、後進行驗證,如果有違反condition就拋出exception。

以下是一組設定範例:
Spring XML
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />

<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor" />
Method Validation Sample
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.NotBlank;
import org.springframework.validation.annotation.Validated;
import com.gss.acl.service.ws.domain.AclUserDetails;

@Validated
public interface AclUserDetailsWebService {

    @NotNull
    AclUserDetails loadUserByUsername(@NotBlank String username);

}
這樣就能在interface中規範condition並強化domain service的完整性,而且將validation邏輯從implementation class中抽離。

Applying Spring Security for SSO of Cross Web Applications

因應雲端世代的來臨,在架構設計上必須讓系統具有分散式佈署的能力,所以將一個大的系統用多個web application來開發是無可避免的方式,除了在module上可以做功能性的分割之外,也可以將不同的web application佈署到不同的application server上,做到server loading分散的目的,只要架設一個Apache HTTP Server做為load balancer,對前端使用者來說就不會感覺到系統後面其實有很多application server在服務。

在這個前提之下,我們會面臨不同web application的session其實是被隔離的問題,也就是說在一個web application登入之後,另外一個web application並不會知道使用者已經登入過,或許會有人用cross servlet context的方式讓不同的web application交換資料,但這牽涉到所使用application server的特性,不見得是好的方法。

我們使用Spring Security的Pre-Authentication機制來處理SSO of Cross Web Applications問題,在某個master web application中實作登入機制,其它的slave web application只需要套用Pre-Authentication機制取得使用者曾經登入的資訊,那麼就不會發生登入不同步的問題了。

以下是我們簡單利用cookie記錄使用者曾經登入過的帳號密碼來做Pre-Authentication,當然帳號、密碼是經過加密的。
Spring XML for Master Web Application
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:security="http://www.springframework.org/schema/security"
    xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <security:http pattern="/login.action" security="none" />
    <security:http pattern="/struts/**" security="none" />
    <security:http pattern="/css/**" security="none" />
    <security:http pattern="/images/**" security="none" />
    <security:http pattern="/js/**" security="none" />
    <security:http pattern="/styles/**" security="none" />
    <security:http pattern="/template/**" security="none" />
    <security:http auto-config="true" authentication-manager-ref="authenticationManager" use-expressions="true">
        <security:form-login authentication-success-handler-ref="cookieTokenAwareAuthenticationSuccessHandler"
            login-page="/login.action" authentication-failure-url="/login.action?login_error=1" />
        <security:logout invalidate-session="true" success-handler-ref="cookieTokenClearingLogoutSuccessHandler"
            logout-url="/spring_security_logout" />
        <security:custom-filter position="PRE_AUTH_FILTER" ref="cookieTokenAuthenticationFilter" />
        <security:custom-filter position="CAS_FILTER" ref="casFilter" />
        <security:intercept-url pattern="/**" access="authenticated" />
    </security:http>

    <security:authentication-manager alias="authenticationManager">
        <security:authentication-provider ref="preauthAuthProvider" />
        <security:authentication-provider ref="casAuthenticationProvider" />
        <security:authentication-provider ref="ldapAuthenticationProvider" />
        <security:authentication-provider user-service-ref="userDetailService">
            <!-- <security:password-encoder ref="passwordEncoder"/> -->
        </security:authentication-provider>
    </security:authentication-manager>

    <!-- <bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder"/> -->

    <bean id="cookieTokenAwareAuthenticationSuccessHandler" class="com.gss.gmo.cao.spring.security.web.authentication.CookieTokenAwareAuthenticationSuccessHandler">
        <property name="defaultTargetUrl" value="/init/menu.jsp" />
        <property name="alwaysUseDefaultTargetUrl" value="true" />
    </bean>

    <bean id="cookieTokenClearingLogoutSuccessHandler" class="com.gss.gmo.cao.spring.security.web.authentication.logout.CookieTokenClearingLogoutSuccessHandler">
        <property name="defaultTargetUrl" value="/login.action" />
        <property name="alwaysUseDefaultTargetUrl" value="true" />
    </bean>

    <bean id="cookieTokenAuthenticationFilter" class="com.gss.gmo.cao.spring.security.web.authentication.preauth.CookieTokenAuthenticationFilter">
        <property name="authenticationManager" ref="authenticationManager" />
        <property name="continueFilterChainOnUnsuccessfulAuthentication" value="true" />
        <property name="checkForPrincipalChanges" value="true" />
    </bean>

    <bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
        <property name="preAuthenticatedUserDetailsService" ref="authenticationUserDetailsService" />
    </bean>

    <bean id="authenticationUserDetailsService" class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
        <property name="userDetailsService" ref="userDetailService" />
    </bean>

    <bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
        <property name="service" value="http://localhost:8080/portal-web/gss/j_spring_cas_security_check" />
    </bean>

    <bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
        <property name="filterProcessesUrl" value="/gss/j_spring_cas_security_check" />
        <property name="authenticationManager" ref="authenticationManager" />
        <property name="authenticationSuccessHandler" ref="cookieTokenAwareAuthenticationSuccessHandler" />
        <property name="authenticationFailureHandler">
            <bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
                <property name="defaultFailureUrl" value="/login.action?login_error=1" />
            </bean>
        </property>
    </bean>

    <bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
        <property name="serviceProperties" ref="serviceProperties" />
        <property name="ticketValidator">
            <bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
                <constructor-arg value="http://teamkube.gss.com.tw/cas" />
            </bean>
        </property>
        <property name="key" value="teamkube-cas" />
        <property name="authenticationUserDetailsService" ref="authenticationUserDetailsService" />
    </bean>

</beans>
Spring XML for Slave Web Application
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:security="http://www.springframework.org/schema/security"
    xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <import resource="classpath:spring-acm-core.xml" />
    <import resource="classpath:gmo-cao-struts2-spring-context.xml" />

    <context:annotation-config />
    <context:component-scan base-package="com.gss.acm.web" />

    <security:http pattern="/struts/**" security="none" />
    <security:http pattern="/css/**" security="none" />
    <security:http pattern="/images/**" security="none" />
    <security:http pattern="/js/**" security="none" />
    <security:http pattern="/styles/**" security="none" />
    <security:http pattern="/template/**" security="none" />
    <security:http auto-config="true" use-expressions="true">
        <!-- Additional http configuration omitted -->
        <security:form-login login-page="/../portal-web/login.action" />
        <security:logout invalidate-session="true" logout-success-url="/../portal-web/spring_security_logout"
            logout-url="/spring_security_logout" />
        <security:custom-filter position="PRE_AUTH_FILTER" ref="cookieTokenAuthenticationFilter" />
        <security:intercept-url pattern="/**" access="authenticated" />
    </security:http>

    <security:authentication-manager alias="authenticationManager">
        <security:authentication-provider ref="preauthAuthProvider" />
    </security:authentication-manager>

</beans>
CookieTokenAuthenticationFilter
package com.gss.gmo.cao.spring.security.web.authentication.preauth;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import org.jasypt.encryption.StringEncryptor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.web.util.WebUtils;

import com.gss.gmo.cao.spring.security.util.StringEncryptorFactory;

/**
 * Get username and password from cookie for SSO.
 *
 * @author linus_chien
 *
 */
public class CookieTokenAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {

    /**
     * Encryptor.
     */
    private StringEncryptor encryptor = StringEncryptorFactory.createInstance();

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (getPreAuthenticatedPrincipal((HttpServletRequest) request) == null) {
            SecurityContextHolder.clearContext();
        }
        super.doFilter(request, response, chain);
    }

    @Override
    protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
        Cookie passwordCookie = WebUtils.getCookie(request, UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY);
        if (passwordCookie != null) {
            return encryptor.decrypt(passwordCookie.getValue());
        }
        return null;
    }

    @Override
    protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
        Cookie usernameCookie = WebUtils.getCookie(request, UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY);
        if (usernameCookie != null) {
            return encryptor.decrypt(usernameCookie.getValue());
        }
        return null;
    }

}
CookieTokenAwareAuthenticationSuccessHandler
package com.gss.gmo.cao.spring.security.web.authentication;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.jasypt.encryption.StringEncryptor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.gss.gmo.cao.spring.security.util.StringEncryptorFactory;

/**
 * Set username/password to cookie for cross web context SSO.
 *
 * @author linus_chien
 *
 */
public class CookieTokenAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    /**
     * Encryptor.
     */
    private StringEncryptor encryptor = StringEncryptorFactory.createInstance();
 
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException,
            IOException {
     
        UserDetails user = (UserDetails) authentication.getPrincipal();
        Cookie username = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY, encryptor.encrypt(user.getUsername()));
        username.setPath("/");
        Cookie password = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY, encryptor.encrypt(user.getPassword()));
        password.setPath("/");

        response.addCookie(username);
        response.addCookie(password);

        super.onAuthenticationSuccess(request, response, authentication);
    }

}
CookieTokenClearingLogoutSuccessHandler
package com.gss.gmo.cao.spring.security.web.authentication.logout;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;

/**
 * Clear username/password from cookie for cross web context SSO.
 *
 * @author linus_chien
 *
 */
public class CookieTokenClearingLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        Cookie username = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY, null);
        username.setPath("/");
        username.setMaxAge(0);
        Cookie password = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY, null);
        password.setPath("/");
        password.setMaxAge(0);

        response.addCookie(username);
        response.addCookie(password);

        super.onLogoutSuccess(request, response, authentication);
    }

}
最後需要強調的是AbstractPreAuthenticatedProcessingFilter和AbstractAuthenticationProcessingFilter是不相關的filter類別,如果是內部系統彼此間的SSO可以使用AbstractPreAuthenticatedProcessingFilter,但是外部系統的SSO最好還是使用AbstractAuthenticationProcessingFilter的子類別來處理,但無論使用哪種方式,我們都會reuse UserDetailsService instance。

2013年1月22日 星期二

Interview Spring Security



Spring Security的設計如diagram所示:
  1. 每個Filter都會有一個能處理的Authentication type,從request取得資料後生出Authentication instance,然後傳給AuthenticationManager進行驗證。
  2. Spring Security預設只提供一個ProviderManager實作了AuthenticationManager介面。
  3. ProviderManager會依序將Authentication傳給AuthenticationProvider進行驗證。
  4. Spring Security實作的AuthenticationProvider大多都可以傳入UserDetailsService,以方便在通過身分驗證後取得使用者權限,在實作上我們會reuse UserDetailsService instance,如sequence diagram中的UserDetailsServiceImpl就被DaoAuthenticationProvider、LdapAuthenticationProvider、CasAuthenticationProvider所使用。
PS:Sequence diagram省略了org.springframework.security.web.FilterChainProxy和ProviderManager。

Integrate CAS with Spring Security

Spring Security和CAS的整合關係有outgoing和incoming兩個方向:
  1. Outgoing:是指將未經過身分驗證的使用者導到CAS的登入畫面進行身分驗證。
  2. Incoming:是指在CAS已經完成身分認證,然後透過CAS的token機制進行SSO,讓使用者不需要再輸入一次帳號密碼即可進入系統。
一般的系統若是沒有自己的登入入口,就兩者都需要設定,但如果系統本身已經有登入入口,那麼只需要設定第二個SSO的設定就可以了。

以下是在Spring Security設定可以從CAS進行SSO到我們的系統的設定方式,也就是incoming的方向,其中一些handler暫時不多做說明。
Spring XML
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:security="http://www.springframework.org/schema/security"
    xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <security:http pattern="/login.action" security="none" />
    <security:http pattern="/struts/**" security="none" />
    <security:http pattern="/css/**" security="none" />
    <security:http pattern="/images/**" security="none" />
    <security:http pattern="/js/**" security="none" />
    <security:http pattern="/styles/**" security="none" />
    <security:http pattern="/template/**" security="none" />
    <security:http auto-config="true" authentication-manager-ref="authenticationManager" use-expressions="true">
        <security:form-login authentication-success-handler-ref="cookieTokenAwareAuthenticationSuccessHandler"
            login-page="/login.action" authentication-failure-url="/login.action?login_error=1" />
        <security:logout invalidate-session="true" success-handler-ref="cookieTokenClearingLogoutSuccessHandler"
            logout-url="/spring_security_logout" />
        <security:custom-filter position="PRE_AUTH_FILTER" ref="cookieTokenAuthenticationFilter" />
        <security:custom-filter position="CAS_FILTER" ref="casFilter" />
        <security:intercept-url pattern="/**" access="authenticated" />
    </security:http>

    <security:authentication-manager alias="authenticationManager">
        <security:authentication-provider ref="preauthAuthProvider" />
        <security:authentication-provider ref="casAuthenticationProvider" />
        <security:authentication-provider ref="ldapAuthenticationProvider" />
        <security:authentication-provider user-service-ref="userDetailService">
            <!-- <security:password-encoder ref="passwordEncoder"/> -->
        </security:authentication-provider>
    </security:authentication-manager>

    <!-- <bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder"/> -->

    <bean id="cookieTokenAwareAuthenticationSuccessHandler" class="com.gss.gmo.cao.spring.security.web.authentication.CookieTokenAwareAuthenticationSuccessHandler">
        <property name="defaultTargetUrl" value="/init/menu.jsp" />
        <property name="alwaysUseDefaultTargetUrl" value="true" />
    </bean>

    <bean id="cookieTokenClearingLogoutSuccessHandler" class="com.gss.gmo.cao.spring.security.web.authentication.logout.CookieTokenClearingLogoutSuccessHandler">
        <property name="defaultTargetUrl" value="/login.action" />
        <property name="alwaysUseDefaultTargetUrl" value="true" />
    </bean>

    <bean id="cookieTokenAuthenticationFilter" class="com.gss.gmo.cao.spring.security.web.authentication.preauth.CookieTokenAuthenticationFilter">
        <property name="authenticationManager" ref="authenticationManager" />
        <property name="continueFilterChainOnUnsuccessfulAuthentication" value="true" />
        <property name="checkForPrincipalChanges" value="true" />
    </bean>

    <bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
        <property name="preAuthenticatedUserDetailsService" ref="authenticationUserDetailsService" />
    </bean>

    <bean id="authenticationUserDetailsService" class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
        <property name="userDetailsService" ref="userDetailService" />
    </bean>

    <bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
        <property name="service" value="http://localhost:8080/portal-web/gss/j_spring_cas_security_check" />
    </bean>

    <bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
        <property name="filterProcessesUrl" value="/gss/j_spring_cas_security_check" />
        <property name="authenticationManager" ref="authenticationManager" />
        <property name="authenticationSuccessHandler" ref="cookieTokenAwareAuthenticationSuccessHandler" />
        <property name="authenticationFailureHandler">
            <bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
                <property name="defaultFailureUrl" value="/login.action?login_error=1" />
            </bean>
        </property>
    </bean>

    <bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
        <property name="serviceProperties" ref="serviceProperties" />
        <property name="ticketValidator">
            <bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
                <constructor-arg value="http://teamkube.gss.com.tw/cas" />
            </bean>
        </property>
        <property name="key" value="teamkube-cas" />
        <property name="authenticationUserDetailsService" ref="authenticationUserDetailsService" />
    </bean>

</beans>
依照我們的設定,用URL:
http://teamkube.gss.com.tw/cas/login?service=http%3A%2F%2Flocalhost%3A8080%2Fportal-web%2Fgss%2Fj_spring_cas_security_check
就可以用CAS的畫面登入到我們的系統。

2013年1月18日 星期五

Customize Hibernate JSON UserType & Apply Package Level Annotation

這裡我們展示如何自訂一個generic UserType,功能是將entity的任意物件轉換成JSON之後以String的方式儲存到database中,然後提供一個sample供參考,而這個sample會以package level的方式設定。
GenericJsonStringType<T>
package com.gss.gmo.cao.hibernate.usertype;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Date;

import org.apache.commons.lang3.ObjectUtils;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.usertype.UserType;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.gss.gmo.cao.gson.ISODateTimeAdapter;

public abstract class GenericJsonStringType<T> implements UserType {

    private static final int[] SQL_TYPES = { Types.LONGVARCHAR };

    private final Type userType;

    private final Gson gson;

    public GenericJsonStringType() {
        Type type = getClass().getGenericSuperclass();
        ParameterizedType pt = (ParameterizedType) type;
        userType = pt.getActualTypeArguments()[0];
        gson = new GsonBuilder().registerTypeAdapter(Date.class, new ISODateTimeAdapter()).create();
    }

    @Override
    public int[] sqlTypes() {
        return SQL_TYPES;
    }

    @SuppressWarnings("rawtypes")
    @Override
    public Class returnedClass() {
        if (userType instanceof Class) {
            return (Class) userType;
        } else {
            return (Class) ((ParameterizedType) userType).getRawType();
        }
    }

    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        return ObjectUtils.equals(x, y);
    }

    @Override
    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }

    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
        String jsonString = rs.getString(names[0]);
        return gson.fromJson(jsonString, userType);
    }

    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
        st.setString(index, gson.toJson(value, userType));
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException {
        return gson.fromJson(gson.toJson(value, userType), userType);
    }

    @Override
    public boolean isMutable() {
        return true;
    }

    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        return gson.toJson(value, userType);
    }

    @Override
    public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return gson.fromJson((String) cached, userType);
    }

    @Override
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }

}
Sample有四個部分:
  1. Setup org.springframework.orm.hibernate4.LocalSessionFactoryBean
  2. Setup org.hibernate.annotations.TypeDef in package-info.java
  3. Extend GenericJsonStringType<T>
  4. Mapping UserType field in entity
Setup org.springframework.orm.hibernate4.LocalSessionFactoryBean
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource">
        <ref bean="dataSource" />
    </property>
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
            <prop key="hibernate.show_sql">true</prop>
            <prop key="hibernate.format_sql">true</prop>
        </props>
    </property>
    <property name="packagesToScan" value="com.gss.gmo.cao.hibernate.impl" />
    <property name="annotatedPackages" value="com.gss.gmo.cao.hibernate.impl" />
</bean>
Setup org.hibernate.annotations.TypeDef in package-info.java
@TypeDef(name = "listType", typeClass = ListJsonType.class)
package com.gss.gmo.cao.hibernate.impl;

import org.hibernate.annotations.TypeDef;
Extend GenericJsonStringType<T>
package com.gss.gmo.cao.hibernate.impl;

import java.util.List;
import com.gss.gmo.cao.hibernate.usertype.GenericJsonStringType;

public class ListJsonType extends GenericJsonStringType<List<String>> {
}
Mapping UserType field in entity
package com.gss.gmo.cao.hibernate.impl;

import java.util.List;

import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.Id;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import org.hibernate.annotations.Type;

import com.gss.gmo.cao.base.BaseDto;
import com.gss.gmo.cao.hibernate.audit.AuditInfo;

/**
 * @author linus_chien
 *
 */
@Getter
@Setter
@EqualsAndHashCode(callSuper = false, of = { "id" })
@ToString
@Entity
public class Person extends BaseDto {

    private static final long serialVersionUID = 1L;

    @Id
    private String id;

    @Column
    private String name;

    @Column
    private String address;

    @Embedded
    private AuditInfo auditInfo;

    @Type(type = "listType")
    private List<String> emails;

}

2013年1月17日 星期四

Configure LdapAuthenticationProvider Programmatically

LdapAuthenticationProvider基本的設定,將此bean塞到AuthenticationManager中即可。
SpringConfiguration
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.authentication.BindAuthenticator;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import com.gss.gmo.cao.spring.security.ldap.userdetails.UserDetailsServiceContextMapper;

@Configuration
public class SpringConfiguration {

    @SneakyThrows
    @Bean
    @Autowired
    LdapAuthenticationProvider ldapAuthenticationProvider(UserDetailsService userDetailsService, @Value("${ldap.server.url}") String serverUrl,
            @Value("${ldap.server.username}") String serverUsername, @Value("${ldap.server.password}") String serverPassword) {

        DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(serverUrl);
        contextSource.setUserDn(serverUsername);
        contextSource.setPassword(serverPassword);
        contextSource.afterPropertiesSet();

        FilterBasedLdapUserSearch filterBasedLdapUserSearch = new FilterBasedLdapUserSearch("OU=GSS,DC=gss,DC=com,DC=tw", "(CN={0})", contextSource);

        BindAuthenticator bindAuthenticator = new BindAuthenticator(contextSource);
        bindAuthenticator.setUserSearch(filterBasedLdapUserSearch);
        bindAuthenticator.afterPropertiesSet();

        LdapAuthenticationProvider ldapAuthenticationProvider = new LdapAuthenticationProvider(bindAuthenticator);
        ldapAuthenticationProvider.setUserDetailsContextMapper(new UserDetailsServiceContextMapper(userDetailsService));
        return ldapAuthenticationProvider;
    }

}
UserDetailsContextMapper的用途是在通過身分驗證之後,可以利用UserDetailsService取得使用者權限。
UserDetailsServiceContextMapper
import java.util.Collection;
import lombok.extern.apachecommons.CommonsLog;
import org.apache.commons.lang.Validate;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;

/**
 * @author linus_chien
 *
 */
@CommonsLog
public class UserDetailsServiceContextMapper implements UserDetailsContextMapper {

    private UserDetailsService userDetailsService;

    public UserDetailsServiceContextMapper(UserDetailsService userDetailsService) {
        Validate.notNull(userDetailsService);
        this.userDetailsService = userDetailsService;
    }

    @Override
    public UserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> authorities) {
        try {
            return userDetailsService.loadUserByUsername(username);
        } catch (UsernameNotFoundException e) {
            throw e;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new UsernameNotFoundException(e.getMessage());
        }
    }

    @Override
    public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
        throw new UnsupportedOperationException(
                "UserDetailsServiceContextMapper only supports reading from a context. Please use a subclass if mapUserToContext() is required.");
    }

}