1. 程式人生 > 其它 >Shiro-01-快速入門

Shiro-01-快速入門

Shiro

Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼和會話管理。

使用Shiro的易於理解的API,您可以快速、輕鬆地獲得任何應用程式,從最小的移動應用程式到最大的網路和企業應用程式。

官網:https://shiro.apache.org


1 快速入門

1.1 github 專案

首先,找到 Shiro 託管在 Github 上的原始碼:https://github.com/apache/shiro

然後開啟裡面的 samples/quickstart 資料夾,檢視 pom.xml 依賴並且根據它的依賴來配置我們自己的依賴

<dependencies>
        <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.8.0</version>
        </dependency>

        <!-- configure logging -->
        <!-- https://mvnrepository.com/artifact/org.slf4j/jcl-over-slf4j -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.25</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/log4j/log4j -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
</dependencies>

然後可以看到示例專案的資料夾下的兩個配置檔案,一個是 log4j 的,一個是 shiro 的:

複製這兩個檔案的內容到自己的專案下,然後將 QuickStart 類複製到自己的專案下:

其中可能要修改部分 import 的程式碼,主要是:

import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.util.Factory;

這兩個 import 語句。注意將 pom 檔案中的 scope 設定成 runtime 或者乾脆去掉,執行主程式:

2021-09-19 22:08:32,696 INFO [org.apache.shiro.session.mgt.AbstractValidatingSessionManager] - Enabling session validation scheduler... 
2021-09-19 22:08:33,057 INFO [Quickstart] - Retrieved the correct value! [aValue] 
2021-09-19 22:08:33,058 INFO [Quickstart] - User [lonestarr] logged in successfully. 
2021-09-19 22:08:33,059 INFO [Quickstart] - May the Schwartz be with you! 
2021-09-19 22:08:33,059 INFO [Quickstart] - You may use a lightsaber ring.  Use it wisely. 
2021-09-19 22:08:33,059 INFO [Quickstart] - You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  Here are the keys - have fun! 

1.2 QuickStart 解讀

我們慢慢地看這些程式碼:

具體程式碼:

public static void main(String[] args) {

		Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
		SecurityManager securityManager = factory.getInstance();
		SecurityUtils.setSecurityManager(securityManager);

		// 獲取當前使用者物件 subject
		Subject currentUser = SecurityUtils.getSubject();

		// 通過當前使用者拿到 session
		Session session = currentUser.getSession();
		session.setAttribute("someKey", "你好,世界!");
		String value = (String) session.getAttribute("someKey");
		if (value.equals("你好,世界!")) {
			log.info("得到了正確的結果! [" + value + "]");
		}


		// 判斷當前的使用者是否被認證
		if (!currentUser.isAuthenticated()) {
			// new 一個 token,通過使用者名稱和密碼
			UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
			token.setRememberMe(true);	// 設定 記住我
			try {
				currentUser.login(token);	// 執行登入操作
			} catch (UnknownAccountException uae) {	// 未知賬戶
				log.info("There is no user with username of " + token.getPrincipal());
			} catch (IncorrectCredentialsException ice) {
				log.info("Password for account " + token.getPrincipal() + " was incorrect!");
			} catch (LockedAccountException lae) {	// 使用者被鎖定
				log.info("The account for username " + token.getPrincipal() + " is locked.  " +
						"Please contact your administrator to unlock it.");
			}
			// ... catch more exceptions here (maybe custom ones specific to your application?
			catch (AuthenticationException ae) {	// 認證異常
				//unexpected condition?  error?
			}
		}

		// 表明是誰:
		//print their identifying principal (in this case, a username):
		log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

		// 測試角色
		if (currentUser.hasRole("schwartz")) {
			log.info("你擁有 schwartz 的角色");
		} else {
			log.info("你只是凡人.");
		}

		// 測試許可權(粗粒度)
		if (currentUser.isPermitted("lightsaber:wield")) {
			log.info("你擁有 lightsaber:* 的許可權");
		} else {
			log.info("抱歉,你沒有 lightsaber:* 的許可權");
		}

		// 測試許可權(細粒度),帶有引數
		if (currentUser.isPermitted("winnebago:drive:eagle5")) {
			log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  " +
					"Here are the keys - have fun!");
		} else {
			log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
		}

		// 登出
		currentUser.logout();

		System.exit(0);
	}

比較核心的幾個方法或者步驟就有:

Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
currentUser.isAuthenticated()
currentUser.getPrincipal()
currentUser.hasRole("schwartz")
currentUser.isPermitted("lightsaber:wield")
currentUser.logout()

1.3 SpringBoot 整合 Shiro

我的 github 配套原始碼:https://github.com/Amor128/shiro/tree/master/my-shiro/my-shiro-02-ShiroInSpringBoot

三大物件:

  • Subject 使用者
  • SecurityManager 管理所有使用者
  • Realm 連線資料

1.3.1 環境搭建

先看 pom 依賴,主要就是 SpringBoot 整合 Shiro 的依賴:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.8.0</version>
</dependency>

再看專案目錄:

對於 UserRealm 檔案:

package com.ermao.config;

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;

/**
 * 自定義的 Realm,繼承自 AuthorizingRealm
 * @author Ermao
 * Date: 2021/9/19 23:19
 */
public class UserRealm extends AuthorizingRealm {
	// 授權
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
		System.out.println("執行了授權==>doGetAuthorizationInfo");
		return null;
	}

	// 認證
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
		System.out.println("執行了認證==>doGetAuthenticationInfo");
		return null;
	}
}

ShiroConfig 檔案:

