設(shè)計(jì)模式綜合應(yīng)用——MyShiro

1.概述

模仿 Shiro 開發(fā)一個(gè)簡單權(quán)限框架案例。

主要使用了以下幾種設(shè)計(jì)模式:

  • 單例模式
  • 工廠模式
  • 策略模式
  • 責(zé)任鏈模式

該權(quán)限框架主要有以下開發(fā)點(diǎn):

  • 讀取配置文件
  • 密碼加密(策略模式耳璧,工廠模式)
  • 身份認(rèn)證(責(zé)任鏈模式嫩舟,單例模式)
  • 權(quán)限認(rèn)證

2.各個(gè)模塊

2.1 讀取配置文件

public class Config {

    private static Map<String, String> configMap = new HashMap<>();

    static {
        InputStream in = Config.class.getResourceAsStream("/permission.ini");
        DataInputStream dis = new DataInputStream(in);
        String str;
        try{
            while ((str = dis.readLine()) != null){
                String[] configs = str.split("=");
                if(configs.length == 2){
                    configMap.put(configs[0].trim(),configs[1].trim());
                }
            }
            dis.close();
        }catch (Exception e){
            throw new RuntimeException("配置文件不存在");
        }

    }

    public static String get(String name){
        return configMap.get(name);
    }

    public static String get(String name,String defaultValue){
        String value = configMap.get(name);
        return value == null ? defaultValue : value;
    }


}

在上面 Config 類中用到了 IO 流,IO 流本身就是裝飾器模式的一種應(yīng)用。

2.2 密碼加密

密碼加密模式采用 MD5 加密,但是我們要開發(fā)一個(gè)可靈活擴(kuò)展的框架,允許開發(fā)者們自定義加密方式漓雅,并且能夠通過修改配置文件來修改加密方式。這里我們采用了策略模式朽色,其類圖如下:


public interface PasswordEncrypt {

    String encrypt(String password);

}

默認(rèn)的 MD5 加密:

public class Md5Encrypt implements PasswordEncrypt {

    @Override
    public String encrypt(String password) {
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
            md5.update(password.getBytes());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        return new BigInteger(1,md5.digest()).toString(16);
    }

}

用工廠來創(chuàng)建加密策略類(使用反射機(jī)制動(dòng)態(tài)創(chuàng)建策略類):

public class EncryptFactory {
    /**
     * md5
     * @param clazz 類名
     * @return
     */
    public static PasswordEncrypt create(String clazz){

        try {
            Class cls = Class.forName(clazz);
            Object obj = cls.newInstance();
            if(obj instanceof PasswordEncrypt){
                return (PasswordEncrypt)obj;
            }else{
                throw new RuntimeException("class not found:" + clazz);
            }
        } catch (Exception e) {
            throw new RuntimeException("class not found:" + clazz);
        }
    }
}

在 EncryptContext 中邻吞,根據(jù)配置文件的配置來動(dòng)態(tài)選擇使用哪種加密策略,默認(rèn) MD5 加密

public class EncryptContext {

    private PasswordEncrypt pe;

    public EncryptContext() {
        String cls = Config.get("encryptType","com.design.pattern.encrypt.Md5Encrypt");
        this.pe = EncryptFactory.create(cls);
    }

    public String encrypt(String password){
        return this.pe.encrypt(password);
    }

}

測試一下默認(rèn)的 MD5 加密:

String encryptedPwd = (new EncryptContext()).encrypt("123");
System.out.println("加密后:"+encryptedPwd);

2.2.1 自定義加密邏輯

第一葫男,增加加密策略類:

public class MyEncrypt implements PasswordEncrypt {

    @Override
    public String encrypt(String password) {
        return password + " encrypted pwd";
    }

}

第二抱冷,把該類配置到配置文件中:

encryptType = test.encrypt.MyEncrypt

2.3 身份認(rèn)證與權(quán)限認(rèn)證

Realm 這個(gè)概念來自于 Shiro 框架,它是用于進(jìn)行身份認(rèn)證和獲取用戶權(quán)限梢褐。在本案例中徘层,Realm 主要有兩個(gè)抽象方法:

  • abstract boolean loginAuth(AuthToken token);
  • abstract PermissionInfo doGetPermissionInfo(AuthToken token);

第一個(gè)方法用于判斷當(dāng)前用戶是否認(rèn)證成功,在用戶登錄時(shí)將調(diào)用該方法利职。

第二個(gè)方法是獲取當(dāng)前用戶擁有哪些權(quán)限趣效,在判斷用戶是否有某權(quán)限時(shí)調(diào)用該方法。

這兩個(gè)方法都需要開發(fā)者去實(shí)現(xiàn)猪贪。

