Springboot2集成Shiro框架(三)自定義Realm

相關

  1. Springboot2集成Shiro框架(一)認識shiro
  2. Springboot2集成Shiro框架(二)自定義Filter

一、realm的作用

Realm充當了Shiro與應用安全數據間的“橋樑”或者“連接器”。也就是說,當對用戶執行認證(登錄)和授權(訪問控制)驗證時,Shiro會從應用配置的Realm中查找用戶及其權限信息。
  從這個意義上講,Realm實質上是一個安全相關的DAO:它封裝了數據源的連接細節,並在需要時將相關數據提供給Shiro。當配置Shiro時,你必須至少指定一個Realm,用於認證和(或)授權。配置多個Realm是可以的,但是至少需要一個。
  Shiro內置了可以連接大量安全數據源(又名目錄)的Realm,如LDAP、關係數據庫(JDBC)、類似INI的文本配置資源以及屬性文件等。如果缺省的Realm不能滿足需求,你還可以插入代表自定義數據源的自己的Realm實現。

二、數據庫設計

下圖是一個簡單的權限關係庫,主要是有三個表,用戶角色權限,爲了容易管理,用戶只關聯角色,而不同的角色對應不同的權限,shiro框架允許我們隨意配置權限粗細度,可以精確到角色,也可以細微至某個權限(比如:用戶添加權限等),我們需要了解的是用戶和角色是一對多的關係(一個用戶對應多個角色),而權限和角色關係亦是一對多的關係。
在這裏插入圖片描述

三、創建自定義realm

即使shiro提供瞭如jdbcrealm,jndiRealm,和使用配置文件的realm,但是實際開發中,大部分情況仍需要我們根據實際情況定製適合自己項目的realm處理器,shiro提供了 AuthorizingRealm 抽象類,創建 MyShiroRealm 類繼承 AuthorizingRealm 抽象類,重寫 doGetAuthorizationInfo 方法,和 doGetAuthenticationInfo 方法,完成自定義realm,下面是我配置的realm。

需要注意的是,doGetAuthenticationInfo 是每次登錄會調用,而 doGetAuthorizationInfo 方法則是訪問到需要權限、角色的接口的時候纔會調用!

import java.util.Set;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import cn.junengxiong.bean.User;
import cn.junengxiong.service.UserService;

/**
 * 自定義登錄權限認證
 * 
 * @ClassName: MyShiroRealm
 * @Description TODO
 * @version
 * @author jh
 * @date 2019年8月27日 下午4:12:40
 */

public class MyShiroRealm extends AuthorizingRealm {
    @Autowired
    UserService userService;

