領域驅動設計之實戰權限系統微服務

作一個租戶系統下的權限服務,接管用戶的認證和受權,咱們取名該服務爲oneday-auth-serverjava

寫在前面

​ DDD(領域驅動設計)中涉及到幾個概念,實體,值對象,聚合,限定上下文。本篇只涉及實踐,概念講解將放在下一篇,同時上一篇爲何咱們須要領域驅動設計做爲科普帖,你們能夠在看完代碼以後再回頭理解一下,同時對比一下現有項目,知其然更要知其因此然,你常常遇到了什麼問題,爲何DDD可以更好的解決軟件負責的問題。git

需求描述

  1. ​ 認證功能即登陸功能,登陸成功登陸態的設定,登陸失敗的處理方式例如IP鎖定,失敗超過次數鎖定等方式github

  2. ​ 受權功能即對認證經過的用戶,進行角色和權限授予,同時開啓資源保護,未具有訪問該資源權限的用戶將沒法訪問。算法

    本篇將詳細介紹如何在DDD的指導下實現第一點功能。數據庫

領域、子域和界限上下文

​ 咱們先明白的一點是領域這個詞語承載了太多的含義,既能夠表示整個業務系統,也能夠表示其中的某個核心域或者支撐子域。舉個不是很恰當的例子,假設咱們本來想要在一個叫帳戶模塊實現了這個功能,同時還有用戶信息功能,這個時候,帳戶就是一個大的領域,一塊的大蛋糕,而oneday-auth則是這塊大蛋糕的某一塊,用戶信息又是另外一塊,這被分出的一塊一塊蛋糕,咱們稱之爲由帳戶領域分紅的子域,權限子域和用戶信息子域。子域下還能夠再接着劃分出子域,沒有最小的子域,只有最合適的子域。緩存

​ 你會以爲這個微服務的拆分很像,是的,微服務的拆分是遵循DDD的思想,可是你再仔細思考下,你是否是隻學了一個形式而已?能夠對比一下下面的了兩張圖片和你的思路是否是不謀而合。session

DDD中的劃分

普通微服務的劃分

​ 本文中我將權限子域再劃分出了認證上下文和受權上下文。對於界限上下文,咱們把重點放在界限上,摘抄實現領域驅動設計的一段話:架構

好比,「顧客」這個術語可能有多種含義。在瀏覽產品目錄的時候,「顧客」表示一種意思;而在下單的時候,「顧客」又表示另外一種意思。緣由在於:當瀏覽產品目錄時,「顧客」被放在了先前購買狀況、忠誠度、可買產品、折扣和物流方式這樣的上下文中。而在上下單時,「顧客」的上下文包括名字、產品寄送地址、訂單總價和一些付款術語app

​ 我在oneday-auth中設計了一個類LoginUserE,用來表明登陸用戶實體類,包含的信息僅僅跟認證和受權相關,而用戶信息子域中,確定也有一個用戶類UserInfo,可是這裏的表明的含義是跟業務系統相關信息,好比說性別,暱稱。我相信大多數讀者確定經歷過一個類中承擔過多功能,試圖去建立一個全功能的類,最終致使的結果各位也可想而知,貪一時之方便帶來的是不斷拆東牆補西牆。dom

​ 用戶進入認證界限上下文,他在這裏只會被認爲 一個待認證,並且只具有認證相關的信息,用戶進入受權界限上下文,他在這裏只會被認爲一個認證成功,等待受權或者具有權限的用戶。認證上下文和受權上下文咱們能夠

​ 因而在代碼裏,我劃分了兩個包模塊:

> one.day.auth:
    >> authentication :認證即用戶登陸,身份識別等功能
    >> authorization :受權上下文:給予用戶身份,角色,權限,並判斷用戶是否具有訪問某個功能的權限等功能

​ 看到這裏,請讀者本身思考一個問題,若是按照原來的作法,你會不會分出兩個包,你的大體作法是否是以下

> one.day.auth.service
    >> authenticationServiceImpl
    >> authorizationServiceImpl

​ 若是你看到這裏忽然有了一種思惟的自我鬥爭,甚至有一種恍然大悟的感受,那麼恭喜你,你已經開始培養了DDD的思惟。

