單點登入與訊息佇列以及在J2EE中的實現方案

segmentfault發表於2015-09-18

前言

很久都沒有寫部落格了,這次為大家簡單介紹兩個在WEB開發中經常使用的概念——單點登入和訊息佇列以及具體到J2EE中的一些實現方案。本文原創性的工作比較少,主要是一些總結概括和自己的理解。

單點登入SSO

SSO的業務場景

所謂單點登入就是在一個站點登入之後可以授信給其他站點,這樣就可以做到一次登入,到處操作。單點登入的實質就是安全上下文(Security Context)或憑證(Credential)在多個應用系統之間的傳遞或共享。

大部分的網站採用Cookie作為登入的一種簡單實現方案,在同一個一級域名下面,這樣做並無問題,不需要對各個子系統分別驗證。但是Cookie無法跨域傳遞。將使用者的登入、憑證取得等解耦處理單獨作為一個子系統是合理的選擇。

SSO的核心要素

  • 共享同一個身份認證系統,也就是說所有站點的身份驗證操作在同一個系統下完成
  • 每個子系統從共同的身份認證系統中取得使用者憑證,包含使用者的身份、許可權資訊等

示意圖如下:

SSO結構示意圖

SSO的一種簡單實現方案

下面以採用Cookie的一種方案為例來解釋:

我們首先定義授信伺服器A,受信伺服器B,客戶C;當前的業務是B需要驗證C的身份。需要注意的是B和C都會保有session來記錄C的登入狀態,均會向C 的Header中寫入對應自己域名的Cookie以儲存憑證資訊。Cookie中含有tokenId來標示C,也就是說對於A和B他們的Cookie中對應於同一個C,其tokenId應該一致。

C向B發起請求後,會有以下幾種情形:

  1. B含有session,C含有Cookie,且session和Cookie中的token一致,那麼不需要向A求助
  2. C中對於B無Cookie或Cookie過期或session與Cookie不一致,將向A發起請求。之後根據A的情形,有以下情況:
    • A中session與C中Cookie的token一致,重新生成憑證資訊返回給B,B重新寫入Cookie與session
    • A中Cookie過期或資訊不一致,將重定向到登入頁面
  3. 對於登入情形,A將更新Cookie與session,然後C再向B發起請求,這時就會變成2中第一種情況,導致A和B的資訊完成同步。

訊息佇列

MQ的業務場景

訊息佇列本身是簡單的,可以直接看做一個佇列,重點是如何定義儲存在佇列中的資料格式,以滿足我們對應的操作需求。MQ常常應用於那些併發量大而對於實時性要求不高的情況。舉個例子,比如一個使用者量較大的社交網站的評論釋出,為什麼這麼說呢?對於這個任務,佇列中只用儲存評論相關資訊,對於從佇列中取的一方,只需要進行插入操作,符合前面所說的併發量大且可以有延時,同時並不難實現。

MQ的兩種模式

訊息佇列在WEB開發中主要有兩種模式:

  • 生產者/消費者模式:對於一則訊息,只有一個消費者執行緒會去處理它,適用於我們上面所說的評論系統
  • 釋出者/訂閱者模式:對於所有訂閱者,它可以讀取所有在它加入之後釋出的訊息

在J2EE中加入訊息佇列,我個人認為應該是這樣的:對於特定的HTTP請求,呼叫生產者/釋出者的介面,入隊必要訊息,這個並不困難。大有蹊蹺的我覺得在於處理訊息的一方,可以實現listener將其交由容器管理,也可以自己開闢池來排程。舉例來說明,對於前者Spring-redis實現的pub/sub模式佇列就是直接在配置檔案中設定RedisListener的實現類,對於後者,你可以直接獨立出來寫離線指令碼來監聽佇列。

MQ的實現方案

目前業界有比較成熟的MQ解決產品,如下:

  • RabbitMQ
  • ActiveMQ
  • kafka
  • Redis

MQ的Spring+Redis實現簡單示例

在Pom.xml中加入以下依賴

<dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-redis</artifactId>
        <version>1.4.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <version>2.3</version>
    </dependency>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.6.2</version>
    </dependency>

在ApplicationContext.xml的頭部插入schema

xmlns:redis="http://www.springframework.org/schema/redis"

在ApplicationContext中加入Redis的配置

<!-- 配置redis池,依次為最大例項數,最大空閒例項數,(建立例項時)最大等待時間,(建立例項時)是否驗證 -->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxTotal" value="${redis.maxTotal}"/>
        <property name="maxIdle" value="${redis.maxIdle}"/>
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}"/>
        <property name="testOnBorrow" value="${redis.testOnBorrow}"/>
    </bean>

    <!-- 配置資料來源-->
    <bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="127.0.0.1"></property>
        <property name="port" value="6379"></property>
        <property name="usePool" value="true"></property>
    </bean>    

    <!-- 配置資料操作 -->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="redisConnectionFactory"></property>
    </bean>        
    <bean id="jdkSerializer" class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />

    <bean id="messageListener" class="org.springframework.data.redis.listener.adapter.MessageListenerAdapter">
        <property name="delegate" ref="messageDelegateListener" /> <!--這裡的messageDelegateListener在後面的檔案中註解的,這裡對應的具體訊息處理類的實現-->
        <property name="serializer" ref="jdkSerializer" />
    </bean>     

    <!-- 將訊息handler註冊 -->
    <redis:listener-container>
        <redis:listener ref="messageListener" method="handleMessage" serializer="jdkSerializer" topic="java"/>
    </redis:listener-container>

上文在定義Listener的時候採用了註解物件作為實現類,也可以手動在配置檔案中再寫一個bean,如下

<bean id="messageDelegateListener" class="***.***.***" />

最後我們給出一個接收方的實現

import java.io.Serializable;

import org.springframework.stereotype.Component;

@Component(value="messageDelegateListener")
public class ListenMessage {
    public void handleMessage(Serializable message){
        System.out.println(message);
    }
}

相關文章