    /**
     * 權限設置
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        if (userService == null) {
            userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
        }
        System.out.println("進入自定義權限設置方法!");
        String username = (String) principals.getPrimaryPrincipal();
        // 從數據庫或換村中獲取用戶角色信息
        User user = userService.findByUsername(username);
        // 獲取用戶角色
        Set<String> roles = user.getRole();
        // 獲取用戶權限
        Set<String> permissions = user.getPermission();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        // 設置權限
        simpleAuthorizationInfo.setStringPermissions(permissions);
        // 設置角色
        simpleAuthorizationInfo.setRoles(roles);

        return simpleAuthorizationInfo;
    }

   /**
     * 身份驗證
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("進入自定義登錄驗證方法!");
        if (userService == null) {
            userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
        }
        // 通過username從數據庫中查找 User對象,如果找到,沒找到.
        // 實際項目中,這裏可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String username = usernamePasswordToken.getUsername();
        String pwd = String.valueOf(usernamePasswordToken.getPassword());// 獲取用戶輸入的密碼
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UnknownAccountException();// 用戶不存在
        }
        String password = user.getPassword();// 數據庫獲取的密碼
        // 此處對用戶輸入密碼進行加密,對比數據庫查詢出來加密後的密碼,自定義加密方式
        // ............................
        // password = new SimpleHash("MD5", password, username, 1024).toString();
        if (!password.equals(pwd)) {
            throw new IncorrectCredentialsException();// 憑證錯誤
        }
        // 主要的(可以使用戶名,也可以是用戶對象),資格證書(數據庫獲取的密碼),區域名稱(當前realm名稱)
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, getName());
        // 加鹽,使用每個用戶各自的用戶名加鹽,保證密碼相同時但是加密後密碼仍然不同,如果不適用shiro自帶憑證比較器,或者自定義憑證比較器,可以不設置加鹽
        // simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
        return simpleAuthenticationInfo;
    }


}

3.1 userService爲null問題

因爲springboot的bean注入機制,會導致在此realm生成的時候userService不會被注入,所有需要我們手動把UserService注入進來,用到了SpringBeanFactoryUtil工具類:


import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
 * 
 * @ClassName:  SpringBeanFactoryUtil   
 * @Description 手動bean注入工具類,用於解決filter中注入service爲null問題
 * @version 
 * @author jh
 * @date 2019年8月28日 上午10:29:37
 */
@Component
public class SpringBeanFactoryUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringBeanFactoryUtil.applicationContext == null) {
            SpringBeanFactoryUtil.applicationContext = applicationContext;
        }
    }

    private SpringBeanFactoryUtil() {
        // TODO Auto-generated constructor stub
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    // 根據名稱-------@Resource 註解
    public static Object getBeanByName(String name) {
        return getApplicationContext().getBean(name);
    }

    // 根據類型-------@Autowired 註解
    public static <T> T getBeanByType(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    // 根據名稱和類型--------@Autowired+@Qualifier
    public static <T> T getBeanByNameAndType(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }
}

四、把自定義的Realm交給SecurityManager

我們聲明好了自定義的realm後,需要把它交給SecurityManager來管理,需要在ShiroConfig類中,添加如下代碼

  • 返回自定義realm,放入SecurityManager
    /**
     * 自定義身份認證 realm;
     * <p>
     * 必須寫這個類,並加上 @Bean 註解,目的是注入 MyShiroRealm, 否則會影響 MyShiroRealm類 中其他類的依賴注入
     */
    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        return myShiroRealm;
    }
  • SecurityManager 配置
    /**
     * 注入 securityManager
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }

五、user類

上面已經講過了,一個用戶對應着多個角色,而一個角色對應多個權限,
爲了方便,不進行數據庫的操作,在service中僞造三個用戶,而每個用戶擁有n個角色和n個權限,我們用set集合表示,可以根據情況隨意拓展。


import java.util.Set;

public class User {
    private String username;
    private String password;
    private Set<String> role;
    private Set<String> permission;
    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;
    }

    public Set<String> getRole() {
        return role;
    }

    public void setRole(Set<String> role) {
        this.role = role;
    }

    public Set<String> getPermission() {
        return permission;
    }

    public void setPermission(Set<String> permission) {
        this.permission = permission;
    }

    @Override
    public String toString() {
        return "User [username=" + username + ", password=" + password + ", role=" + role + ", permission=" + permission
                + "]";
    }


}

六、 業務層

service

public interface UserService {
    User findByUsername(String username);
}

serviceImpl


import java.util.HashSet;
import java.util.Set;

import org.springframework.stereotype.Service;

import cn.junengxiong.bean.User;
import cn.junengxiong.service.UserService;

@Service
public class UserServiceImpl implements UserService {

    @Override
    public User findByUsername(String username) {
        User user = new User();
        user.setUsername(username);
        Set<String> roleList = new HashSet<>();
        Set<String> permissionsList = new HashSet<>();
        switch (username) {
        case "admin":
            roleList.add("admin");
            user.setPassword("admin");
            permissionsList.add("user:add");
            permissionsList.add("user:delete");
            break;
        case "consumer":
            roleList.add("consumer");
            user.setPassword("consumer");
            permissionsList.add("consumer:modify");
            break;
        default:
            roleList.add("guest");
            user.setPassword("guest");
            break;
        }
        user.setRole(roleList);
        user.setPermission(permissionsList);
        return user;
    }

}

七、controller


import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import cn.junengxiong.bean.ReturnMap;

@RestController
public class ShiroController {

    @RequestMapping("/consumer/{str}")
    @RequiresRoles(value = { "admin", "consumer" }, logical = Logical.OR)
    public ReturnMap getMessage(@PathVariable(value = "str") String str) {
        return new ReturnMap().success().data(str);
    }

    @RequestMapping("/admin/{str}")
    @RequiresRoles("admin")
    public ReturnMap getMessageAdmin(@PathVariable(value = "str") String str) {
        return new ReturnMap().success().data(str);
    }

    @RequestMapping("/guest/{str}")
    public ReturnMap getMessageGuest(@PathVariable(value = "str") String str) {
        return new ReturnMap().success().data(str);
    }

    @RequestMapping("/login")
    public ReturnMap login(String username, Boolean rememberMe, String password) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        if (rememberMe != null) {
            token.setRememberMe(rememberMe);
        }
        try {
            // 登錄
            subject.login(token);
        } catch (UnknownAccountException uae) {
            // 用戶名未知...
            return new ReturnMap().fail().message("用戶不存在!");
        } catch (IncorrectCredentialsException ice) {
            // 憑據不正確,例如密碼不正確 ...
            return new ReturnMap().fail().message("密碼不正確!");
        } catch (LockedAccountException lae) {
            // 用戶被鎖定,例如管理員把某個用戶禁用...
            return new ReturnMap().fail().message("用戶被鎖定!");
        } catch (ExcessiveAttemptsException eae) {
            // 嘗試認證次數多餘系統指定次數 ...
            return new ReturnMap().fail().message("嘗試認證次數過多,請稍後重試!");
        } catch (AuthenticationException ae) {
            // 其他未指定異常
            return new ReturnMap().fail().message("未知異常!");
        }
        return new ReturnMap().success().data("登錄成功!");
    }

    @RequestMapping("/loginout")
    public ReturnMap getMessageGuest() {
        Subject subject = SecurityUtils.getSubject();
        // 登出
        subject.logout();
        return new ReturnMap().success().message("登出成功!");
    }

    /**
     * 無權限訪問時
     * 
     * @return
     */
    @RequestMapping("/unauthorized")
    public ReturnMap unauthorized() {
        return new ReturnMap().invalid();
    }
}

八、添加對權限註解支持

這一步卡了我一些時間,剛開始接觸shiro框架,但是加入權限控制註解,每次只會在登錄的時候調用登錄驗證接口,但是訪問有權限的接口時,並不會進入自定義realm中的 doGetAuthorizationInfo 方法,經查詢,該方法只會在用戶訪問到需要權限的接口的時候纔會調用,如果未使用緩存,則每次訪問權限接口均會調用一次 doGetAuthorizationInfo 方法,另外權限註解需要加入兩個配置項來支持,在shiroConfig配置類中添加如下代碼,啓動對權限註解的支持。

class ShiroConfig
....................
 /**
     * 開啓Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證
     * 配置以下兩個bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可實現此功能
     * 
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 開啓aop註解支持
     * 
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

九、補充

在學習shiro中查看了很多資料,博客,發現一些教學中會在ShiroConfig配置中添加如下配置

    /**
     * 配置Shiro生命週期處理器
     * 
     * @return
     */
//    @Bean(name = "lifecycleBeanPostProcessor")
//    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
//        return new LifecycleBeanPostProcessor();
//    }

但是,如果配合spring食用時,是無需配置此項的,可以查看
* shiro-spring-config.ShiroBeanConfiguration文件,此配置已經幫我們配置好了

十、測試頁面

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登錄</title>
</head>

<body>
	<table>
		<tr>
			<td>請輸入姓名</td>
			<td><input type="text" name="username" id="username" /></td>
		</tr>
		<tr>
			<td>請輸入密碼</td>
			<td><input type="password" name="password" id="password" /></td>
		</tr>
		<tr>
			<td>記住我</td>
			<td><input type="checkbox" name="rememberMe" id="rememberMe" /></td>
		</tr>
	</table>
	<button type="button" onclick="login()">登錄</button>
	<button type="button" onclick="loginout()">登出</button>
	<table>
		<tr>
		  <td><button onclick="authority('/admin/可以訪問admin')">admin 管理員接口</button></td>
		  <td><button onclick="authority('/consumer/可以訪問consumer')">consumer 用戶接口</button></td>
		  <td><button onclick="authority('/guest/可以訪問guest')">guest 訪客接口</button></td>
		</tr>
	</table>
	<textarea rows="5" cols="60" id="textarea"></textarea>
	<script src="https://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
</body>
<script type="text/javascript">
	function login() {//登錄
		var username = $('#username').val();
		var password = $('#password').val();
		var flag = $('#rememberMe').prop('checked');
		console.log(flag)
		$.ajax({
			url : '/login',
			data : {
				'username' : username,
				'password' : password,
				'rememberMe' : flag
			},
			success : function(res) {
				console.log(res)
				$('#textarea').val(JSON.stringify(res));
			}
		})
	}
	function loginout() {//登出
		$.ajax({
			url : '/loginout',
			success : function(res) {
				$('#textarea').val(JSON.stringify(res));
			}
		})
	}
	//權限測試
	function authority(url){
		$.ajax({
            url : url,
            success : function(res) {
                $('#textarea').val(JSON.stringify(res));
            }
        })
	}
</script>
</html>

十一、測試結果

下面是這次的controller配置,可以看到

  • admin可以訪問所有接口
  • consumer可以訪問本身和guest接口
  • guest只可以訪問自己的接口
    在這裏插入圖片描述

進行測試

1.admin登錄,進入登錄認證方法,登錄成功!
在這裏插入圖片描述
2. 測試管理員接口,進入權限驗證方法,權限驗證通過在這裏插入圖片描述
3. consumer接口測試,可以看到,因爲此接口設置了允許兩種角色訪問,shiro訪問了兩次權限認證方法在這裏插入圖片描述
4. guest接口測試,可以看到,在沒有加權限註解時,shiro是不會訪問權限認證方法的在這裏插入圖片描述
5. 換一個角色嘗試一下,首先登錄成功!在這裏插入圖片描述
6. 訪問admin接口,得到無權限訪問,可以看到後臺進行權限驗證後拋出了 org.apache.shiro.authz.UnauthorizedException: Subject does not have role [admin] 權限驗證異常:缺少角色admin,成功攔截訪問!在這裏插入圖片描述
7. 訪問自己的consumer接口,可以訪問,並且仍然進行了兩次權限認證方法,即使我在註解上面填寫所需角色爲or關係也不行,有點像 非短路與 的意思,這裏猜想如果填寫三個是不是訪問三次。。。。。。在這裏插入圖片描述
8. 馬上更改代碼嘗試,在consumer接口新增可訪問角色‘superman’,直接點擊訪問執行了三次權限認證,猜想成功!在這裏插入圖片描述在這裏插入圖片描述

十二、源碼

如果需要源碼可以在下面地址得到,如果幫助到了你可以

      ."`".
  .-./ _=_ \.-.
 {  (,(oYo),) }}
 {{ |   "   |} }
 { { \(---)/  }}
 {{  }'-=-'{ } }
 { { }._:_.{  }}
 {{  } -:- { } }
 {_{ }`===`{  _}
((((\)     (/))))

springboot 整合Shiro

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章