​ 小結:代碼目錄的不一樣,就從一開始決定了你的開發思惟。傳統的MVC分層註定沒法真正有效的劃分領域,從而實現面向對象開發

代碼實踐

代碼分層

爲何咱們須要領域驅動設計提到了兩個架構,四層架構和六邊形架構(又稱端口-適配器)。其中六邊形架構是從四層架構進一步發來而來的,是邏輯意義上的,代碼的物理分層是作不到所謂六邊形的。咱們暫時拋開這一切,只關注咱們想要的目的。

領域對象要作到只關心業務邏輯,不能出現絲毫技術細節,即不直接依賴任何外部,經過接口去依賴

​ 應用層:非業務相關處理;領域層:業務相關處理;基礎設施:持久化,緩存等技術細節實現。代碼目錄分層以下:

> one.day.auth.authentication
> > app 應用層
> > client 二方包,這裏方便起見放在了同一個Maven項目中
> > domain 領域層
> > > entity 實體包,具有行爲,不具有數據狀態
> > > port 端口定義,外部依賴統必定義爲端口
> > > service 領域服務
> > infrastructure 基礎設施層
> > > adapt 適配器,實現領域層定義的端口接口
> > > converter DTO,DO,Entity互相轉換的工具類
> > > dataobject 表映射包 不具有行爲,具有數據狀態
> > > repository 倉儲
> > > tunnel 通道

功能實現

​ 咱們來看看登陸這一個功能具體是如何實現的。

@Component
public class AuthenticationApp {
   /**
     * 領域層,登陸領域服務
     */
    @Autowired
    private LoginService loginService;
    /**
     * 登陸
     *
     * @param loginCmd
     */
    public void login(LoginCmd loginCmd) {
        //調用領域層進行登陸校驗
        String userId = loginService.login(loginCmd);
        //session中存放userId已證實登陸
        //因爲領域層主要負責登陸,或者校驗密碼,登陸成功以後的登陸態設定不關心,交由應用層負責
        ProjectUtil.setSession("userId", userId);
    }
    public void addLoginUser(AddLoginUserCmd addLoginUserCmd) {
        loginService.addLoginUser(addLoginUserCmd);
    }
}

​ 咱們能夠看到,應用層AuthenticationApp先調用了領域層的領域服務LoginService,當該方法沒有拋出異常則證實用戶校驗成功,可是注意的是LoginService核心做用的是校驗,登陸不登陸,即登陸態的設定並非他所關心的,並非他的業務邏輯。領域層只保證用戶和密碼是正確的,而其餘一切東西都是外圍,應用層,甚至是上游服務得知校驗成功以後再來設定登陸態。

六邊形架構

​ 咱們接着看看領域層,領域服務是如何工做的。

​ 咱們先介紹兩個類,LoginUserRepositoryPortLoginUserConverter。讀者可能會有一個疑惑是,怎麼可能會沒有技術細節呢,我怎樣都須要將數據保存到數據庫中,這確定就涉及到持久化技術,這個時候六邊形架構就應運而生了。咱們的口號是「領域層不摻雜任何技術細節」,任何的外部依賴,咱們都定義成一個端口類,而具體的實現交由各個層的適配器去實現,經過依賴注入實現相應的依賴功能。如何檢驗這一點,就是要看你的領域層能不能作到拷貝不走樣,即若是你單純複製domain目錄到其餘的項目中,是否可以正常編譯。

LoginUserConverter存在的意義是什麼,DTO,Entity,DataObject之間總會互相轉換,將這一部分代碼統一放到Converter類中。我相信讀者的很多項目,各類轉換都是很隨意的,開心就好:)

