前言
前面的兩篇博客使用了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)證截圖如下所示.
自定義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)證截圖如下所示,證明其可行.
結(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)注騷棟,謝謝!