Spring Cloud之路:(七)SpringBoot+Shiro實現登入認證和許可權管理
一、Shiro介紹
1、Shiro是什麼?
Shiro是Apache下的一個開源專案,我們稱之為Apache Shiro。它是一個很易用與Java專案的的安全框架,提供了認證、授權、加密、會話管理,與 Spring Security 一樣都是做一個許可權的安全框架,但是與Spring Security 相比,在於 Shiro 使用了比較簡單易懂易於使用的授權方式。
2、Shiro能做什麼?
- 驗證使用者身份
- 使用者訪問許可權控制,比如:1、判斷使用者是否分配了一定的安全形色。2、判斷使用者是否被授予完成某個操作的許可權
- 在非 web 或 EJB 容器的環境下可以任意使用Session API
- 可以響應認證、訪問控制,或者 Session 生命週期中發生的事件
- 可將一個或以上使用者安全資料來源資料組合成一個複合的使用者 “view”(檢視)
- 支援單點登入(SSO)功能
- 支援提供“Remember Me”服務,獲取使用者關聯資訊而無需登入
- ……
Shiro 致力在所有應用環境下實現上述功能,小到命令列應用程式,大到企業應用中,而且不需要藉助第三方框架、容器、應用伺服器等。當然 Shiro 的目的是儘量的融入到這樣的應用環境中去,但也可以在它們之外的任何環境下開箱即用。
3、Shiro框架圖
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
4、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 是授權訪問控制,用於對使用者進行的操作授權,證明該使用者是否允許進行當前操作,如訪問某個連結,某個資原始檔等。
二、專案程式碼
檔案目錄
1、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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2、application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=1234567890
spring.datasource.schema=database/import.sql
spring.datasource.sql-script-encoding=utf-8
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.database=mysql
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
# thymeleaf的配置是為了去掉html的校驗
spring.thymeleaf.cache=false
spring.thymeleaf.mode=LEGACYHTML5
3、View層
我們新建了六個頁面用來測試:
- index.html :首頁
- login.html :登入頁
- userInfo.html : 使用者資訊頁面
- userInfoAdd.html :新增使用者頁面
- userInfoDel.html :刪除使用者頁面
- 403.html : 沒有許可權的頁面
登入頁面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
錯誤資訊:<h4 th:text="${msg}"></h4>
<form action="" method="post">
<p>賬號:<input type="text" name="username" value="admin"/></p>
<p>密碼:<input type="text" name="password" value="123456"/></p>
<p><input type="submit" value="登入"/></p>
</form>
</body>
</html>
其他頁面
除登入頁面其它都很簡單,大概如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--index 這裡該為使用者資訊頁面,或者新增使用者頁面等-->
<h1>index</h1>
</body>
</html>
4、Entity層
JPA是什麼?
JPA(Java Persistence API)是Sun官方提出的Java持久化規範。它為Java開發人員提供了一種物件/關聯對映工具來管理Java應用中的關係資料。他的出現主要是為了簡化現有的持久化開發工作和整合ORM技術,結束現在Hibernate,TopLink,JDO等ORM框架各自為營的局面。值得注意的是,JPA是在充分吸收了現有Hibernate,TopLink,JDO等ORM框架的基礎上發展而來的,具有易於使用,伸縮性強等優點。從目前的開發社群的反應上看,JPA受到了極大的支援和讚揚,其中就包括了Spring與EJB3.0的開發團隊。
注意:JPA是一套規範,不是一套產品,那麼像Hibernate,TopLink,JDO他們是一套產品,如果說這些產品實現了這個JPA規範,那麼我們就可以叫他們為JPA的實現產品。
spring data jpa
Spring Data JPA 是 Spring 基於 ORM 框架、JPA 規範的基礎上封裝的一套JPA應用框架,可使開發者用極簡的程式碼即可實現對資料的訪問和操作。它提供了包括增刪改查等在內的常用功能,且易於擴充套件!學習並使用 Spring Data JPA 可以極大提高開發效率!
spring data jpa讓我們解脫了DAO層的操作,基本上所有CRUD都可以依賴於它來實現
RBAC是什麼?
RBAC 是基於角色的訪問控制(Role-Based Access Control )在 RBAC 中,許可權與角色相關聯,使用者通過成為適當角色的成員而得到這些角色的許可權。這就極大地簡化了許可權的管理。這樣管理都是層級相互依賴的,許可權賦予給角色,而把角色又賦予使用者,這樣的許可權設計很清楚,管理起來很方便。
採用jpa技術來自動生成基礎表格,對應的entity如下:
使用者資訊
@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 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 `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);
5、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
Filter Name | Class |
---|---|
anon | org.apache.shiro.web.filter.authc.AnonymousFilter |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
port | org.apache.shiro.web.filter.authz.PortFilter |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
ssl | org.apache.shiro.web.filter.authz.SslFilter |
user | org.apache.shiro.web.filter.authc.UserFilter |
- anon:所有url都都可以匿名訪問
- authc: 需要認證才能進行訪問
- user:配置記住我或認證通過可以訪問
登入認證實現
在認證、授權內部實現機制中都有提到,最終處理都將交給Realm進行處理。因為在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、編寫好後就可以啟動程式,訪問http://localhost:8080/userInfo/userList頁面,由於沒有登入就會跳轉到http://localhost:8080/login頁面。登入之後就會跳轉到index頁面,登入後,直接在瀏覽器中輸入http://localhost:8080/userInfo/userList訪問就會看到使用者資訊。上面這些操作時候觸發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很強大,這僅僅是完成了登入認證和許可權管理這兩個功能,更多內容以後有時間再做探討。
參考:
Spring Boot Shiro許可權管理【從零開始學Spring Boot】