12.23 Spring Boot 整合 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 功能的框架圖:

Spring Boot 整合 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。下面的圖展示了這些組件如何相互作用,我們將在下面依次對其進行描述。

Spring Boot 整合 Shiro-登錄認證和權限管理

  • Subject:當前用戶,Subject 可以是一個人,但也可以是第三方服務、守護進程帳戶、時鐘守護任務或者其它–當前和軟件交互的任何事件。
  • SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架構的核心,配合內部安全組件共同組成安全傘。
  • Realms:用於進行權限信息的驗證,我們自己實現。Realm 本質上是一個特定的安全 DAO:它封裝與數據源連接的細節,得到Shiro 所需的相關的數據。在配置 Shiro 的時候,你必須指定至少一個Realm 來實現認證(authentication)和/或授權(authorization)。

我們需要實現Realms的Authentication 和 Authorization。其中 Authentication 是用來驗證用戶身份,Authorization 是授權訪問控制,用於對用戶進行的操作授權,證明該用戶是否允許進行當前操作,如訪問某個鏈接,某個資源文件等。

快速上手

基礎信息

pom包依賴

<code><dependencies>
\t<dependency>
\t\t<groupid>org.springframework.boot/<groupid>
\t\t<artifactid>spring-boot-starter-data-jpa/<artifactid>
\t/<dependency>

\t<dependency>
\t\t<groupid>org.springframework.boot/<groupid>
\t\t<artifactid>spring-boot-starter-thymeleaf/<artifactid>
\t/<dependency>
\t<dependency>
\t\t<groupid>net.sourceforge.nekohtml/<groupid>
\t\t<artifactid>nekohtml/<artifactid>
\t\t<version>1.9.22/<version>
\t/<dependency>
\t<dependency>
\t\t<groupid>org.springframework.boot/<groupid>
\t\t<artifactid>spring-boot-starter-web/<artifactid>
\t/<dependency>
\t<dependency>
\t\t<groupid>org.apache.shiro/<groupid>
\t\t<artifactid>shiro-spring/<artifactid>
\t\t<version>1.4.0/<version>
\t/<dependency>
\t<dependency>
\t\t<groupid>mysql/<groupid>
\t\t<artifactid>mysql-connector-java/<artifactid>
\t\t<scope>runtime/<scope>
\t/<dependency>
/<dependencies>
/<code>

重點是 shiro-spring 包

配置文件

<code>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
/<code>

thymeleaf的配置是為了去掉html的校驗

頁面

我們新建了六個頁面用來測試:

  • index.html :首頁
  • login.html :登錄頁
  • userInfo.html : 用戶信息頁面
  • userInfoAdd.html :添加用戶頁面
  • userInfoDel.html :刪除用戶頁面
  • 403.html : 沒有權限的頁面

除過登錄頁面其它都很簡單,大概如下:

<code>



<title>Title/<title>


index




/<code>

RBAC

RBAC 是基於角色的訪問控制(Role-Based Access Control )在 RBAC 中,權限與角色相關聯,用戶通過成為適當角色的成員而得到這些角色的權限。這就極大地簡化了權限的管理。這樣管理都是層級相互依賴的,權限賦予給角色,而把角色又賦予用戶,這樣的權限設計很清楚,管理起來很方便。

採用 Jpa 技術來自動生成基礎表格,對應的實體如下:

用戶信息

<code>@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 方法
}

/<sysrole>/<code>

角色信息

<code>@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 方法
}
/<userinfo>/<syspermission>/<code>

權限信息

<code>@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 方法
}
/<sysrole>/<code>

根據以上的代碼會自動生成 user_info(用戶信息表)、sys_role(角色表)、sys_permission(權限表)、sys_user_role(用戶角色表)、sys_role_permission(角色權限表)這五張表,為了方便測試我們給這五張表插入一些初始化數據:

<code>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);
/<code>

Shiro 配置

首先要配置的是 ShiroConfig 類,Apache Shiro 核心通過 Filter 來實現,就好像 SpringMvc 通過 DispachServlet 來主控制一樣。 既然是使用 Filter 一般也就能猜到,是通過 URL 規則來進行過濾和權限校驗,所以我們需要定義一系列關於 URL 的規則和訪問權限。