@Service
public class LoginServiceImpl implements LoginService {
    private final LoginUserRepositoryPort loginUserRepositoryPort;
    private final LoginUserConverter loginUserConverter;
    @Autowired
    public LoginServiceImpl(LoginUserRepositoryPort loginUserRepositoryPort, LoginUserConverter loginUserConverter) {
        this.loginUserRepositoryPort = loginUserRepositoryPort;
        this.loginUserConverter = loginUserConverter;
    }
    @Override
    public String login(LoginCmd loginCmd) {
        Optional<LoginUserE> optionalLoginUserE = loginUserRepositoryPort.findByUsername(loginCmd.getUsername());
        optionalLoginUserE.orElseThrow(() -> new BaseException(GlobalEnum.NON_EXIST));
        LoginUserE loginUserE = optionalLoginUserE.get();
        loginUserE.login(loginCmd.getPassword());
        //todo 登陸成功,異步通知觀察者
        return loginUserE.getUserId();
    }
    @Override
    public void addLoginUser(AddLoginUserCmd addLoginUserCmd) {
        LoginUserE loginUserE = loginUserConverter.convert2Entity(addLoginUserCmd);
        loginUserE.prepareToAdd();
        loginUserRepositoryPort.add(loginUserE);
    }
}

​ 領域服務LoginServiceImpl的第一件事是經過依賴注入獲取的LoginUserRepositoryPort去查詢獲取登陸用戶LoginUserE,若是存在則調用login方法。咱們看看LoginUserE到底是什麼玩意。

@Data
public class LoginUserE extends Unique {
    public static final String COMMON_SALT = "commonSalt";
    /**
     * 登陸用戶名
     */
    private String username;
    /**
     * 登陸密碼
     */
    private String password;
    /**
     * 鹽
     */
    private String salt;
    /**
     * 加密算法
     */
    private EncryptionAlgorithmV encryptionAlgorithmV;
    /**
     * 業務惟一ID
     */
    private String userId;
    private TenantIdV tenantIdV;
    /**
     * 比較密碼
     *
     * @param sendPwd 傳入的密碼
     * @return true/false
     */
    public boolean login(String sendPwd) {
        //檢查available
        //錯誤次數限制
        //鎖號 ip
        return StringUtils.equals(password, encryptionAlgorithmV.getPasswordEncoder().encoder(sendPwd, salt));
    }
    /**
     * 密碼加密
     */
    public void encryptPassword() {
        this.setSalt(RandomStringUtils.randomNumeric(8));
        this.setPassword(encryptionAlgorithmV.getPasswordEncoder().encoder(password, salt));
    }
}

​ 代碼邏輯其實很簡單,留着幾個擴展功能沒有實現,一個是針對登陸失敗的各類場景操做,第二個是,對不一樣的租戶下的用戶系統實現不一樣的加密器。功能從上帝Service類轉移到具有真正意義的實體類上,具有真正的行爲,符合類的單一職責標準。

​ 到這裏登陸功能講解就算是結束,但其中我留有一個功能未開發,即登陸成功,異步通知觀察者,DDD中同時倡導事件驅動開發和最終一致性。這其實也是跟類的單一職責原則有關。在整個登陸功能中,校驗是第一步,校驗成功緊接着是進行受權,二者是上下游關係,核心業務邏輯不該該寫在一塊,這在傳統MVC項目中二者是絕對的耦合在一塊兒。而採用事件驅動能夠將二者分離,不管是異步或者同步,簡單起見的話能夠直接使用guava的EventBus。

​ 持久化層的設計和特色本篇暫不涉及,不可一步而就,事實上若是你還關心這一點的話則證實你還未能理解DDD。重點是業務邏輯,無技術細節。持久化只是一種存儲技術,不要由於用了這一個技術反而被綁架了你的思路。

總結

​ 業務層執行非業務邏輯,領域層只執行業務邏輯,使用端口-適配器模式隔離外部依賴,檢驗的標準是拷貝不走樣。第一步的界限上下文劃分很關鍵。一開始的劃分就決定了你是面向對象仍是面向過程。不要被持久化技術綁架了咱們的開發思路。咱們的口號是「領域層不摻雜任何技術細節」,咱們的目標是真正的面向對象開發,咱們的理想是永不加班!!!

​ 源碼地址:https://github.com/iamlufy/oneday-auth

​ 做者:plz叫我紅領巾   

​ 出處:http://www.javashuo.com/article/p-vgbzhxot-hw.html

  本博客歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。

相關文章
相關標籤/搜索