作一個租戶系統下的權限服務,接管用戶的認證和受權,咱們取名該服務爲
oneday-auth-server
java
DDD(領域驅動設計)中涉及到幾個概念,實體,值對象,聚合,限定上下文。本篇只涉及實踐,概念講解將放在下一篇,同時上一篇爲何咱們須要領域驅動設計做爲科普帖,你們能夠在看完代碼以後再回頭理解一下,同時對比一下現有項目,知其然更要知其因此然,你常常遇到了什麼問題,爲何DDD可以更好的解決軟件負責的問題。git
認證功能即登陸功能,登陸成功登陸態的設定,登陸失敗的處理方式例如IP鎖定,失敗超過次數鎖定等方式github
受權功能即對認證經過的用戶,進行角色和權限授予,同時開啓資源保護,未具有訪問該資源權限的用戶將沒法訪問。算法
本篇將詳細介紹如何在DDD的指導下實現第一點功能。數據庫
咱們先明白的一點是領域這個詞語承載了太多的含義,既能夠表示整個業務系統,也能夠表示其中的某個核心域或者支撐子域。舉個不是很恰當的例子,假設咱們本來想要在一個叫帳戶模塊實現了這個功能,同時還有用戶信息功能,這個時候,帳戶就是一個大的領域,一塊的大蛋糕,而oneday-auth
則是這塊大蛋糕的某一塊,用戶信息又是另外一塊,這被分出的一塊一塊蛋糕,咱們稱之爲由帳戶領域分紅的子域,權限子域和用戶信息子域。子域下還能夠再接着劃分出子域,沒有最小的子域,只有最合適的子域。緩存
你會以爲這個微服務的拆分很像,是的,微服務的拆分是遵循DDD的思想,可是你再仔細思考下,你是否是隻學了一個形式而已能夠對比一下下面的了兩張圖片和你的思路是否是不謀而合。bash
本文中我將權限子域再劃分出了認證上下文和受權上下文。對於界限上下文,咱們把重點放在界限上,摘抄實現領域驅動設計的一段話:session
好比,「顧客」這個術語可能有多種含義。在瀏覽產品目錄的時候,「顧客」表示一種意思;而在下單的時候,「顧客」又表示另外一種意思。緣由在於:當瀏覽產品目錄時,「顧客」被放在了先前購買狀況、忠誠度、可買產品、折扣和物流方式這樣的上下文中。而在上下單時,「顧客」的上下文包括名字、產品寄送地址、訂單總價和一些付款術語架構
我在oneday-auth
中設計了一個類LoginUserE
,用來表明登陸用戶實體類,包含的信息僅僅跟認證和受權相關,而用戶信息子域中,確定也有一個用戶類UserInfo
,可是這裏的表明的含義是跟業務系統相關信息,好比說性別,暱稱。我相信大多數讀者確定經歷過一個類中承擔過多功能,試圖去建立一個全功能的類,最終致使的結果各位也可想而知,貪一時之方便帶來的是不斷拆東牆補西牆。app
用戶進入認證界限上下文,他在這裏只會被認爲 一個待認證,並且只具有認證相關的信息,用戶進入受權界限上下文,他在這裏只會被認爲一個認證成功,等待受權或者具有權限的用戶。認證上下文和受權上下文咱們能夠
因而在代碼裏,我劃分了兩個包模塊:
> 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
的核心做用的是校驗,登陸不登陸,即登陸態的設定並非他所關心的,並非他的業務邏輯。領域層只保證用戶和密碼是正確的,而其餘一切東西都是外圍,應用層,甚至是上游服務得知校驗成功以後再來設定登陸態。
咱們接着看看領域層,領域服務是如何工做的。
咱們先介紹兩個類,LoginUserRepositoryPort
和LoginUserConverter
。讀者可能會有一個疑惑是,怎麼可能會沒有技術細節呢,我怎樣都須要將數據保存到數據庫中,這確定就涉及到持久化技術,這個時候六邊形架構就應運而生了。咱們的口號是「領域層不摻雜任何技術細節」,任何的外部依賴,咱們都定義成一個端口類,而具體的實現交由各個層的適配器去實現,經過依賴注入實現相應的依賴功能。如何檢驗這一點,就是要看你的領域層能不能作到拷貝不走樣,即若是你單純複製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。重點是業務邏輯,無技術細節。持久化只是一種存儲技術,不要由於用了這一個技術反而被綁架了你的思路。
業務層執行非業務邏輯,領域層只執行業務邏輯,使用端口-適配器模式隔離外部依賴,檢驗的標準是拷貝不走樣。第一步的界限上下文劃分很關鍵。一開始的劃分就決定了你是面向對象仍是面向過程。不要被持久化技術綁架了咱們的開發思路。咱們的口號是「領域層不摻雜任何技術細節」,咱們的目標是真正的面向對象開發,咱們的理想是永不加班!!!'
走過路過不要錯誤,您的點贊是支持我寫做最好的動力
源碼地址:github.com/iamlufy/one…
做者:plz叫我紅領巾
本博客歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。