SpringBoot(十四):springboot整合shiro-登錄認證和權限管理
原文出處: 純潔的微笑
這篇文章我們來學習如何使用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如下:
用戶信息
@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 方法
}
根據以上的代碼會自動生成user_info(用戶信息表)、sys_role(角色表)、sys_permission(權限表)、sys_user_role(用戶角色表)、sys_role_permission(角色權限表)這五張表,為了方便測試我們給這五張表插入一些初始化數據:
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_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 (
1
,
2
);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (
1
,
3
);
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (
1
,
1
);
INSERT INTO `user_info` (`uid`,`username`,`name`,`password`,`salt`,`state`) VALUES (
‘1‘
,
‘admin‘
,
‘管理員‘
,
‘d3c59d25033dbf980d29554025c23a75‘
,
‘8d78869f470951332959580424d4bf4f‘
,
0
);
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());
}
}
return
authorizationInfo;
}
當然也可以添加set集合:roles是從數據庫查詢的當前用戶的角色,stringPermissions是從數據庫查詢的當前用戶對應的權限
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(stringPermissions);
就是說如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “perms[權限添加]”);就說明訪問/add這個鏈接必須要有“權限添加”這個權限才可以訪問,如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “roles[100002],perms[權限添加]”);就說明訪問/add這個鏈接必須要有“權限添加”這個權限和具有“100002”這個角色才可以訪問。
登錄實現
登錄過程其實只是處理異常的相關信息,具體的登錄驗證交給shiro來處理
@RequestMapping
(
"/login"
)
public
String login(HttpServletRequest request, Map<String, Object> map)
throws
Exception{
System.out.println(
"HomeController.login()"
);
// 登錄失敗從request中獲取shiro處理的異常信息。
// shiroLoginFailure:就是shiro異常類的全類名.
String exception = (String) request.getAttribute(
"shiroLoginFailure"
);
System.out.println(
"exception="
+ exception);
String msg =
""
;
if
(exception !=
null
) {
if
(UnknownAccountException.
class
.getName().equals(exception)) {
System.out.println(
"UnknownAccountException -- > 賬號不存在:"
);
msg =
"UnknownAccountException -- > 賬號不存在:"
;
}
else
if
(IncorrectCredentialsException.
class
.getName().equals(exception)) {
System.out.println(
"IncorrectCredentialsException -- > 密碼不正確:"
);
msg =
"IncorrectCredentialsException -- > 密碼不正確:"
;
}
else
if
(
"kaptchaValidateFailed"
.equals(exception)) {
System.out.println(
"kaptchaValidateFailed -- > 驗證碼錯誤"
);
msg =
"kaptchaValidateFailed -- > 驗證碼錯誤"
;
}
else
{
msg =
"else >> "
+exception;
System.out.println(
"else -- >"
+ exception);
}
}
map.put(
"msg"
, msg);
// 此方法不處理登錄成功,由shiro進行處理
return
"/login"
;
}
其它dao層和service的代碼就不貼出來了大家直接看代碼。
測試
1、編寫好後就可以啟動程序,訪問index頁面,由於沒有登錄就會跳轉到login頁面。登錄之後就會跳轉到index頁面,登錄後,有直接在瀏覽器中輸入index頁面訪問,又會跳轉到login頁面。上面這些操作時候觸發MyShiroRealm.doGetAuthenticationInfo()這個方法,也就是登錄認證的方法。
2、登錄admin賬戶,訪問:http://127.0.0.1:8080/userInfo/userAdd顯示用戶添加界面,訪問http://127.0.0.1:8080/userInfo/userDel顯示403沒有權限。上面這些操作時候觸發MyShiroRealm.doGetAuthorizationInfo()這個方面,也就是權限校驗的方法。
3、修改admin不同的權限進行測試
shiro很強大,這僅僅是完成了登錄認證和權限管理這兩個功能,更多內容以後有時間再做探討。
示例代碼
參考:
- Apache Shiro中文手冊
- Spring Boot Shiro權限管理【從零開始學Spring Boot】
- SpringBoot+shiro整合學習之登錄認證和權限控制
SpringBoot(十四):springboot整合shiro-登錄認證和權限管理