SpringBoot:整合Shiro之自定義Realm實現認證授權

前言
前面的兩篇部落格使用了INI的形式完成了使用者的認證授權操作.我曾經多次在部落格中提到過INI檔案形式進行認證授權只適用於使用者較少的情況下,但是,當用戶較多的情況下,我們可能需要資料庫來管理,這時候,我們就需要自定義Realm了. 接下來,我們來看一下如何使用自定義的Realm實現認證授權操作.
自定義Realm的繼承與建立
前面我們說到我們要自定義Realm,首先我們需要先確定我們定義的Realm類中所需要的功能都需要什麼,我們需要快取功能,認證功能,授權功能,三大功能 .我們首先看一下 INiRealm 的繼承圖,從中選出最適合的繼承父類,如下圖所示.

來實現我們的Realm子類即可.
- 首先我們創建出一個類繼承於 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 { }
建立好的類我們會發現他處於報錯狀態,如下圖所示.

這是因為繼承於 AuthorizingRealm 的子類必須要實現認證方法和授權方法.我們用 Alt +Enter 快速建立這兩個方法.

其中 doGetAuthenticationInfo 為認證方法, doGetAuthorizationInfo 為授權方法.程式碼如下所示.
public class MyRealm extends AuthorizingRealm { @Override public String getName() { return "MyRealm"; } //授權方法 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } //認證方法 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { return null; } }
自定義Realm的連線使用
上一個模組我們已經把我們自定義的Realm創建出來了,在編寫具體的認證授權邏輯程式碼之前,我們要先把我們Realm注入到我們的工程中,這裡有兩種方式.一種是INI檔案注入,例外一種就是傳統的程式碼注入Bean.下面我們分別來看一下我們使用這兩種方式.
首先,我們看一下如何使用 INI檔案 的形式注入我們自定義的Realm.這時候我們可能就需要用到 INI檔案 中的 [main] 模組了,具體 INI檔案 的配置可以看我前面的 ofollow,noindex">SpringBoot:整合Shiro之INI配置篇 .
First
我們新建一個INI檔案.取名叫 shiro-realm.ini .(不做任何設定的話,專案載入的是在resources目錄下或者是resources/META-INF目錄下的 shiro.ini 檔案,這裡因為我要做一個整體的Demo,所以就寫兩個INI檔案作為區分了.) 結構如下圖所示.

接下來,我們就配置我們的INI檔案[main]模組的內容,這裡我們只需要使用Shiro的自定義Realm功能,所以程式碼如下所示.
[main] #定義Realm myRealm = com.dong.shiro.MyRealm #配置Realm securityManager.realms = $myRealm
然後我們接下來就需要和 SpringBoot:整合Shiro之INI認證篇 中的配置過程一樣,通過INI初始化我們的SecurityManager物件.其他程式碼一致即可.
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-realm.ini");
整體程式碼如下所示.
//初始化SecurityManager物件 使用INI檔案進行自定義Realm的設定 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-realm.ini"); //通過SecurityManager工廠物件,獲取SecurityManager例項物件. SecurityManager securityManager =factory.getInstance(); // 把 securityManager 例項 繫結到 SecurityUtils SecurityUtils.setSecurityManager(securityManager); //組建Subject主體. Subject subject = SecurityUtils.getSubject(); //建立 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,相比於第一種而言,較為麻煩一下,雖然我編寫的專案中使用的是程式碼注入的形式.但是不得不說第一種形式很是方便簡單.大家酌情區分使用這兩種情況.廢話不多說,我們看下程式碼注入的形式是如何實現的.
首先我們需要建立一個Shiro的配置類 ShiroConfiguration .程式碼如下所示(程式碼是由我從專案中直接拷貝而來,可能會多很多的import).當然了,我們需要確定這個配置類能被啟動類載入到.
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 { }
然後我們需要配置核心安全事務管理器和配置自定義的許可權登入器兩大模組,程式碼如下所示.
//配置核心安全事務管理器 @Bean(name="securityManager") public DefaultWebSecurityManager securityManager(@Qualifier("myShiroRealm") MyShiroRealm myShiroRealm) { DefaultWebSecurityManager manager=new DefaultWebSecurityManager(); manager.setRealm(myShiroRealm); return manager; } //配置自定義的許可權登入器 @Bean(name="myShiroRealm") public MyRealm authRealm() { MyRealm myShiroRealm=new MyRealm(); return myShiroRealm; }
這樣我們就完成了自定義Realm類的配置.整體程式碼如下所示.
@Configuration public class ShiroConfiguration { //配置核心安全事務管理器 @Bean(name="securityManager") public DefaultWebSecurityManager securityManager(@Qualifier("myShiroRealm") MyShiroRealm myShiroRealm) { DefaultWebSecurityManager manager=new DefaultWebSecurityManager(); manager.setRealm(myShiroRealm); return manager; } //配置自定義的許可權登入器 @Bean(name="myShiroRealm") public MyRealm authRealm() { MyRealm myShiroRealm=new MyRealm(); return myShiroRealm; } }
看完這個模組大家是不是覺得第一種形式更加的簡潔方便呢?
自定義Realm的認證邏輯
通過上面的兩種方式,我們已經可以把我們自定義的Realm注入到Bean中了,下面我們就看一下,如何使用自定義Realm連線 資料庫 完成認證過程.
我們前面說過,認證過程是在 doGetAuthenticationInfo 方法中實現的,我們看到有個 AuthenticationToken 型別的引數,我們就可以通過這個引數進行使用者名稱稱的獲取.如下所示.
//通過token獲取使用者賬號 String userName = (String)authenticationToken.getPrincipal();
當我們得到了使用者名稱稱,我們就可以通過使用者名稱稱查詢資料庫.那麼就會出現使用者存在和不存在,密碼正確和不正確四種情況.模擬查詢資料庫程式碼如下所示.
//模擬查詢資料庫(假資料) String password = null; if (userName.equals("admin")){ password = "admin"; }else { return null; }
那麼我們查詢出password該怎麼使用呢?我們看到 doGetAuthenticationInfo 方法返回值是實現 AuthenticationInfo 介面的型別,如果返回為null值,則表示使用者不存在,而密碼的正確與否需要進一步的判斷.

接下來,我們需要使用 SimpleAuthenticationInfo (實現了 AuthenticationInfo 介面)這個類組裝返回值,它的構造方法需要三個值,分別是賬號,密碼,以及當前Realm的名稱. 所以,程式碼如下所示.
//模擬查詢資料庫(假資料) String password = null; if (userName.equals("admin")){ password = "admin"; SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userName, password, getName()); return simpleAuthenticationInfo; }else { return null; }
這時候,我們重新編寫一下 UserLoginController ,驗證當賬號為 admin ,密碼為 admin 時是否能夠通過.介面方法如下所示.
@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驗證截圖如下所示.

驗證成功

驗證失敗
自定義Realm的授權邏輯
上一個模組我們已經實現資料庫使用者通過自定義Realm進行了登入認證.那麼,我們該如何對已經登入的使用者進行授權操作呢?這時候,我們需要對自定義Realm中的 doGetAuthorizationInfo 方法進行編寫了.
和認證過程中返回值一樣,假設返回為null,則沒有任何許可權和角色設定.我們看到 doGetAuthorizationInfo 方法有個 principalCollection 引數, principalCollection 引數是使用者的驗證資訊的封裝引數.所以我們需要通過這個引數拿到使用者賬號資訊,程式碼如下所示.
String userName = (String) principalCollection.getPrimaryPrincipal();
緊接著我們就去查詢資料庫的使用者角色和許可權,假設admin使用者擁有superAdmin角色和add許可權.那麼,我們該如何操作呢,程式碼如下所示.
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; }
然後我們如同上一個模組一樣組裝返回資料,這裡我們需要使用到實現 AuthorizationInfo 介面的 SimpleAuthorizationInfo ,然後我們把組裝的角色List和許可權List新增到 SimpleAuthorizationInfo 中去,完成返回資料的組裝.所以 doGetAuthorizationInfo 的整體程式碼如下所示.
//授權方法 @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"); //組裝返回資料 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.addRoles(roles); simpleAuthorizationInfo.addStringPermissions(permissions); return simpleAuthorizationInfo; }else { return null; } }
接下來,我們繼續編寫MyShiro這個類的程式碼.原始程式碼如下所示.
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物件 使用INI檔案進行自定義Realm的設定 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-realm.ini"); //通過SecurityManager工廠物件,獲取SecurityManager例項物件. SecurityManager securityManager =factory.getInstance(); // 把 securityManager 例項 繫結到 SecurityUtils SecurityUtils.setSecurityManager(securityManager); //組建Subject主體. Subject subject = SecurityUtils.getSubject(); //建立 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; } }
我們在使用者登入模組(如下圖位置.)來驗證使用者是否具有相應的許可權和角色.

驗證程式碼類似 SpringBoot:整合Shiro之INI授權篇 中的驗證過程,這裡就不過多囉嗦,程式碼如下所示.
//使用者登入操作. try{ subject.login(token); resultMap.put("code","200"); resultMap.put("msg","使用者登入成功"); if (subject.isPermitted("add")){ resultMap.put("PermittedMsg","使用者擁有add許可權"); }else { resultMap.put("PermittedMsg","使用者未擁有add許可權"); } 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","使用者登入失敗"); }
我們修改下認證過程,讓root使用者通過認證,但是沒有角色和許可權.MyRealm中 doGetAuthenticationInfo 中程式碼如下所示.
//認證方法 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //通過token獲取使用者賬號 String userName = (String)authenticationToken.getPrincipal(); //模擬查詢資料庫(假資料) String password = null; if (userName.equals("admin") || userName.equals("root")){ password = "admin"; SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userName, password, getName()); return simpleAuthenticationInfo; }else { return null; } }
這時候,我們使用PostMan驗證我們的程式碼實現是否可行.驗證截圖如下所示,證明其可行.

驗證admin使用者擁有許可權和角色

驗證root使用者沒有許可權和角色
結語
自定義Realm的實現已經可以實現資料庫使用者的認證授權了,下一篇部落格我們將看一下如何使用Shiro的攔截器相關內容,讓Shiro的認證授權發揮出真正的功能,如果有任何問題,歡迎在評論區留言,我們一起探討.歡迎繼續關注騷棟,謝謝!
Demo傳送門
