一起來學SpringBoot | 第二十六篇:輕鬆搞定安全框架(Shiro)
SpringBoot
是為了簡化Spring
應用的建立、執行、除錯、部署等一系列問題而誕生的產物,自動裝配的特性讓我們可以更好的關注業務本身而不是外部的XML配置,我們只需遵循規範,引入相關的依賴就可以輕易的搭建出一個 WEB 工程
Shiro 是 Apache
旗下開源的一款強大且易用的Java安全框架,身份驗證、授權、加密、會話管理。 相比 Spring Security
而言 Shiro
更加輕量級,且 API 更易於理解…
Shiro
Shiro
主要分為 安全認證 和 介面授權 兩個部分,其中的核心元件為 Subject
、SecurityManager
、Realms
Shiro
都已經為我們封裝好了,我們只需要按照一定的規則去編寫響應的程式碼即可…
Subject
即表示主體,將使用者的概念理解為當前操作的主體,因為它即可以是一個通過瀏覽器請求的使用者,也可能是一個執行的程式,外部應用與 Subject 進行互動,記錄當前操作使用者。Subject 代表了當前使用者的安全操作,SecurityManager 則管理所有使用者的安全操作。SecurityManager
即安全管理器,對所有的 Subject 進行安全管理,並通過它來提供安全管理的各種服務(認證、授權等)Realm
充當了應用與資料安全間的 橋樑 或 聯結器。當對使用者執行認證(登入)和授權(訪問控制)驗證時,Shiro 會從應用配置的 Realm 中查詢使用者及其許可權資訊。
本章目標
利用 Spring Boot
與 Shiro
實現安全認證和授權….
匯入依賴
依賴 spring-boot-starter-web
…
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version> 1.8</java.version>
<shiro.version>1.4.0</shiro.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- shiro 相關包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- End -->
</dependencies>
屬性配置
快取配置
Shiro 為我們提供了 CacheManager
即快取管理,將使用者許可權資料儲存在快取,可以提高它的效能。支援 EhCache
、Redis
等常規快取,這裡為了簡單起見就用 EhCache
了 , 在resources
目錄下建立一個 ehcache-shiro.xml
檔案
<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" name="shiroCache">
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>
</ehcache>
實體類
建立一個 User.java
,標記為資料庫使用者
package com.battcn.entity;
/**
* @author Levin
* @since 2018/6/28 0028
*/
public class User {
/** 自增ID */
private Long id;
/** 賬號 */
private String username;
/** 密碼 */
private String password;
/** 角色名:Shiro 支援多個角色,而且接收引數也是 Set<String> 集合,但這裡為了簡單起見定義成 String 型別了 */
private String roleName;
/** 是否禁用 */
private boolean locked;
// 省略 GET SET 建構函式...
}
偽造資料
支援 roles
、permissions
,比如你一個介面可以允許使用者擁有某一個角色,也可以是擁有某一個 permission
…
package com.battcn.config;
import com.battcn.entity.User;
import java.util.*;
/**
* 主要不想連線資料庫..
*
* @author Levin
* @since 2018/6/28 0028
*/
public class DBCache {
/**
* K 使用者名稱
* V 使用者資訊
*/
public static final Map<String, User> USERS_CACHE = new HashMap<>();
/**
* K 角色ID
* V 許可權編碼
*/
public static final Map<String, Collection<String>> PERMISSIONS_CACHE = new HashMap<>();
static {
// TODO 假設這是資料庫記錄
USERS_CACHE.put("u1", new User(1L, "u1", "p1", "admin", true));
USERS_CACHE.put("u2", new User(2L, "u2", "p2", "admin", false));
USERS_CACHE.put("u3", new User(3L, "u3", "p3", "test", true));
PERMISSIONS_CACHE.put("admin", Arrays.asList("user:list", "user:add", "user:edit"));
PERMISSIONS_CACHE.put("test", Collections.singletonList("user:list"));
}
}
ShiroConfiguration
Shiro 的主要配置資訊都在此檔案內實現;
package com.battcn.config;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Shiro 配置
*
* @author Levin
*/
@Configuration
public class ShiroConfiguration {
private static final Logger log = LoggerFactory.getLogger(ShiroConfiguration.class);
@Bean
public EhCacheManager getEhCacheManager() {
EhCacheManager em = new EhCacheManager();
em.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return em;
}
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 加密器:這樣一來資料庫就可以是密文儲存,為了演示我就不開啟了
*
* @return HashedCredentialsMatcher
*/
// @Bean
// public HashedCredentialsMatcher hashedCredentialsMatcher() {
// HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// //雜湊演算法:這裡使用MD5演算法;
// hashedCredentialsMatcher.setHashAlgorithmName("md5");
// //雜湊的次數,比如雜湊兩次,相當於 md5(md5(""));
// hashedCredentialsMatcher.setHashIterations(2);
// return hashedCredentialsMatcher;
// }
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
@Bean(name = "authRealm")
public AuthRealm authRealm(EhCacheManager cacheManager) {
AuthRealm authRealm = new AuthRealm();
authRealm.setCacheManager(cacheManager);
return authRealm;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(AuthRealm authRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(authRealm);
// <!-- 使用者授權/認證資訊Cache, 採用EhCache 快取 -->
defaultWebSecurityManager.setCacheManager(getEhCacheManager());
return defaultWebSecurityManager;
}
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(
DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
/**
* ShiroFilter<br/>
* 注意這裡引數中的 StudentService 和 IScoreDao 只是一個例子,因為我們在這裡可以用這樣的方式獲取到相關訪問資料庫的物件,
* 然後讀取資料庫相關配置,配置到 shiroFilterFactoryBean 的訪問規則中。實際專案中,請使用自己的Service來處理業務邏輯。
*
* @param securityManager 安全管理器
* @return ShiroFilterFactoryBean
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必須設定 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不設定預設會自動尋找Web工程根目錄下的"/login"頁面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登入成功後要跳轉的連線
shiroFilterFactoryBean.setSuccessUrl("/index");
shiroFilterFactoryBean.setUnauthorizedUrl("/denied");
loadShiroFilterChain(shiroFilterFactoryBean);
return shiroFilterFactoryBean;
}
/**
* 載入shiroFilter許可權控制規則(從資料庫讀取然後配置)
*/
private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {
/////////////////////// 下面這些規則配置最好配置到配置檔案中 ///////////////////////
// TODO 重中之重啊,過濾順序一定要根據自己需要排序
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 需要驗證的寫 authc 不需要的寫 anon
filterChainDefinitionMap.put("/resource/**", "anon");
filterChainDefinitionMap.put("/install", "anon");
filterChainDefinitionMap.put("/hello", "anon");
// anon:它對應的過濾器裡面是空的,什麼都沒做
log.info("##################從資料庫讀取許可權規則,載入到shiroFilter中##################");
// 不用註解也可以通過 API 方式載入許可權規則
Map<String, String> permissions = new LinkedHashMap<>();
permissions.put("/users/find", "perms[user:find]");
filterChainDefinitionMap.putAll(permissions);
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
}
}
AuthRealm
上面介紹過 Realm
,安全認證和許可權驗證的核心處理就是重寫 AuthorizingRealm
中的 doGetAuthenticationInfo(登入認證)
與 doGetAuthorizationInfo(許可權驗證)
package com.battcn.config;
import com.battcn.entity.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.context.annotation.Configuration;
import java.util.*;
/**
* 認證領域
*
* @author Levin
* @version 2.5.1
* @since 2018-01-10
*/
@Configuration
public class AuthRealm extends AuthorizingRealm {
/**
* 認證回撥函式,登入時呼叫
* 首先根據傳入的使用者名稱獲取User資訊;然後如果user為空,那麼丟擲沒找到帳號異常UnknownAccountException;
* 如果user找到但鎖定了丟擲鎖定異常LockedAccountException;最後生成AuthenticationInfo資訊,
* 交給間接父類AuthenticatingRealm使用CredentialsMatcher進行判斷密碼是否匹配,
* 如果不匹配將丟擲密碼錯誤異常IncorrectCredentialsException;
* 另外如果密碼重試此處太多將丟擲超出重試次數異常ExcessiveAttemptsException;
* 在組裝SimpleAuthenticationInfo資訊時, 需要傳入:身份資訊(使用者名稱)、憑據(密文密碼)、鹽(username+salt),
* CredentialsMatcher使用鹽加密傳入的明文密碼和此處的密文密碼進行匹配。
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
String principal = (String) token.getPrincipal();
User user = Optional.ofNullable(DBCache.USERS_CACHE.get(principal)).orElseThrow(UnknownAccountException::new);
if (!user.isLocked()) {
throw new LockedAccountException();
}
// 從資料庫查詢出來的賬號名和密碼,與使用者輸入的賬號和密碼對比
// 當用戶執行登入時,在方法處理上要實現 user.login(token)
// 然後會自動進入這個類進行認證
// 交給 AuthenticatingRealm 使用 CredentialsMatcher 進行密碼匹配,如果覺得人家的不好可以自定義實現
// TODO 如果使用 HashedCredentialsMatcher 這裡認證方式就要改一下 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, "密碼", ByteSource.Util.bytes("密碼鹽"), getName());
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, user.getPassword(), getName());
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute("USER_SESSION", user);
return authenticationInfo;
}
/**
* 只有需要驗證許可權時才會呼叫, 授權查詢回撥函式, 進行鑑權但快取中無使用者的授權資訊時呼叫.在配有快取的情況下,只加載一次.
* 如果需要動態許可權,但是又不想每次去資料庫校驗,可以存在ehcache中.自行完善
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
Session session = SecurityUtils.getSubject().getSession();
User user = (User) session.getAttribute("USER_SESSION");
// 許可權資訊物件info,用來存放查出的使用者的所有的角色(role)及許可權(permission)
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 使用者的角色集合
Set<String> roles = new HashSet<>();
roles.add(user.getRoleName());
info.setRoles(roles);
// 使用者的角色對應的所有許可權,如果只使用角色定義訪問許可權,下面可以不要
// 只有角色並沒有顆粒度到每一個按鈕 或 是操作選項 PERMISSIONS 是可選項
final Map<String, Collection<String>> permissionsCache = DBCache.PERMISSIONS_CACHE;
final Collection<String> permissions = permissionsCache.get(user.getRoleName());
info.addStringPermissions(permissions);
return info;
}
}
控制器
在 ShiroConfiguration
中的 shiroFilter
處配置了 /hello = anon
,意味著可以不需要認證也可以訪問,那麼除了這種方式外 Shiro
還為我們提供了一些註解相關的方式…
常用註解
@RequiresGuest
代表無需認證即可訪問,同理的就是/path = anon
@RequiresAuthentication
需要認證,只要登入成功後就允許你操作@RequiresPermissions
需要特定的許可權,沒有則丟擲AuthorizationException
@RequiresRoles
需要特定的橘色,沒有則丟擲AuthorizationException
@RequiresUser
不太清楚,不常用…
LoginController
package com.battcn.controller;
import com.battcn.config.ShiroConfiguration;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* @author Levin
* @since 2018/6/28 0028
*/
@RestController
public class LoginController {
private static final Logger log = LoggerFactory.getLogger(ShiroConfiguration.class);
@GetMapping(value = "/hello")
public String hello() {
log.info("不登入也可以訪問...");
return "hello...";
}
@GetMapping(value = "/index")
public String index() {
log.info("登陸成功了...");
return "index";
}
@GetMapping(value = "/denied")
public String denied() {
log.info("小夥子許可權不足,別無謂掙扎了...");
return "denied...";
}
@GetMapping(value = "/login")
public String login(String username, String password, RedirectAttributes model) {
// 想要得到 SecurityUtils.getSubject() 的物件..訪問地址必須跟 shiro 的攔截地址內.不然後會報空指標
Subject sub = SecurityUtils.getSubject();
// 使用者輸入的賬號和密碼,,存到UsernamePasswordToken物件中..然後由shiro內部認證對比,
// 認證執行者交由 com.battcn.config.AuthRealm 中 doGetAuthenticationInfo 處理
// 當以上認證成功後會向下執行,認證失敗會丟擲異常
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
sub.login(token);
} catch (UnknownAccountException e) {
log.error("對使用者[{}]進行登入驗證,驗證未通過,使用者不存在", username);
token.clear();
return "UnknownAccountException";
} catch (LockedAccountException lae) {
log.error("對使用者[{}]進行登入驗證,驗證未通過,賬戶已鎖定", username);
token.clear();
return "LockedAccountException";
} catch (ExcessiveAttemptsException e) {
log.error("對使用者[{}]進行登入驗證,驗證未通過,錯誤次數過多", username);
token.clear();
return "ExcessiveAttemptsException";
} catch (AuthenticationException e) {
log.error("對使用者[{}]進行登入驗證,驗證未通過,堆疊軌跡如下", username, e);
token.clear();
return "AuthenticationException";
}
return "success";
}
}
UserController
package com.battcn.controller;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Levin
* @since 2018/6/28 0028
*/
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping
public String get() {
return "get.....";
}
/**
* RequiresRoles 是所需角色 包含 AND 和 OR 兩種
* RequiresPermissions 是所需許可權 包含 AND 和 OR 兩種
*
* @return msg
*/
@RequiresRoles(value = {"admin", "test"}, logical = Logical.OR)
//@RequiresPermissions(value = {"user:list", "user:query"}, logical = Logical.OR)
@GetMapping("/query")
public String query() {
return "query.....";
}
@GetMapping("/find")
public String find() {
return "find.....";
}
}
主函式
package com.battcn;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Levin
*/
@SpringBootApplication
public class Chapter25Application {
public static void main(String[] args) {
SpringApplication.run(Chapter25Application.class, args);
}
}
測試
啟動 Chapter25Application.java
中的 main
方法,為了更好的演示效果這裡打開了 postman
做的測試,只演示其中一個流程,剩下的可以自己複製程式碼測試…
先登入,由於 u3
在 DBCache
中擁有的角色是 test
,只有 user:list
這一個許可權
訪問 /users/query
成功,因為我們符合響應的角色/許可權
訪問 /users/find
失敗,並重定向到了 /denied
介面,問題來了為什麼 /users/find
沒有寫註解也許可權不足呢?
細心的朋友肯定會發現 在 ShiroConfiguration 中寫了一句 permissions.put(“/users/find”, “perms[user:find]”); 意味著我們不僅可以通過註解方式,同樣可以通過初始化時載入資料庫中的許可權樹做控制,看各位喜好了….
總結
目前很多大佬都寫過關於 SpringBoot
的教程了,如有雷同,請多多包涵,本教程基於最新的 spring-boot-starter-parent:2.0.3.RELEASE
編寫,包括新版本的特性都會一起介紹…
說點什麼
- 個人QQ:1837307557
- battcn開源群(適合新手):391619659
- 微信公眾號(歡迎調戲):
battcn