Shiro-01-快速入門
Shiro
Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼和會話管理。
使用Shiro的易於理解的API,您可以快速、輕鬆地獲得任何應用程式,從最小的移動應用程式到最大的網路和企業應用程式。
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:
打頭的標籤屬性