開發(fā)者可以自定義多個(gè) Realm跷敬,比如 Realm1 驗(yàn)證用戶名密碼,Realm2 用于驗(yàn)證第三方登錄(微信登錄等)热押。在這里我使用了責(zé)任鏈模式西傀,多個(gè) Realm 只要有一個(gè)驗(yàn)證通過,那么該用戶就登錄成功桶癣。


多個(gè)自定義 Realm 將形成一個(gè)責(zé)任鏈拥褂,而形成責(zé)任鏈的步驟將由AuthManager 完成,并且AuthManager 類是一個(gè)單例模式牙寞。

AuthRealm類:

public abstract class AuthRealm {

    private AuthRealm successor;

    public void setSuccessor(AuthRealm realm){
        this.successor = realm;
    }

    public final boolean auth(AuthToken token){

        if(token == null) return false;
        //如果驗(yàn)證成功饺鹃,就返回成功
        if(this.loginAuth(token)){
            return true;
        }
        //失敗就將請(qǐng)求傳給下一個(gè)責(zé)任處理器
        return successor != null && successor.auth(token);
    }

    /**
     * 登錄驗(yàn)證
     * @return
     */
    protected abstract boolean loginAuth(AuthToken token);

    /**
     * 權(quán)限驗(yàn)證
     * @return
     */
    protected abstract PermissionInfo doGetPermissionInfo(AuthToken token);
}

在上面類中,使用到了AuthToken 和 PermissionInfo 间雀,前者是用戶認(rèn)證信息悔详,存放用戶名密碼等,后者是保存權(quán)限信息惹挟,包括“角色”和“權(quán)限”茄螃。

AuthToken類:

public class AuthToken {

    private String username;
    private String password;

    public AuthToken() {
    }

    public AuthToken(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}

PermissionInfo 類:

public class PermissionInfo {

    private Set<String> permissions;
    private Set<String> roles;

    public Set<String> getPermissions() {
        return permissions;
    }

    public void setPermissions(Set<String> permissions) {
        this.permissions = permissions;
    }

    public Set<String> getRoles() {
        return roles;
    }

    public void setRoles(Set<String> roles) {
        this.roles = roles;
    }
    /**
     * 判斷是否有某權(quán)限
     * @param permission
     * @return
     */
    public boolean isPermitted(String permission){
        return this.permissions.contains(permission);
    }

}

AuthManager類:

public class AuthManager {

    private List<AuthRealm> list = new ArrayList<>();

    private static AuthManager instance = new AuthManager();

    /**
     * 私有構(gòu)造方法,讀取配置文件连锯,通過反射機(jī)制生成Realm归苍,并構(gòu)建責(zé)任鏈
     */
    private AuthManager() {

        String realms = Config.get("realms");

        if(realms == null || realms.isEmpty()){
            throw new RuntimeException("請(qǐng)定義Realm");
        }

        String[] clss = realms.split(",");
        for (int i = 0;i < clss.length; i++){
            try {
                Object obj = Class.forName(clss[i]).newInstance();
                if(obj instanceof AuthRealm){
                    this.list.add((AuthRealm)obj);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //形成責(zé)任鏈
        for (int i=0;i<list.size()-1;i++){
            AuthRealm next = list.get(i+1);
            if(next != null){
                list.get(i).setSuccessor(next);
            }
        }

    }
   /**
     * 調(diào)用 Realm 中的 DoGetPermissionInfo 方法用狱,如果有多個(gè) Realm,只調(diào)用第一個(gè)
     * @param token
     * @return
     */
    public PermissionInfo getPermissionInfo(AuthToken token){
        if(token == null){
            return null;
        }
        if(list.size() > 0){
            return this.list.get(0).doGetPermissionInfo(token);
        }
        return null;
    }

    /**
     * 登錄認(rèn)證拼弃,調(diào)用 Realm 責(zé)任鏈
     * @return
     */
    public boolean auth(AuthToken token){
        if(list.size() == 0){
            return false;
        }
        return list.get(0).auth(token);
    }
    /**
     * 單例
     * @return
     */
    public static AuthManager getInstance(){
        return instance;
    }

}

AuthManager 主要有以下職責(zé):

  • 生成 Realm 責(zé)任鏈
  • 調(diào)用身份認(rèn)證和權(quán)限認(rèn)證方法

2.4 主體類

還缺一個(gè)用戶主體類Subject夏伊。所謂的用戶主體,有點(diǎn)類似 Web 開發(fā)中的 Session肴敛,一個(gè)用戶請(qǐng)求對(duì)應(yīng)一個(gè)Session,而在權(quán)限框架中吗购,用戶主體類 Subject 就代表了當(dāng)前用戶医男。

Auth 接口,定義了登錄捻勉,權(quán)限等方法:

public interface Auth {

    /**
     * 登錄操作
     * @param token
     * @return
     */
    boolean login(AuthToken token);

    /**
     * 是否已登錄
     * @return
     */
    boolean isLogin();

    /**
     * 是否有權(quán)限
     * @param permission
     * @return
     */
    boolean isPermitted(String permission);
}

用戶主體類 Subject :

/**
 * 登錄用戶主體
 */
public class Subject implements Auth{

    private AuthToken token;

    @Override
    public boolean login(AuthToken token) {
        //調(diào)用密碼加密策略
        String password = (new EncryptContext()).encrypt(token.getPassword());
        token.setPassword(password);
        //調(diào)用auth方法镀梭,即觸發(fā)責(zé)任鏈
        if(AuthManager.getInstance().auth(token)){
            System.out.println("登錄成功");
            this.token = token;
            return true;
        }
        return false;
    }

    @Override
    public boolean isLogin() {
        return token != null;
    }

    @Override
    public boolean isPermitted(String permission) {
        PermissionInfo info =  AuthManager.getInstance().getPermissionInfo(this.token);
        return info != null && info.isPermitted(permission);
    }

    public String getUsername(){
        return token.getUsername();
    }
}

工具類 SecurityUtils,提供了全局獲取用戶主體類 Subject 的方法:

public class SecurityUtils {

    private static Map<String, Subject> subjectList = new HashMap<>();

    /**
     * 獲取當(dāng)前請(qǐng)求的用戶
     * @return
     */
    public static Subject getSubject(){
        //此處應(yīng)借用 Session 等方式獲取當(dāng)前請(qǐng)求用戶
        String name = "123";
        Subject subject = subjectList.get(name);
        return subject == null ? new Subject() : subject;
    }

    public static void addSubject(Subject subject){
        subjectList.put("123",subject);
    }
}

實(shí)際上上面SecurityUtils 中的 getSubject() 的實(shí)現(xiàn)機(jī)制也應(yīng)該是一個(gè)類似 Session 的機(jī)制踱启,就像我們?cè)?Web 請(qǐng)求中獲取當(dāng)前Session报账,Session 和當(dāng)前用戶對(duì)應(yīng)。但本次案例主要是介紹設(shè)計(jì)模式埠偿,就不去實(shí)現(xiàn)那么復(fù)雜的功能了透罢,因此這里就簡單地直接給出 Subject 了。

2.5 整體結(jié)構(gòu)圖

2.6 測試

創(chuàng)建 Realm1:

public class Realm1 extends AuthRealm {

    @Override
    protected boolean loginAuth(AuthToken token) {
        System.out.println("===Realm1 loginAuth===");
        String username = token.getUsername();
        String pwd = token.getPassword();
        //傳進(jìn)來的密碼是加密過的密碼冠蒋,直接和數(shù)據(jù)庫中的密碼比對(duì)
        System.out.println("pwd:"+pwd);
        //查詢數(shù)據(jù)庫操作略過
        return false;
    }

    @Override
    protected PermissionInfo doGetPermissionInfo(AuthToken token) {

        String username = token.getUsername();
        System.out.println("doGetPermissionInfo1");
        //從數(shù)據(jù)庫讀取該用戶的權(quán)限信息
        PermissionInfo info = new PermissionInfo();
        Set<String> s = new HashSet<String>();
        s.add("permission1");
        s.add("permission2");
        info.setPermissions(s);
        //角色
        Set<String> r = new HashSet<String>();
        r.add("role1");
        info.setRoles(r);
        return info;

    }
}

再創(chuàng)建一個(gè) Realm2羽圃,Realm2 的和1結(jié)構(gòu)是一樣的,具體的業(yè)務(wù)邏輯要根據(jù)你項(xiàng)目實(shí)際情況去修改抖剿,這里只是測試朽寞,就直接給出一模一樣的代碼:

public class Realm2 extends AuthRealm {

    @Override
    protected boolean loginAuth(AuthToken token) {
        System.out.println("===Realm2 loginAuth===");
        String username = token.getUsername();
        String pwd = token.getPassword();
        //傳進(jìn)來的密碼是加密過的密碼,直接和數(shù)據(jù)庫中的密碼比對(duì)
        System.out.println("pwd:"+pwd);
        //查詢數(shù)據(jù)庫操作略過
        return true;
    }

    @Override
    protected PermissionInfo doGetPermissionInfo(AuthToken token) {
        String username = token.getUsername();
        System.out.println("doGetPermissionInfo2");
        //從數(shù)據(jù)庫讀取該用戶的權(quán)限信息
        PermissionInfo info = new PermissionInfo();
        Set<String> s = new HashSet<String>();
        s.add("printer:print");
        s.add("printer:query");
        info.setPermissions(s);

        //角色
        Set<String> r = new HashSet<String>();
        r.add("role1");

        info.setRoles(r);
        return info;
    }
}

然后將兩個(gè) Realm 配置到配置文件中斩郎,多個(gè) Realm 用逗號(hào)隔開:

encryptType = test.encrypt.MyEncrypt
realms=test.realm.Realm1,test.realm.Realm2

測試代碼:

public class TestDemo {

    public static void main(String[] args) throws IOException{
        //測試密碼加密
        String encryptedPwd = (new EncryptContext()).encrypt("123");
        System.out.println("加密后:"+encryptedPwd);
        //獲取當(dāng)前用戶
        Subject currentUser = SecurityUtils.getSubject();
        //是否登錄
        System.out.println("是否已登錄:"+currentUser.isLogin());
        //執(zhí)行登錄操作
        currentUser.login(new AuthToken("admin","123"));
        //是否登錄
        System.out.println("是否已登錄:"+currentUser.isLogin());
        //是否有權(quán)限脑融,權(quán)限用字符串表示
        System.out.println("是否有權(quán)限:"+currentUser.isPermitted("permission1"));
    }
}

測試結(jié)果:


3.總結(jié)

僅僅看是沒用的,看10遍真的不如自己寫一遍缩宜。找一個(gè)知名的開源框架肘迎,了解其工作流程后,嘗試著用設(shè)計(jì)模式自己去寫一個(gè)簡單的例子锻煌,例如寫個(gè) SpringMVC膜宋。就像本節(jié)課的權(quán)限框架的例子,就是仿照 Shiro 框架寫的炼幔。

項(xiàng)目源碼參考github

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末秋茫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子乃秀,更是在濱河造成了極大的恐慌肛著,老刑警劉巖圆兵,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異枢贿,居然都是意外死亡殉农,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門局荚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來超凳,“玉大人,你說我怎么就攤上這事耀态÷职” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵首装,是天一觀的道長创夜。 經(jīng)常有香客問我,道長仙逻,這世上最難降的妖魔是什么驰吓? 我笑而不...
    開封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮系奉,結(jié)果婚禮上檬贰,老公的妹妹穿的比我還像新娘。我一直安慰自己缺亮,他們只是感情好偎蘸,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瞬内,像睡著了一般摆昧。 火紅的嫁衣襯著肌膚如雪围肥。 梳的紋絲不亂的頭發(fā)上奇瘦,一...
    開封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天闷盔,我揣著相機(jī)與錄音,去河邊找鬼能真。 笑死赁严,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的粉铐。 我是一名探鬼主播疼约,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼蝙泼!你這毒婦竟也來了程剥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤汤踏,失蹤者是張志新(化名)和其女友劉穎织鲸,沒想到半個(gè)月后舔腾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡搂擦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年稳诚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瀑踢。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扳还,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出橱夭,到底是詐尸還是另有隱情氨距,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布徘钥,位于F島的核電站衔蹲,受9級(jí)特大地震影響肢娘,放射性物質(zhì)發(fā)生泄漏呈础。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一橱健、第九天 我趴在偏房一處隱蔽的房頂上張望而钞。 院中可真熱鬧,春花似錦拘荡、人聲如沸臼节。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽网缝。三九已至,卻和暖如春蟋定,著一層夾襖步出監(jiān)牢的瞬間粉臊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來泰國打工驶兜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扼仲,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓抄淑,卻偏偏與公主長得像屠凶,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子肆资,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理矗愧,服務(wù)發(fā)現(xiàn),斷路器郑原,智...
    卡卡羅2017閱讀 134,672評(píng)論 18 139
  • 用兩張圖告訴你贱枣,為什么你的 App 會(huì)卡頓? - Android - 掘金 Cover 有什么料监署? 從這篇文章中你...
    hw1212閱讀 12,732評(píng)論 2 59
  • 說明:本文很多觀點(diǎn)和內(nèi)容來自互聯(lián)網(wǎng)以及各種資料,如果侵犯了您的權(quán)益纽哥,請(qǐng)及時(shí)聯(lián)系我钠乏,我會(huì)刪除相關(guān)內(nèi)容。 權(quán)限管理 基...
    寇寇寇先森閱讀 7,595評(píng)論 8 76
  • 這仿佛是一個(gè)帶著西瓜味的開始春塌,清甜多汁晓避,嘴角還縈繞著冰西瓜的味道,案板上還剩一塊紅西瓜只壳。 暑熱還...
    初璴閱讀 153評(píng)論 0 0
  • 【生吼句,不能選擇重新來過锅必,就讓今生一直美麗。愛了惕艳,恨了搞隐,怨了,想了……远搪,當(dāng)一切思想靜止的一刻劣纲,生命的花也會(huì)在美麗中凋...
    2016冰山來客閱讀 200評(píng)論 2 2