ShiroConfig

<code>@Configuration
public class ShiroConfig {
\t@Bean
\tpublic ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
\t\tSystem.out.println("ShiroConfiguration.shirFilter()");
\t\tShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
\t\tshiroFilterFactoryBean.setSecurityManager(securityManager);
\t\t//攔截器.
\t\tMap<string> filterChainDefinitionMap = new LinkedHashMap<string>();
\t\t// 配置不會被攔截的鏈接 順序判斷
\t\tfilterChainDefinitionMap.put("/static/**", "anon");
\t\t//配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了
\t\tfilterChainDefinitionMap.put("/logout", "logout");
\t\t//:這是一個坑呢,一不小心代碼就不好使了;
\t\t//
\t\tfilterChainDefinitionMap.put("/**", "authc");
\t\t// 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
\t\tshiroFilterFactoryBean.setLoginUrl("/login");
\t\t// 登錄成功後要跳轉的鏈接
\t\tshiroFilterFactoryBean.setSuccessUrl("/index");

\t\t//未授權界面;
\t\tshiroFilterFactoryBean.setUnauthorizedUrl("/403");
\t\tshiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
\t\treturn shiroFilterFactoryBean;
\t}

\t@Bean
\tpublic MyShiroRealm myShiroRealm(){
\t\tMyShiroRealm myShiroRealm = new MyShiroRealm();
\t\treturn myShiroRealm;
\t}


\t@Bean
\tpublic SecurityManager securityManager(){
\t\tDefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
\t\tsecurityManager.setRealm(myShiroRealm());
\t\treturn securityManager;
\t}
}
/<string>/<string>/<code>

Filter Chain 定義說明:

  • 1、一個URL可以配置多個 Filter,使用逗號分隔
  • 2、當設置多個過濾器時,全部驗證通過,才視為通過
  • 3、部分過濾器可指定參數,如 perms,roles

Shiro 內置的 FilterChain

Filter NameClassanonorg.apache.shiro.web.filter.authc.AnonymousFilterauthcorg.apache.shiro.web.filter.authc.FormAuthenticationFilterauthcBasicorg.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilterpermsorg.apache.shiro.web.filter.authz.PermissionsAuthorizationFilterportorg.apache.shiro.web.filter.authz.PortFilterrestorg.apache.shiro.web.filter.authz.HttpMethodPermissionFilterrolesorg.apache.shiro.web.filter.authz.RolesAuthorizationFiltersslorg.apache.shiro.web.filter.authz.SslFilteruserorg.apache.shiro.web.filter.authc.UserFilter

  • 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 的重寫

<code>@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;
}
/<code>

鏈接權限的實現

Shiro 的權限授權是通過繼承AuthorizingRealm抽象類,重載doGetAuthorizationInfo();當訪問到頁面的時候,鏈接配置了相應的權限或者 Shiro 標籤才會執行此方法否則不會執行,所以如果只是簡單的身份認證沒有權限的控制的話,那麼這個方法可以不進行實現,直接返回 null 即可。在這個方法中主要是使用類:SimpleAuthorizationInfo進行角色的添加和權限的添加。

<code>@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;
}
/<code>

當然也可以添加 set 集合:roles 是從數據庫查詢的當前用戶的角色,stringPermissions 是從數據庫查詢的當前用戶對應的權限

<code>authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(stringPermissions);
/<code>

就是說如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “perms[權限添加]”);就說明訪問/add這個鏈接必須要有“權限添加”這個權限才可以訪問,如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “roles[100002],perms[權限添加]”);就說明訪問/add這個鏈接必須要有“權限添加”這個權限和具有“100002”這個角色才可以訪問。

登錄實現

登錄過程其實只是處理異常的相關信息,具體的登錄驗證交給 Shiro 來處理

<code>@RequestMapping("/login")
public String login(HttpServletRequest request, Map<string> 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";
}
/<string>/<code>

其它 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 很強大,這僅僅是完成了登錄認證和權限管理這兩個功能,更多內容以後有時間再做探討。


分享到:


相關文章: