springboot 2.x 整合 shiro 許可權框架

MobiusStrip發表於2020-09-04

在實際專案中,經常需要用到角色許可權區分,以此來為不同的角色賦予不同的權利,分配不同的任務。比如,普通使用者只能瀏覽;會員可以瀏覽和評論;超級會員可以瀏覽、評論和看視訊課等;實際應用場景很多。毫不誇張的說,幾乎每個完整的專案都會設計到許可權管理。

在 Spring Boot 中做許可權管理,一般來說,主流的方案是 Spring Security ,但是由於 Spring Security 過於龐大和複雜,只要能滿足業務需要,大多數公司還是會選擇 Apache Shiro 來使用。

一般來說,Spring Security 和 Shiro 的區別如下:

Spring SecurityApache Shiro
重量級的安全管理框架輕量級的安全管理框架
概念複雜,配置繁瑣概念簡單、配置簡單
功能強大功能簡單

因此,這篇文章,阿淼首先會帶大家瞭解 Apache Shiro ,然後和大家一起將 shiro 許可權框架整合到 SpringBoot 中,以達到快速的實現整合許可權管理的功能。

走進 Apache Shiro

官網認知

照例又去官網扒了扒介紹:

Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
Apache Shiro™是一個強大且易用的Java安全框架,能夠用於身份驗證、授權、加密和會話管理。Shiro擁有易於理解的API,您可以快速、輕鬆地獲得任何應用程式——從最小的移動應用程式到最大的網路和企業應用程式。

簡而言之,Apache Shiro 是一個強大靈活的開源安全框架,可以完全處理身份驗證、授權、加密和會話管理。

Shiro能到底能做些什麼呢?

  • 驗證使用者身份
  • 使用者訪問許可權控制,比如:1、判斷使用者是否分配了一定的安全形色。2、判斷使用者是否被授予完成某個操作的許可權
  • 在非 Web 或 EJB 容器的環境下可以任意使用Session API
  • 可以響應認證、訪問控制,或者 Session 生命週期中發生的事件
  • 可將一個或以上使用者安全資料來源資料組合成一個複合的使用者 “view”(檢視)
  • 支援單點登入(SSO)功能
  • 支援提供“Remember Me”服務,獲取使用者關聯資訊而無需登入
    ···

為什麼今天還要使用Apache Shiro?

對此,官方給出了詳細的解釋:http://shiro.apache.org/

自2003年以來,框架環境發生了很大變化,因此今天仍然有充分的理由使用Shiro。實際上有很多原因。Apache Shiro是:

  • 易於使用 -易於使用是該專案的最終目標。應用程式安全性可能非常令人困惑和沮喪,並被視為“必要的邪惡”。如果您使它易於使用,以使新手程式設計師可以開始使用它,那麼就不必再痛苦了。
  • 全面 -Apache Shiro聲稱沒有其他具有範圍廣度的安全框架,因此它可能是滿足安全需求的“一站式服務”。
  • 靈活 -Apache Shiro可以在任何應用程式環境中工作。儘管它可以在Web,EJB和IoC環境中執行,但並不需要它們。Shiro也不要求任何規範,甚至沒有很多依賴性。
  • 具有Web功能 -Apache Shiro具有出色的Web應用程式支援,使您可以基於應用程式URL和Web協議(例如REST)建立靈活的安全策略,同時還提供一組JSP庫來控制頁面輸出。
  • 可插拔 -Shiro乾淨的API和設計模式使它易於與許多其他框架和應用程式整合。您會看到Shiro與Spring,Grails,Wicket,Tapestry,Mule,Apache Camel,Vaadin等框架無縫整合。
  • 受支援 -Apache Shiro是Apache Software Foundation(Apache軟體基金會)的一部分,該組織被證明以其社群的最大利益行事。專案開發和使用者群體友好的公民隨時可以提供幫助。如果需要,像Katasoft這樣的商業公司也可以提供專業的支援和服務。

Shiro 核心概念

Apache Shiro 是一個全面的、蘊含豐富功能的安全框架。

下圖為描述 Shiro 功能的框架圖:

圖片來源於網路

如圖所示,功能包括:

  • Authentication(認證):使用者身份識別,通常被稱為使用者“登入”
  • Authorization(授權):訪問控制。比如某個使用者是否具有某個操作的使用許可權。
  • Session Management(會話管理):特定於使用者的會話管理,甚至在非web 或 EJB 應用程式。
  • Cryptography(加密):在對資料來源使用加密演算法加密的同時,保證易於使用。

並且 Shiro 還有通過增加其他的功能來支援和加強這些不同應用環境下安全領域的關注點。

特別是對以下的功能支援:

  • Web支援:Shiro 提供的 Web 支援 api ,可以很輕鬆的保護 Web 應用程式的安全。
  • 快取:快取是 Apache Shiro 保證安全操作快速、高效的重要手段。
  • 併發:Apache Shiro 支援多執行緒應用程式的併發特性。
  • 測試:支援單元測試和整合測試,確保程式碼和預想的一樣安全。
  • “Run As”:這個功能允許使用者假設另一個使用者的身份(在許可的前提下)。
  • “Remember Me”:跨 session 記錄使用者的身份,只有在強制需要時才需要登入。

注意: Shiro 不會去維護使用者、維護許可權,這些需要我們自己去設計/提供,然後通過相應的介面注入給 Shiro

使用案例 Demo

1.新建 maven 專案

為方便我們初始化專案,Spring Boot給我們提供一個專案模板生成網站。

  • 1、開啟瀏覽器,訪問:https://start.spring.io/
  • 2、根據頁面提示,選擇構建工具,開發語言,專案資訊等。

2.匯入 springboot 父依賴

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.0.2.RELEASE</version>
</parent>

3.相關 jar 包

web 包

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

shiro-spring 包就是此篇文章的核心

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.4.0</version>
</dependency>
shiro 註解會用到 aop
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
資料庫相關包使用的是mybatisplus
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid</artifactId>
	<version>1.1.12</version>
</dependency>
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-boot-starter</artifactId>
	<version>3.1.0</version>
</dependency>
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-generator</artifactId>
	<version>3.1.0</version>
</dependency>
	<dependency>
	<groupId>org.apache.velocity</groupId>
	<artifactId>velocity-engine-core</artifactId>
	<version>2.0</version>
</dependency>

4.資料庫

建表語句在專案中有,專案地址: https://github.com/mmzsblog/mmzsblog-util

5.自定義 realm

public class MyShiroRealm extends AuthorizingRealm {
	@Autowired
	private UserService userService;
	@Autowired
	private RoleService roleService;
	@Autowired
	private PermissionService permissionService;

	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

		SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
		// HttpServletRequest request = (HttpServletRequest) ((WebSubject) SecurityUtils
		// .getSubject()).getServletRequest();//這個可以用來獲取在登入的時候提交的其他額外的引數資訊
		String username = (String) principals.getPrimaryPrincipal();
		// 受理許可權
		// 角色
		Set<String> roles = new HashSet<String>();
		Role role = roleService.getRoleByUserName(username);
		System.out.println(role.getRoleName());
		roles.add(role.getRoleName());
		authorizationInfo.setRoles(roles);
		// 許可權
		Set<String> permissions = new HashSet<String>();
		List<Permission> querypermissions = permissionService.getPermissionsByRoleId(role.getId());
		for (Permission permission : querypermissions) {
			permissions.add(permission.getPermissionName());
		}
		authorizationInfo.setStringPermissions(permissions);
		return authorizationInfo;
	}

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
			throws AuthenticationException {
		String loginName = (String) authcToken.getPrincipal();
		// 獲取使用者密碼
		User user = userService.getOne(new QueryWrapper<User>().eq("username", loginName));
		if (user == null) {
			// 沒找到帳號
			throw new UnknownAccountException();
		}
		String password = new String((char[]) authcToken.getCredentials());
		String inpass = (new Md5Hash(password, user.getUsername())).toString();
		if (!user.getPassword().equals(inpass)) {
			throw new IncorrectCredentialsException();
		}
		// 交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配
		SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(loginName, user.getPassword(),
				ByteSource.Util.bytes(loginName), getName());

		return authenticationInfo;
	}

}

6.shiro 配置類

@Configuration
public class ShiroConfiguration {
	private static final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class);

	/**
	 * Shiro的Web過濾器Factory 命名:shiroFilter
	 */
	@Bean(name = "shiroFilter")
	public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
		// Shiro的核心安全介面,這個屬性是必須的
		shiroFilterFactoryBean.setSecurityManager(securityManager);
		//需要許可權的請求,如果沒有登入則會跳轉到這裡設定的url
		shiroFilterFactoryBean.setLoginUrl("/login.html");
		//設定登入成功跳轉url,一般在登入成功後自己程式碼設定跳轉url,此處基本沒用
		shiroFilterFactoryBean.setSuccessUrl("/main.html");
		//設定無許可權跳轉介面,此處一般不生效,一般自定義異常
		shiroFilterFactoryBean.setUnauthorizedUrl("/error.html");
		Map<String, Filter> filterMap = new LinkedHashMap<>();
		// filterMap.put("authc", new AjaxPermissionsAuthorizationFilter());
		shiroFilterFactoryBean.setFilters(filterMap);
		/*
		 * 定義shiro過濾鏈 Map結構
		 * Map中key(xml中是指value值)的第一個'/'代表的路徑是相對於HttpServletRequest.getContextPath()的值來的
		 * anon:它對應的過濾器裡面是空的,什麼都沒做,這裡.do和.jsp後面的*表示引數,比方說login.jsp?main這種
		 * authc:該過濾器下的頁面必須驗證後才能訪問,它是Shiro內建的一個攔截器org.apache.shiro.web.filter.authc.
		 * FormAuthenticationFilter
		 */
		Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
		/*
		 * 過濾鏈定義,從上向下順序執行,一般將 /**放在最為下邊; authc:所有url都必須認證通過才可以訪問;
		 * anon:所有url都都可以匿名訪問
		 */
		filterChainDefinitionMap.put("/login.html", "authc");
		filterChainDefinitionMap.put("/login", "anon");
		filterChainDefinitionMap.put("/js/**", "anon");
		filterChainDefinitionMap.put("/css/**", "anon");
		filterChainDefinitionMap.put("/logout", "logout");
		filterChainDefinitionMap.put("/**", "authc");
		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
		return shiroFilterFactoryBean;
	}

	/**
	 * 許可權管理
	 */
	@Bean
	public SecurityManager securityManager() {
		logger.info("=======================shiro=======================");
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		securityManager.setRealm(MyShiroRealm());
		// securityManager.setRememberMeManager(rememberMeManager);
		return securityManager;
	}

	/**
	 * Shiro Realm 繼承自AuthorizingRealm的自定義Realm,即指定Shiro驗證使用者登入的類為自定義的
	 */
	@Bean
	public MyShiroRealm MyShiroRealm() {
		MyShiroRealm userRealm = new MyShiroRealm();
		userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
		return userRealm;
	}

	/**
	 * 憑證匹配器 密碼驗證
	 */
	@Bean(name = "credentialsMatcher")
	public HashedCredentialsMatcher hashedCredentialsMatcher() {
		HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
		// 雜湊演算法:這裡使用MD5演算法;
		hashedCredentialsMatcher.setHashAlgorithmName("md5");
		// 雜湊的次數,比如雜湊兩次,相當於 md5(md5(""));
		hashedCredentialsMatcher.setHashIterations(1);
		// storedCredentialsHexEncoded預設是true,此時用的是密碼加密用的是Hex編碼;false時用Base64編碼
		hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
		return hashedCredentialsMatcher;
	}

	/**
	 * 開啟Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證
	 */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
		AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
		authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
		return authorizationAttributeSourceAdvisor;
	}

}

7.測試類

@RestController
public class UserController {
	@PostMapping("login")
	public String name(String username, String password) {
		String result = "已登入";
		Subject currentUser = SecurityUtils.getSubject();
		UsernamePasswordToken token = new UsernamePasswordToken(username, password);
		if (!currentUser.isAuthenticated()) {
			try {
				currentUser.login(token);// 會觸發com.shiro.config.MyShiroRealm的doGetAuthenticationInfo方法
				result = "登入成功";
			} catch (UnknownAccountException e) {
				result = "使用者名稱錯誤";
			} catch (IncorrectCredentialsException e) {
				result = "密碼錯誤";
			}
		}
		return result;
	}

	@GetMapping("logout")
	public void logout() {
		Subject currentUser = SecurityUtils.getSubject();
		currentUser.logout();
	}

	@RequiresPermissions("role:update")
	@GetMapping("/role")
	public String name() {
		return "hello";
	}

	@RequiresPermissions("user:select")
	@GetMapping("/role2")
	public String permission() {
		return "hello sel";
	}

}

7.1 登入測試

資料庫賬號(密碼經過md5加鹽加密)

資料庫賬號

賬號錯誤測試

密碼錯誤測試

賬號正確測試

登入成功介面

7.2 許可權測試

許可權測試1

許可權測試2

8.說明

8.1 無許可權時的處理

無許可權時自定義了一個異常。所以,許可權測試的時候沒有許可權就會提示配置的提示語 “沒有許可權”。

@ControllerAdvice
public class ShiroException {
	@ExceptionHandler(value = UnauthorizedException.class)
	@ResponseBody
	public String name() {
		return "沒有許可權";
	}
}

8.2 角色許可權測試與許可權測試相同

許可權設定可在shiro配置類中shiro過濾鏈設定,也可用註解方式設定,本文使用註解方式。

8.3 shiro 的 session 和 cache

shiro 的 session 和 cache 管理可以自定義,本文用的是預設的,推薦自定義,方便管理。

小結

  • Apache Shiro是Java的一個安全框架
  • Shiro是一個強大的簡單易用的Java安全框架,主要用來更便捷的認證、授權、加密、會話管理、與Web整合、快取等
  • Shiro使用起來小而簡單
  • spring中有spring security ,是一個許可權框架,它和spring依賴過於緊密,沒有shiro使用簡單。
  • shiro不依賴於spring,shiro不僅可以實現web應用的許可權管理,還可以實現c/s系統,分散式系統許可權管理,
  • shiro屬於輕量框架,越來越多企業專案開始使用shiro.

參考:

相關文章