這篇文章我們來學習如何使用 Spring Boot 集成 Apache Shiro 。安全應該是互聯網公司的一道生命線,幾乎任何的公司都會涉及到這方面的需求。在 Java 領域一般有 Spring Security、 Apache Shiro 等安全框架,但是由於 Spring Security ...
這篇文章我們來學習如何使用 Spring Boot 集成 Apache Shiro 。安全應該是互聯網公司的一道生命線,幾乎任何的公司都會涉及到這方面的需求。在 Java 領域一般有 Spring Security、 Apache Shiro 等安全框架,但是由於 Spring Security 過於龐大和複雜,大多數公司會選擇 Apache Shiro 來使用,這篇文章會先介紹一下 Apache Shiro ,在結合 Spring Boot 給出使用案例。
Apache Shiro
What is Apache Shiro?
Apache Shiro 是一個功能強大、靈活的,開源的安全框架。它可以乾凈利落地處理身份驗證、授權、企業會話管理和加密。
Apache Shiro 的首要目標是易於使用和理解。安全通常很複雜,甚至讓人感到很痛苦,但是 Shiro 卻不是這樣子的。一個好的安全框架應該屏蔽複雜性,向外暴露簡單、直觀的 API,來簡化開發人員實現應用程式安全所花費的時間和精力。
Shiro 能做什麼呢?
-
驗證用戶身份
-
用戶訪問許可權控制,比如:1、判斷用戶是否分配了一定的安全形色。2、判斷用戶是否被授予完成某個操作的許可權
-
在非 Web 或 EJB 容器的環境下可以任意使用 Session API
-
可以響應認證、訪問控制,或者 Session 生命周期中發生的事件
-
可將一個或以上用戶安全數據源數據組合成一個複合的用戶 "view"(視圖)
-
支持單點登錄(SSO)功能
-
支持提供“Remember Me”服務,獲取用戶關聯信息而無需登錄
…
等等——都集成到一個有凝聚力的易於使用的 API。
Shiro 致力在所有應用環境下實現上述功能,小到命令行應用程式,大到企業應用中,而且不需要藉助第三方框架、容器、應用伺服器等。當然 Shiro 的目的是儘量的融入到這樣的應用環境中去,但也可以在它們之外的任何環境下開箱即用。
Apache Shiro Features 特性
Apache Shiro 是一個全面的、蘊含豐富功能的安全框架。下圖為描述 Shiro 功能的框架圖:
Authentication(認證), Authorization(授權), Session Management(會話管理), Cryptography(加密)被 Shiro 框架的開發團隊稱之為應用安全的四大基石。那麼就讓我們來看看它們吧:
-
Authentication(認證):用戶身份識別,通常被稱為用戶“登錄”
-
Authorization(授權):訪問控制。比如某個用戶是否具有某個操作的使用許可權。
-
Session Management(會話管理):特定於用戶的會話管理,甚至在非web 或 EJB 應用程式。
-
Cryptography(加密):在對數據源使用加密演算法加密的同時,保證易於使用。
還有其他的功能來支持和加強這些不同應用環境下安全領域的關註點。特別是對以下的功能支持:
-
Web支持:Shiro 提供的 Web 支持 api ,可以很輕鬆的保護 Web 應用程式的安全。
-
緩存:緩存是 Apache Shiro 保證安全操作快速、高效的重要手段。
-
併發:Apache Shiro 支持多線程應用程式的併發特性。
-
測試:支持單元測試和集成測試,確保代碼和預想的一樣安全。
-
"Run As":這個功能允許用戶假設另一個用戶的身份(在許可的前提下)。
-
"Remember Me":跨 session 記錄用戶的身份,只有在強制需要時才需要登錄。
註意: Shiro 不會去維護用戶、維護許可權,這些需要我們自己去設計/提供,然後通過相應的介面註入給 Shiro
High-Level Overview 高級概述
在概念層,Shiro 架構包含三個主要的理念:Subject,SecurityManager和 Realm。下麵的圖展示了這些組件如何相互作用,我們將在下麵依次對其進行描述。
-
Subject:當前用戶,Subject 可以是一個人,但也可以是第三方服務、守護進程帳戶、時鐘守護任務或者其它--當前和軟體交互的任何事件。
-
SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架構的核心,配合內部安全組件共同組成安全傘。
-
Realms:用於進行許可權信息的驗證,我們自己實現。Realm 本質上是一個特定的安全 DAO:它封裝與數據源連接的細節,得到Shiro 所需的相關的數據。在配置 Shiro 的時候,你必須指定至少一個Realm 來實現認證(authentication)和/或授權(authorization)。
我們需要實現Realms的Authentication 和 Authorization。其中 Authentication 是用來驗證用戶身份,Authorization 是授權訪問控制,用於對用戶進行的操作授權,證明該用戶是否允許進行當前操作,如訪問某個鏈接,某個資源文件等。
快速上手
基礎信息
pom包依賴
-
<dependencies>
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-data-jpa</artifactId>
-
</dependency>
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-thymeleaf</artifactId>
-
</dependency>
-
<dependency>
-
<groupId>net.sourceforge.nekohtml</groupId>
-
<artifactId>nekohtml</artifactId>
-
<version>1.9.22</version>
-
</dependency>
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-web</artifactId>
-
</dependency>
-
<dependency>
-
<groupId>org.apache.shiro</groupId>
-
<artifactId>shiro-spring</artifactId>
-
<version>1.4.0</version>
-
</dependency>
-
<dependency>
-
<groupId>mysql</groupId>
-
<artifactId>mysql-connector-java</artifactId>
-
<scope>runtime</scope>
-
</dependency>
-
</dependencies>
重點是 shiro-spring 包
配置文件
-
spring:
-
datasource:
-
url: jdbc:mysql://localhost:3306/test
-
username: root
-
password: root
-
driver-class-name: com.mysql.jdbc.Driver
-
-
jpa:
-
database: mysql
-
show-sql: true
-
hibernate:
-
ddl-auto: update
-
naming:
-
strategy: org.hibernate.cfg.DefaultComponentSafeNamingStrategy
-
properties:
-
hibernate:
-
dialect: org.hibernate.dialect.MySQL5Dialect
-
-
thymeleaf:
-
cache: false
-
mode: LEGACYHTML5
thymeleaf的配置是為了去掉html的校驗
頁面
我們新建了六個頁面用來測試:
-
index.html :首頁
-
login.html :登錄頁
-
userInfo.html : 用戶信息頁面
-
userInfoAdd.html :添加用戶頁面
-
userInfoDel.html :刪除用戶頁面
-
403.html : 沒有許可權的頁面
除過登錄頁面其它都很簡單,大概如下:
-
<!DOCTYPE html>
-
<html lang="en">
-
<head>
-
<meta charset="UTF-8">
-
<title>Title</title>
-
</head>
-
<body>
-
<h1>index</h1>
-
</body>
-
</html>
RBAC
RBAC 是基於角色的訪問控制(Role-Based Access Control )在 RBAC 中,許可權與角色相關聯,用戶通過成為適當角色的成員而得到這些角色的許可權。這就極大地簡化了許可權的管理。這樣管理都是層級相互依賴的,許可權賦予給角色,而把角色又賦予用戶,這樣的許可權設計很清楚,管理起來很方便。
採用 Jpa 技術來自動生成基礎表格,對應的實體如下:
用戶信息
-
@Entity
-
public class UserInfo implements Serializable {
-
@Id
-
@GeneratedValue
-
private Integer uid;
-
@Column(unique =true)
-
private String username;//帳號
-
private String name;//名稱(昵稱或者真實姓名,不同系統不同定義)
-
private String password; //密碼;
-
private String salt;//加密密碼的鹽
-
private byte state;//用戶狀態,0:創建未認證(比如沒有激活,沒有輸入驗證碼等等)--等待驗證的用戶 , 1:正常狀態,2:用戶被鎖定.
-
@ManyToMany(fetch= FetchType.EAGER)//立即從資料庫中進行載入數據;
-
@JoinTable(name = "SysUserRole", joinColumns = { @JoinColumn(name = "uid") }, inverseJoinColumns ={@JoinColumn(name = "roleId") })
-
private List<SysRole> roleList;// 一個用戶具有多個角色
-
-
// 省略 get set 方法
-
}
角色信息
-
@Entity
-
public class SysRole {
-
@Id@GeneratedValue
-
private Integer id; // 編號
-
private String role; // 角色標識程式中判斷使用,如"admin",這個是唯一的:
-
private String description; // 角色描述,UI界面顯示使用
-
private Boolean available = Boolean.FALSE; // 是否可用,如果不可用將不會添加給用戶
-
-
//角色 -- 許可權關係:多對多關係;
-
@ManyToMany(fetch= FetchType.EAGER)
-
@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")})
-
private List<SysPermission> permissions;
-
-
// 用戶 - 角色關係定義;
-
@ManyToMany
-
@JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="uid")})
-
private List<UserInfo> userInfos;// 一個角色對應多個用戶
-
-
// 省略 get set 方法
-
}
許可權信息
-
@Entity
-
public class SysPermission implements Serializable {
-
@Id@GeneratedValue
-
private Integer id;//主鍵.
-
private String name;//名稱.
-
@Column(columnDefinition="enum('menu','button')")
-
private String resourceType;//資源類型,[menu|button]
-
private String url;//資源路徑.
-
private String permission; //許可權字元串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
-
private Long parentId; //父編號
-
private String parentIds; //父編號列表
-
private Boolean available = Boolean.FALSE;
-
@ManyToMany
-
@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")})
-
private List<SysRole> roles;
-
-
// 省略 get set 方法
-
}
根據以上的代碼會自動生成 userinfo(用戶信息表)、sysrole(角色表)、syspermission(許可權表)、sysuserrole(用戶角色表)、sysrole_permission(角色許可權表)這五張表,為了方便測試我們給這五張表插入一些初始化數據:
-
INSERT INTO `user_info` (`uid`,`username`,`name`,`password`,`salt`,`state`) VALUES ('1', 'admin', '管理員', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 0);
-
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (1,0,'用戶管理',0,'0/','userInfo:view','menu','userInfo/userList');
-
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (2,0,'用戶添加',1,'0/1','userInfo:add','button','userInfo/userAdd');
-
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (3,0,'用戶刪除',1,'0/1','userInfo:del','button','userInfo/userDel');
-
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (1,0,'管理員','admin');
-
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (2,0,'VIP會員','vip');
-
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (3,1,'test','test');
-
INSERT INTO `sys_role_permission` VALUES ('1', '1');
-
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,1);
-
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (2,1);
-
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (3,2);
-
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1,1);
Shiro 配置
首先要配置的是 ShiroConfig 類,Apache Shiro 核心通過 Filter 來實現,就好像 SpringMvc 通過 DispachServlet 來主控制一樣。 既然是使用 Filter 一般也就能猜到,是通過 URL 規則來進行過濾和許可權校驗,所以我們需要定義一系列關於 URL 的規則和訪問許可權。
ShiroConfig
-
@Configuration
-
public class ShiroConfig {
-
@Bean
-
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
-
System.out.println("ShiroConfiguration.shirFilter()");
-
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
-
shiroFilterFactoryBean.setSecurityManager(securityManager);
-
//攔截器.
-
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
-
// 配置不會被攔截的鏈接 順序判斷
-
filterChainDefinitionMap.put("/static/**", "anon");
-
//配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了
-
filterChainDefinitionMap.put("/logout", "logout");
-
//<!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最為下邊 -->:這是一個坑呢,一不小心代碼就不好使了;
-
//<!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問-->
-
filterChainDefinitionMap.put("/**", "authc");
-
// 如果不設置預設會自動尋找Web工程根目錄下的"/login.jsp"頁面
-
shiroFilterFactoryBean.setLoginUrl("/login");
-
// 登錄成功後要跳轉的鏈接
-
shiroFilterFactoryBean.setSuccessUrl("/index");
-
-
//未授權界面;
-
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
-
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
-
return shiroFilterFactoryBean;
-
}
-
-
@Bean
-
public MyShiroRealm myShiroRealm(){
-
MyShiroRealm myShiroRealm = new MyShiroRealm();
-
return myShiroRealm;
-
}
-
-
-
@Bean
-
public SecurityManager securityManager(){
-
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
-
securityManager.setRealm(myShiroRealm());
-
return securityManager;
-
}
-
}
Filter Chain 定義說明:
-
1、一個URL可以配置多個 Filter,使用逗號分隔
-
2、當設置多個過濾器時,全部驗證通過,才視為通過
-
3、部分過濾器可指定參數,如 perms,roles
Shiro 內置的 FilterChain
-
anon:所有 url 都都可以匿名訪問
-
authc: 需要認證才能進行訪問
-
user:配置記住我或認證通過可以訪問
登錄認證實現
在認證、授權內部實現機制中都有提到,最終處理都將交給Real進行處理。因為在 Shiro 中,最終是通過 Realm 來獲取應用程式中的用戶、角色及許可權信息的。通常情況下,在 Realm 中會直接從我們的數據源中獲取 Shiro 需要的驗證信息。可以說,Realm 是專用於安全框架的 DAO.
Shiro 的認證過程最終會交由 Realm 執行,這時會調用 Realm 的 getAuthenticationInfo(token)
方法。
該方法主要執行以下操作:
-
1、檢查提交的進行認證的令牌信息
-
2、根據令牌信息從數據源(通常為資料庫)中獲取用戶信息
-
3、對用戶信息進行匹配驗證。
-
4、驗證通過將返回一個封裝了用戶信息的
AuthenticationInfo
實例。 -
5、驗證失敗則拋出
AuthenticationException
異常信息。
而在我們的應用程式中要做的就是自定義一個 Realm 類,繼承AuthorizingRealm 抽象類,重載 doGetAuthenticationInfo(),重寫獲取用戶信息的方法。
doGetAuthenticationInfo 的重寫
-
@Override
-
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
-
throws AuthenticationException {
-
System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
-
//獲取用戶的輸入的賬號.
-
String username = (String)token.getPrincipal();
-
System.out.println(token.getCredentials());
-
//通過username從資料庫中查找 User對象,如果找到,沒找到.
-
//實際項目中,這裡可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重覆執行該方法
-
UserInfo userInfo = userInfoService.findByUsername(username);
-
System.out.println("----->>userInfo="+userInfo);
-
if(userInfo == null){
-
return null;
-
}
-
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
-
userInfo, //用戶名
-
userInfo.getPassword(), //密碼
-
ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt
-
getName() //realm name
-
);
-
return authenticationInfo;
-
}
鏈接許可權的實現
Shiro 的許可權授權是通過繼承 AuthorizingRealm
抽象類,重載 doGetAuthorizationInfo();
當訪問到頁面的時候,鏈接配置了相應的許可權或者 Shiro 標簽才會執行此方法否則不會執行,所以如果只是簡單的身份認證沒有許可權的控制的話,那麼這個方法可以不進行實現,直接返回 null 即可。在這個方法中主要是使用類: SimpleAuthorizationInfo
進行角色的添加和許可權的添加。
-
@Override
-
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
-
System.out.println("許可權配置-->MyShiroRealm.doGetAuthorizationInfo()");
-
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
-
UserInfo userInfo = (UserInfo)principals.getPrimaryPrincipal();
-
for(SysRole role:userInfo.getRoleList()){
-
authorizationInfo.addRole(role.getRole());
-
for(SysPermission p:role.getPermissions()){
-
authorizationInfo.addStringPermission(p.getPermission());<