package com.ermao.config;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author Ermao
 * Date: 2021/9/19 23:16
 */
@Configuration
public class ShiroConfig {
	// 1. 建立 ShiroFilterFactoryBean
	@Bean
	public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager getDefaultWebSecurityManager) {
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
		// 設定安全管理器
		shiroFilterFactoryBean.setSecurityManager(getDefaultWebSecurityManager);
		
		// 新增 shiro 內建過濾器
		// anon 無需認證
		// authc 必須認證才能訪問
		// user 類必須擁有 記住我 才能使用
		// perms 擁有對某個資源的許可權才能訪問
		// role 擁有某個角色產能訪問
		Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//		filterChainDefinitionMap.put("/user/insert", "anon");
//		filterChainDefinitionMap.put("/user/update", "authc");
		filterChainDefinitionMap.put("/user/*", "authc");
		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
		shiroFilterFactoryBean.setLoginUrl("/toLogin");	// 設定登入請求

		return shiroFilterFactoryBean;
	}

	// 2. 建立 DefaultWebSecurityManager
	@Bean
	public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
		DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
		// 管理 realm
		defaultWebSecurityManager.setRealm(userRealm);
		return defaultWebSecurityManager;
	}

	// 1. 建立 Realm 物件,自定義類
	@Bean
	public UserRealm userRealm() {
		return new UserRealm();
	}
}

Shiro 的配置就是這樣固定的套路,搭建好之後自己改就完事了。


1.3.2 攔截

主要是在這段程式碼中:

// 新增 shiro 內建過濾器
// anon 無需認證
// authc 必須認證才能訪問
// user 類必須擁有 記住我 才能使用
// perms 擁有對某個資源的許可權才能訪問
// role 擁有某個角色產能訪問
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//		filterChainDefinitionMap.put("/user/insert", "anon");
//		filterChainDefinitionMap.put("/user/update", "authc");
filterChainDefinitionMap.put("/user/*", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
shiroFilterFactoryBean.setLoginUrl("/toLogin");	// 設定登入請求

1.3.3 使用者認證

將我們的 UserRealm 檔案修改成這樣:

// 認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("執行了認證==>doGetAuthenticationInfo");

    // 封裝使用者當前登入資料
    // 使用者名稱,密碼 從資料庫中取
    String username = "admin";
    String password = "12345";
    UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

    if (!token.getUsername().equals(username)) {
        return null;	// 丟擲異常 UnknownAccountException
    }

    // 密碼認證由 shiro 框架完成
    // 還可以進行加密 MD5、MD5鹽值加密
    return new SimpleAuthenticationInfo("", password, "");
}

controller 中的程式碼是這樣的:

	@RequestMapping("/login")
	public String login(String username, String password, Model model) {
		Subject subject = SecurityUtils.getSubject();	// 獲取 shiro subject 物件
		UsernamePasswordToken token = new UsernamePasswordToken(username, password);	// 構造密碼 token
		try {
			subject.login(token);	// 執行登入的方法,如果沒有異常就說明 OK
			return "index";
		} catch (UnknownAccountException e) {	// 使用者名稱不存在
			model.addAttribute("msg", "使用者名稱錯誤");
			return "login";
		} catch (IncorrectCredentialsException e) {	// 使用者名稱不存在
			model.addAttribute("msg", "密碼錯誤");
			return "login";
		}
	}

最後的效果是這樣的:

1.3.4 使用者授權

鑑權程式碼:

filterChainDefinitionMap.put("/user/insert", "perms[user:insert]");

注意要把這段程式碼放在攔截程式碼的前面

使用者在認證的時候就應該拿到使用者的許可權(從資料庫中),以下是認證部分的程式碼:

	// 認證
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
		System.out.println("執行了認證==>doGetAuthenticationInfo");

		// 封裝使用者當前登入資料
		// 使用者名稱,密碼 從資料庫中取
		UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
		User user = userService.selectUserByUsername(token.getUsername());
		if (user == null) {
			return null;	// 丟擲異常 UnknownAccountException
		}
		// 密碼認證由 shiro 框架完成
		// 還可以進行加密 MD5、MD5鹽值加密
		return new SimpleAuthenticationInfo(user, user.getPwd(), "");
	}

以下是授權部分的程式碼:

	// 授權
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
		System.out.println("執行了授權==>doGetAuthorizationInfo");

		Subject subject = SecurityUtils.getSubject();
		User currentUser = (User) subject.getPrincipal();

		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		info.addStringPermission(currentUser.getPermissions());
		return info;
	}

概括圖,注意其中取得使用者並且作為 principle 傳遞到授權的過程:

經過上述配置就可以實現使用者授權並且基於許可權攔截了。


1.3.5 整合 Thymeleaf 和 Shiro

整合好這兩個元件之後就可以使用 Shiro 的 Dialect 來實現 view 元件的顯示和隱藏,直接上程式碼:

首頁:

<!DOCTYPE html>
<html lang="en"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>首頁</h1>
<p th:text="${msg}"></p>

<div shiro:notAuthenticated="">
    <a th:href="@{/toLogin}">登入</a>
</div>

<div shiro:hasPermission="user:insert"><a th:href="@{user/insert}">insert</a>
</div>
<div shiro:hasPermission="user:update"><a th:href="@{user/update}">update</a>
</div>
<div shiro:authenticated="">
    <form th:action="@{/logout}">
        <input type="submit" value="登出">
    </form>
</div>

</body>
</html>

其他頁面類似,不做贅述,主要就是使用 shiro:打頭的標籤屬性