1、SpringBoot整合Shiro
Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼和會話管理。
1.1、shiro簡介
shiro有個核心元件,分別為Subject、SecurityManager和Realms
- Subject:相當於當前操作的”使用者“,這個使用者不一定是一個具體的人,是一個抽象的概念,表明的是和當前程式進行互動的任何東西,例如爬蟲、指令碼、等等。所有的Subject都繫結到SecurityManager上,與 Subject 的所有互動都會委託給 SecurityManager;可以把 Subject 認為是一個門面;SecurityManager 才是實際的執行者。
- SecurityManager:這個是shiro框架的核心,所有與安全相關的操作都會與它進行互動,它管理者所有的Subject。
- Realms:充當了Shiro與應用安全資料間的”橋樑“,當對使用者執行認證(登入)和授權(訪問控制)驗證時,SecurityManager 需要從 Realm 獲取相應的使用者進行比較以確定使用者身份是否合法;也需要從 Realm 得到使用者相應的角色 / 許可權進行驗證使用者是否能進行操作。
如果想要更加深入的瞭解的shrio可以參考:https://www.w3cschool.cn/shiro/co4m1if2.html
1.2、程式碼的具體實現
1.2.1、Maven的配置
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>
<!--shiro整合thymeleaf-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!--shiro快取-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.7.1</version>
</dependency>
shiro預設是與jsp進行使用的,而這裡是shiro整合thymeleaf所有要匯入shiro整合thymeleaf的jar包
1.2.2、整合需要實現的類
一般來說整合只需要完成兩個類的實現即可
一個是 ShiroConfig 一個是 CustomerRealm
如果需要新增shiro快取並且不是自帶的快取而是redis快取還需要進行另外兩個類的編寫
一個是 RedisCache 一個是 RedisCacheManager
1.2.3、專案結構
1.2.4、ShiroConfig的實現
未加shiro的快取
package com.yuwen.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.yuwen.shiro.cache.RedisCacheManager;
import com.yuwen.shiro.realm.CustomerRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
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.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
//讓頁面shiro標籤生效
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
//1、建立shiroFilter 負責攔截所有請求
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
//給filter設定安全管理
factoryBean.setSecurityManager(defaultWebSecurityManager);
//配置系統的受限資源
//配置系統公共資源 全部都能訪問的設定anon
Map<String,String> map = new HashMap<>();
map.put("/main","authc");//請求這個資源需要認證和授權 authc表示需要認證後才能訪問
map.put("/admin","roles[admin]"); //表示admin角色才能訪問 roles[]表示需要什麼角色才能訪問
map.put("/manage","perms[user:*:*]"); //表示需要user:*:*許可權才能訪問 perms[]表示需要什麼許可權才能訪問
//訪問需要認證的頁面如果未登入會跳轉到/login路由進行登陸
factoryBean.setLoginUrl("/login");
//訪問未授權頁面會自動跳轉到/unAuth路由
factoryBean.setUnauthorizedUrl("/unAuth");
factoryBean.setFilterChainDefinitionMap(map);
return factoryBean;
}
//2、建立安全管理器
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("getRealm") Realm realm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//給安全管理器設定
securityManager.setRealm(realm);
return securityManager;
}
//3、建立自定義的realm
@Bean
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
//修改憑證校驗匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//設定加密演算法為md5
credentialsMatcher.setHashAlgorithmName("MD5");
//設定雜湊次數
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
return customerRealm;
}
}
因為一般在資料庫中設定明文密碼不安全,所有我這裡對密碼進行了md5加密,我的加密方式為:密碼 = 密碼+鹽+雜湊次數 而後進行MD5加密 所以這裡建立自定義的realm時需要進行設定匹配器這樣登入時密碼才能匹配成功
1.2.5、CustomerRealm的實現
package com.yuwen.shiro.realm;
import com.yuwen.pojo.User;
import com.yuwen.pojo.vo.ViewPerms;
import com.yuwen.pojo.vo.ViewRole;
import com.yuwen.service.UserService;
import com.yuwen.shiro.salt.MyByteSource;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.apache.shiro.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource;
import java.util.List;
//自定義realm
public class CustomerRealm extends AuthorizingRealm {
@Resource
private UserService userService;
//授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//獲取身份資訊
String primaryPrincipal = (String)principalCollection.getPrimaryPrincipal();
//根據主身份資訊獲取角色 和 許可權資訊
List<ViewRole> roles = userService.findRolesByUsername(primaryPrincipal);
if (!CollectionUtils.isEmpty(roles)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
roles.forEach(viewRole -> {
simpleAuthorizationInfo.addRole(viewRole.getName());
//許可權資訊
List<ViewPerms> perms = userService.findPermsByRoleId(viewRole.getName());
if (!CollectionUtils.isEmpty(perms)){
perms.forEach(viewPerms -> {
simpleAuthorizationInfo.addStringPermission(viewPerms.getPName());
});
}
});
return simpleAuthorizationInfo;
}
return null;
}
//認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//獲取登入的身份資訊
String principal = (String) authenticationToken.getPrincipal();
User user = userService.findByUsername(principal);
if (!ObjectUtils.isEmpty(user)){
//ByteSource.Util.bytes(user.getSalt()) 通過這個工具將鹽傳入
//如果身份認證驗證成功,返回一個AuthenticationInfo實現;
return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),new MyByteSource(user.getSalt()),this.getName());
}
return null;
}
}
在登入時會自動呼叫這個身份驗證 在驗證時如果出錯,會報異常,我在controller層接收了異常並處理
controller層中登入時的異常處理
@PostMapping("/login")
public String login(String username,String password){
//獲取主體物件
Subject subject = SecurityUtils.getSubject();
try {
//自動呼叫CustomerRealm 類中的身份驗證方法
subject.login(new UsernamePasswordToken(username,password));
return "index";
}catch (UnknownAccountException e){ //接收異常並處理
e.printStackTrace();
model.addAttribute("msg","使用者名稱有誤,請重新登入");
}catch (IncorrectCredentialsException e){//接收異常並處理
e.printStackTrace();
model.addAttribute("msg","密碼有誤,請重新登入");
}
return "login";
}
1.2.6、shiro快取配置
定義了shiro快取,使用者登入後,其使用者資訊、擁有的角色 / 許可權不必每次去查,這樣可以提高效率
預設快取的配置
在 ShiroConfig中 的 getRealm() 方法中開啟快取管理
@Bean
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
//開啟快取管理
customerRealm.setCacheManager(new EhCacheManager());
//開啟全域性快取
customerRealm.setCachingEnabled(true);
//開啟認證快取
customerRealm.setAuthenticationCachingEnabled(true);
customerRealm.setAuthenticationCacheName("authenticationCache");
//開啟許可權快取
customerRealm.setAuthorizationCachingEnabled(true);
customerRealm.setAuthorizationCacheName("authorizationCache");
return customerRealm;
}
與reids整合的快取這裡就不說明了,放在原始碼裡自己檢視,原始碼在下方
1.2.7、主頁index.html的設定
在這裡用標籤來判斷某些區域需要認證或什麼角色或者什麼許可權才能訪問
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>首頁</title>
<link rel="shortcut icon" href="#">
</head>
<body>
<h1>index</h1>
<a href="/logout">退出</a>
<div>
<a href="/main">main</a> | <a href="/manage">manage</a> | <a href="/admin">admin</a>
</div>
<!--獲取認證資訊-->
使用者:<span shiro:principal=""></span><hr>
<!--認證處理-->
<span shiro:authenticated=""><hr>
顯示認證通過內容
</span>
<span shiro:notAuthenticated=""><hr>
沒有認證時 顯示
</span>
<!--授權角色-->
<span shiro:hasRole="admin"><hr>
admin角色 顯示
</span>
<span shiro:hasPermission="user:*:*"><hr>
具有使用者模組的"user:*:*"許可權 顯示
</span>
</body>
</html>
1.3、簡單測試
1.3.1、admin角色所有許可權測試
1.3.2、無角色有許可權測試
1.3.3、無角色無許可權測試
1.4 專案原始碼
需要原始碼的在這:https://gitee.com/residual-temperature/shiro-demo