SpringBoot:集成Shiro之自定義Realm實(shí)現(xiàn)認(rèn)證授權(quán)


前言


前面的兩篇博客使用了INI的形式完成了用戶的認(rèn)證授權(quán)操作.我曾經(jīng)多次在博客中提到過INI文件形式進(jìn)行認(rèn)證授權(quán)只適用于用戶較少的情況下,但是,當(dāng)用戶較多的情況下,我們可能需要數(shù)據(jù)庫來管理,這時(shí)候,我們就需要自定義Realm了. 接下來,我們來看一下如何使用自定義的Realm實(shí)現(xiàn)認(rèn)證授權(quán)操作.


自定義Realm的繼承與創(chuàng)建


前面我們說到我們要自定義Realm,首先我們需要先確定我們定義的Realm類中所需要的功能都需要什么,我們需要緩存功能,認(rèn)證功能,授權(quán)功能,三大功能 .我們首先看一下INiRealm的繼承圖,從中選出最適合的繼承父類,如下圖所示.


通過上圖我們可以確定出 AuthorizingRealm具有我們所需要的所有功能,所以我們只需要繼承于 AuthorizingRealm來實(shí)現(xiàn)我們的Realm子類即可.

  • 首先我們創(chuàng)建出一個(gè)類繼承于AuthorizingRealm,代碼如下所示.
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class MyRealm extends AuthorizingRealm {


}

創(chuàng)建好的類我們會(huì)發(fā)現(xiàn)他處于報(bào)錯(cuò)狀態(tài),如下圖所示.

這是因?yàn)槔^承于AuthorizingRealm的子類必須要實(shí)現(xiàn)認(rèn)證方法和授權(quán)方法.我們用Alt +Enter快速創(chuàng)建這兩個(gè)方法.

其中 doGetAuthenticationInfo為認(rèn)證方法,doGetAuthorizationInfo為授權(quán)方法.代碼如下所示.

public class MyRealm extends AuthorizingRealm {

    @Override
    public String getName() {
        return "MyRealm";
    }

    //授權(quán)方法
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    //認(rèn)證方法
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        return null;
    }
}


自定義Realm的連接使用


上一個(gè)模塊我們已經(jīng)把我們自定義的Realm創(chuàng)建出來了,在編寫具體的認(rèn)證授權(quán)邏輯代碼之前,我們要先把我們Realm注入到我們的工程中,這里有兩種方式.一種是INI文件注入,例外一種就是傳統(tǒng)的代碼注入Bean.下面我們分別來看一下我們使用這兩種方式.

首先,我們看一下如何使用INI文件的形式注入我們自定義的Realm.這時(shí)候我們可能就需要用到INI文件中的[main]模塊了,具體INI文件的配置可以看我前面的SpringBoot:集成Shiro之INI配置篇.

First

我們新建一個(gè)INI文件.取名叫shiro-realm.ini.(不做任何設(shè)置的話,項(xiàng)目加載的是在resources目錄下或者是resources/META-INF目錄下的shiro.ini文件,這里因?yàn)槲乙鲆粋€(gè)整體的Demo,所以就寫兩個(gè)INI文件作為區(qū)分了.) 結(jié)構(gòu)如下圖所示.


?
接下來,我們就配置我們的INI文件[main]模塊的內(nèi)容,這里我們只需要使用Shiro的自定義Realm功能,所以代碼如下所示.

[main]
#定義Realm
myRealm = com.dong.shiro.MyRealm
#配置Realm
securityManager.realms = $myRealm

然后我們接下來就需要和SpringBoot:集成Shiro之INI認(rèn)證篇中的配置過程一樣,通過INI初始化我們的SecurityManager對(duì)象.其他代碼一致即可.

        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-realm.ini");

整體代碼如下所示.

        //初始化SecurityManager對(duì)象 使用INI文件進(jìn)行自定義Realm的設(shè)置
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-realm.ini");

        //通過SecurityManager工廠對(duì)象,獲取SecurityManager實(shí)例對(duì)象.
        SecurityManager securityManager =  factory.getInstance();

        // 把 securityManager 實(shí)例 綁定到 SecurityUtils
        SecurityUtils.setSecurityManager(securityManager);

        //組建Subject主體.
        Subject subject = SecurityUtils.getSubject();

        //創(chuàng)建 token 令牌
        UsernamePasswordToken token = new UsernamePasswordToken(userName,passWord);

        //用戶登錄操作.
        try{
            subject.login(token);
            resultMap.put("code","200");
            resultMap.put("msg","用戶登錄成功");
        }catch (AuthenticationException e){
            //登錄失敗原因 1 用戶不存在 2 用戶密碼不正確
            resultMap.put("code","-1");
            resultMap.put("msg","用戶登錄失敗");
        }


Second

第二種方式,則是使用代碼的形式注入自定義的Realm,相比于第一種而言,較為麻煩一下,雖然我編寫的項(xiàng)目中使用的是代碼注入的形式.但是不得不說第一種形式很是方便簡單.大家酌情區(qū)分使用這兩種情況.廢話不多說,我們看下代碼注入的形式是如何實(shí)現(xiàn)的.

首先我們需要?jiǎng)?chuàng)建一個(gè)Shiro的配置類ShiroConfiguration .代碼如下所示(代碼是由我從項(xiàng)目中直接拷貝而來,可能會(huì)多很多的import).當(dāng)然了,我們需要確定這個(gè)配置類能被啟動(dòng)類加載到.

import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;

@Configuration
public class ShiroConfiguration {

}

然后我們需要配置核心安全事務(wù)管理器和配置自定義的權(quán)限登錄器兩大模塊,代碼如下所示.

    //配置核心安全事務(wù)管理器
    @Bean(name="securityManager")
    public DefaultWebSecurityManager securityManager(@Qualifier("myShiroRealm") MyShiroRealm myShiroRealm) {
        DefaultWebSecurityManager manager=new DefaultWebSecurityManager();
        manager.setRealm(myShiroRealm);
        return manager;
    }
    //配置自定義的權(quán)限登錄器
    @Bean(name="myShiroRealm")
    public MyRealm authRealm() {
        MyRealm myShiroRealm=new MyRealm();
        return myShiroRealm;
    }

這樣我們就完成了自定義Realm類的配置.整體代碼如下所示.

@Configuration
public class ShiroConfiguration {
    //配置核心安全事務(wù)管理器
    @Bean(name="securityManager")
    public DefaultWebSecurityManager securityManager(@Qualifier("myShiroRealm") MyShiroRealm myShiroRealm) {
        DefaultWebSecurityManager manager=new DefaultWebSecurityManager();
        manager.setRealm(myShiroRealm);
        return manager;
    }
    //配置自定義的權(quán)限登錄器
    @Bean(name="myShiroRealm")
    public MyRealm authRealm() {
        MyRealm myShiroRealm=new MyRealm();
        return myShiroRealm;
    }
}

看完這個(gè)模塊大家是不是覺得第一種形式更加的簡潔方便呢?


自定義Realm的認(rèn)證邏輯


通過上面的兩種方式,我們已經(jīng)可以把我們自定義的Realm注入到Bean中了,下面我們就看一下,如何使用自定義Realm連接數(shù)據(jù)庫完成認(rèn)證過程.

我們前面說過,認(rèn)證過程是在doGetAuthenticationInfo方法中實(shí)現(xiàn)的,我們看到有個(gè)AuthenticationToken類型的參數(shù),我們就可以通過這個(gè)參數(shù)進(jìn)行用戶名稱的獲取.如下所示.

        //通過token獲取用戶賬號(hào)
        String userName = (String)authenticationToken.getPrincipal();

當(dāng)我們得到了用戶名稱,我們就可以通過用戶名稱查詢數(shù)據(jù)庫.那么就會(huì)出現(xiàn)用戶存在和不存在,密碼正確和不正確四種情況.模擬查詢數(shù)據(jù)庫代碼如下所示.

       //模擬查詢數(shù)據(jù)庫(假數(shù)據(jù))
        String password = null;
        if (userName.equals("admin")){
            password = "admin";
        }else {
            return null;
        }

那么我們查詢出password該怎么使用呢?我們看到doGetAuthenticationInfo方法返回值是實(shí)現(xiàn)AuthenticationInfo接口的類型,如果返回為null值,則表示用戶不存在,而密碼的正確與否需要進(jìn)一步的判斷.

接下來,我們需要使用SimpleAuthenticationInfo(實(shí)現(xiàn)了AuthenticationInfo接口)這個(gè)類組裝返回值,它的構(gòu)造方法需要三個(gè)值,分別是賬號(hào),密碼,以及當(dāng)前Realm的名稱. 所以,代碼如下所示.

        //模擬查詢數(shù)據(jù)庫(假數(shù)據(jù))
        String password = null;
        if (userName.equals("admin")){
            password = "admin";
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userName, password, getName());
            return simpleAuthenticationInfo;

        }else {
            return null;
        }

這時(shí)候,我們重新編寫一下UserLoginController,驗(yàn)證當(dāng)賬號(hào)為admin,密碼為admin時(shí)是否能夠通過.接口方法如下所示.

   @RequestMapping(value = "/realmLogin",method = RequestMethod.POST)
    public Map<String,Object> userLoginWithRealmAction (@RequestParam(value = "userName") String userName,
                                               @RequestParam(value = "password") String password){

        Map<String,Object> resultMap = myShiro.userLoginActionWithMyRealm(userName,password);

        return resultMap;

    }

使用PostMan驗(yàn)證截圖如下所示.

驗(yàn)證成功
驗(yàn)證失敗


自定義Realm的授權(quán)邏輯


上一個(gè)模塊我們已經(jīng)實(shí)現(xiàn)數(shù)據(jù)庫用戶通過自定義Realm進(jìn)行了登錄認(rèn)證.那么,我們?cè)撊绾螌?duì)已經(jīng)登錄的用戶進(jìn)行授權(quán)操作呢?這時(shí)候,我們需要對(duì)自定義Realm中的doGetAuthorizationInfo方法進(jìn)行編寫了.

和認(rèn)證過程中返回值一樣,假設(shè)返回為null,則沒有任何權(quán)限和角色設(shè)置.我們看到doGetAuthorizationInfo方法有個(gè)principalCollection參數(shù),principalCollection參數(shù)是用戶的驗(yàn)證信息的封裝參數(shù).所以我們需要通過這個(gè)參數(shù)拿到用戶賬號(hào)信息,代碼如下所示.

        String userName = (String) principalCollection.getPrimaryPrincipal();

緊接著我們就去查詢數(shù)據(jù)庫的用戶角色和權(quán)限,假設(shè)admin用戶擁有superAdmin角色和add權(quán)限.那么,我們?cè)撊绾尾僮髂?代碼如下所示.

        String userName = (String) principalCollection.getPrimaryPrincipal();

        if (userName.equals("admin")){

            List<String> permissions=new ArrayList<>();
            List<String> roles =new ArrayList<>();
            permissions.add("add");
            roles.add("superAdmin");

        }else {

            return null;

        }

然后我們?nèi)缤弦粋€(gè)模塊一樣組裝返回?cái)?shù)據(jù),這里我們需要使用到實(shí)現(xiàn)AuthorizationInfo接口的SimpleAuthorizationInfo,然后我們把組裝的角色List和權(quán)限List添加到SimpleAuthorizationInfo中去,完成返回?cái)?shù)據(jù)的組裝.所以doGetAuthorizationInfo的整體代碼如下所示.

    //授權(quán)方法
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        String userName = (String) principalCollection.getPrimaryPrincipal();

        if (userName.equals("admin")){

            List<String> permissions=new ArrayList<>();
            List<String> roles =new ArrayList<>();
            permissions.add("add");
            roles.add("superAdmin");
            //組裝返回?cái)?shù)據(jù)
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            simpleAuthorizationInfo.addRoles(roles);
            simpleAuthorizationInfo.addStringPermissions(permissions);
            return simpleAuthorizationInfo;
        }else {
            return null;
        }
    }

接下來,我們繼續(xù)編寫MyShiro這個(gè)類的代碼.原始代碼如下所示.

import org.apache.commons.collections.ArrayStack;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.springframework.stereotype.Component;

import java.sql.Array;
import java.util.*;


@Component
public class MyShiro {
    public Map<String,Object> userLoginActionWithMyRealm (String userName,String passWord){

        Map<String,Object> resultMap = new HashMap<>();

        //初始化SecurityManager對(duì)象 使用INI文件進(jìn)行自定義Realm的設(shè)置
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-realm.ini");

        //通過SecurityManager工廠對(duì)象,獲取SecurityManager實(shí)例對(duì)象.
        SecurityManager securityManager =  factory.getInstance();

        // 把 securityManager 實(shí)例 綁定到 SecurityUtils
        SecurityUtils.setSecurityManager(securityManager);

        //組建Subject主體.
        Subject subject = SecurityUtils.getSubject();

        //創(chuàng)建 token 令牌
        UsernamePasswordToken token = new UsernamePasswordToken(userName,passWord);

        //用戶登錄操作.
        try{
            subject.login(token);
            resultMap.put("code","200");
            resultMap.put("msg","用戶登錄成功");

        }catch (AuthenticationException e){
            //登錄失敗原因 1 用戶不存在 2 用戶密碼不正確
            resultMap.put("code","-1");
            resultMap.put("msg","用戶登錄失敗");
        }
        return resultMap;

    }
}

我們?cè)谟脩舻卿浤K(如下圖位置.)來驗(yàn)證用戶是否具有相應(yīng)的權(quán)限和角色.


驗(yàn)證代碼類似SpringBoot:集成Shiro之INI授權(quán)篇中的驗(yàn)證過程,這里就不過多啰嗦,代碼如下所示.

        //用戶登錄操作.
        try{
            subject.login(token);
            resultMap.put("code","200");
            resultMap.put("msg","用戶登錄成功");

            if (subject.isPermitted("add")){
                resultMap.put("PermittedMsg","用戶擁有add權(quán)限");
            }else {
                resultMap.put("PermittedMsg","用戶未擁有add權(quán)限");
            }
            if (subject.hasRole("superAdmin")){
                resultMap.put("roleMsg","用戶擁有superAdmin角色");
            }else {
                resultMap.put("roleMsg","用戶未擁有superAdmin角色");
            }

        }catch (AuthenticationException e){
            //登錄失敗原因 1 用戶不存在 2 用戶密碼不正確
            resultMap.put("code","-1");
            resultMap.put("msg","用戶登錄失敗");
        }

我們修改下認(rèn)證過程,讓root用戶通過認(rèn)證,但是沒有角色和權(quán)限.MyRealm中doGetAuthenticationInfo中代碼如下所示.

    //認(rèn)證方法
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        //通過token獲取用戶賬號(hào)
        String userName = (String)authenticationToken.getPrincipal();

        //模擬查詢數(shù)據(jù)庫(假數(shù)據(jù))
        String password = null;
        if (userName.equals("admin") || userName.equals("root")){
            password = "admin";
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userName, password, getName());
            return simpleAuthenticationInfo;
        }else {
            return null;
        }

    }

這時(shí)候,我們使用PostMan驗(yàn)證我們的代碼實(shí)現(xiàn)是否可行.驗(yàn)證截圖如下所示,證明其可行.

驗(yàn)證admin用戶擁有權(quán)限和角色
驗(yàn)證root用戶沒有權(quán)限和角色


結(jié)語


自定義Realm的實(shí)現(xiàn)已經(jīng)可以實(shí)現(xiàn)數(shù)據(jù)庫用戶的認(rèn)證授權(quán)了,下一篇博客我們將看一下如何使用Shiro的攔截器相關(guān)內(nèi)容,讓Shiro的認(rèn)證授權(quán)發(fā)揮出真正的功能,如果有任何問題,歡迎在評(píng)論區(qū)留言,我們一起探討.歡迎繼續(xù)關(guān)注騷棟,謝謝!

Demo傳送門


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末屹电,一起剝皮案震驚了整個(gè)濱河市载萌,隨后出現(xiàn)的幾起案子壮啊,更是在濱河造成了極大的恐慌坎藐,老刑警劉巖遇西,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灭衷,死亡現(xiàn)場(chǎng)離奇詭異次慢,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門迫像,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拭抬,“玉大人,你說我怎么就攤上這事侵蒙≡旎ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵纷闺,是天一觀的道長算凿。 經(jīng)常有香客問我,道長犁功,這世上最難降的妖魔是什么氓轰? 我笑而不...
    開封第一講書人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮浸卦,結(jié)果婚禮上署鸡,老公的妹妹穿的比我還像新娘。我一直安慰自己限嫌,他們只是感情好靴庆,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著怒医,像睡著了一般炉抒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上稚叹,一...
    開封第一講書人閱讀 51,554評(píng)論 1 305
  • 那天焰薄,我揣著相機(jī)與錄音,去河邊找鬼扒袖。 笑死塞茅,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的季率。 我是一名探鬼主播野瘦,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼蚀同!你這毒婦竟也來了缅刽?” 一聲冷哼從身側(cè)響起啊掏,我...
    開封第一講書人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤蠢络,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后迟蜜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刹孔,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了髓霞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卦睹。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖方库,靈堂內(nèi)的尸體忽然破棺而出结序,到底是詐尸還是另有隱情,我是刑警寧澤纵潦,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布徐鹤,位于F島的核電站,受9級(jí)特大地震影響邀层,放射性物質(zhì)發(fā)生泄漏返敬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一寥院、第九天 我趴在偏房一處隱蔽的房頂上張望劲赠。 院中可真熱鬧,春花似錦秸谢、人聲如沸凛澎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽预厌。三九已至,卻和暖如春元媚,著一層夾襖步出監(jiān)牢的瞬間轧叽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來泰國打工刊棕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留炭晒,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓甥角,卻偏偏與公主長得像网严,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子嗤无,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

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