Shiro安全框架【快速入門】就這一篇!

  • 2020 年 3 月 13 日
  • 筆記

本文公眾號來源:我沒有三顆心臟作者:我沒有三顆心臟

Shiro 簡介

照例又去官網扒了扒介紹:

Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro』s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications. Apache Shiro™是一個強大且易用的Java安全框架,能夠用於身份驗證、授權、加密和會話管理。Shiro擁有易於理解的API,您可以快速、輕鬆地獲得任何應用程式——從最小的移動應用程式到最大的網路和企業應用程式。

簡而言之,Apache Shiro 是一個強大靈活的開源安全框架,可以完全處理身份驗證、授權、加密和會話管理。

Shiro能到底能做些什麼呢?

  • 驗證用戶身份
  • 用戶訪問許可權控制,比如:1、判斷用戶是否分配了一定的安全形色。2、判斷用戶是否被授予完成某個操作的許可權
  • 在非 Web 或 EJB 容器的環境下可以任意使用Session API
  • 可以響應認證、訪問控制,或者 Session 生命周期中發生的事件
  • 可將一個或以上用戶安全數據源數據組合成一個複合的用戶 「view」(視圖)
  • 支援單點登錄(SSO)功能
  • 支援提供「Remember Me」服務,獲取用戶關聯資訊而無需登錄 ···

為什麼是 Shiro?

使用 Shiro 官方給了許多令人信服的原因,因為 Shiro 具有以下幾個特點:

  • 易於使用——易用性是項目的最終目標。應用程式安全非常令人困惑和沮喪,被認為是「不可避免的災難」。如果你讓它簡化到新手都可以使用它,它就將不再是一種痛苦了。
  • 全面——沒有其他安全框架的寬度範圍可以同Apache Shiro一樣,它可以成為你的「一站式」為您的安全需求提供保障。
  • 靈活——Apache Shiro可以在任何應用程式環境中工作。雖然在網路工作、EJB和IoC環境中可能並不需要它。但Shiro的授權也沒有任何規範,甚至沒有許多依賴關係。
  • Web支援——Apache Shiro擁有令人興奮的web應用程式支援,允許您基於應用程式的url創建靈活的安全策略和網路協議(例如REST),同時還提供一組JSP庫控制頁面輸出。
  • 低耦合——Shiro乾淨的API和設計模式使它容易與許多其他框架和應用程式集成。你會看到Shiro無縫地集成Spring這樣的框架, 以及Grails, Wicket, Tapestry, Mule, Apache Camel, Vaadin…等。
  • 被廣泛支援——Apache Shiro是Apache軟體基金會的一部分。項目開發和用戶組都有友好的網民願意幫助。這樣的商業公司如果需要Katasoft還提供專業的支援和服務。

有興趣的可以去仔細看看官方的文檔:【傳送門】

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 API中的第一級,以確保安全操作保持快速和高效。
  • 並發性:Apache Shiro支援具有並發功能的多執行緒應用程式。
  • 測試:存在測試支援,可幫助您編寫單元測試和集成測試,並確保程式碼按預期得到保障。
  • 「運行方式」:允許用戶承擔另一個用戶的身份(如果允許)的功能,有時在管理方案中很有用。
  • 「記住我」:記住用戶在會話中的身份,所以用戶只需要強制登錄即可。

注意: 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 是授權訪問控制,用於對用戶進行的操作授權,證明該用戶是否允許進行當前操作,如訪問某個鏈接,某個資源文件等。

Shiro 認證過程

上圖展示了 Shiro 認證的一個重要的過程,為了加深我們的印象,我們來自己動手來寫一個例子,來驗證一下,首先我們新建一個Maven工程,然後在pom.xml中引入相關依賴:

<dependency>      <groupId>org.apache.shiro</groupId>      <artifactId>shiro-core</artifactId>      <version>1.4.0</version>  </dependency>  <dependency>      <groupId>junit</groupId>      <artifactId>junit</artifactId>      <version>4.12</version>  </dependency>  

新建一個【AuthenticationTest】測試類:

import org.apache.shiro.SecurityUtils;  import org.apache.shiro.authc.UsernamePasswordToken;  import org.apache.shiro.mgt.DefaultSecurityManager;  import org.apache.shiro.realm.SimpleAccountRealm;  import org.apache.shiro.subject.Subject;  import org.junit.Before;  import org.junit.Test;    public class AuthenticationTest {        SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();        @Before // 在方法開始前添加一個用戶      public void addUser() {          simpleAccountRealm.addAccount("wmyskxz", "123456");      }        @Test      public void testAuthentication() {            // 1.構建SecurityManager環境          DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();          defaultSecurityManager.setRealm(simpleAccountRealm);            // 2.主體提交認證請求          SecurityUtils.setSecurityManager(defaultSecurityManager); // 設置SecurityManager環境          Subject subject = SecurityUtils.getSubject(); // 獲取當前主體            UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");          subject.login(token); // 登錄            // subject.isAuthenticated()方法返回一個boolean值,用於判斷用戶是否認證成功          System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 輸出true            subject.logout(); // 登出            System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 輸出false      }  }  

運行之後可以看到預想中的效果,先輸出isAuthenticated:true表示登錄認證成功,然後再輸出isAuthenticated:false表示認證失敗退出登錄,再來一張圖加深一下印象:

流程如下:

  1. 首先調用 Subject.login(token) 進行登錄,其會自動委託給 Security Manager,調用之前必須通過 SecurityUtils.setSecurityManager() 設置;
  2. SecurityManager 負責真正的身份驗證邏輯;它會委託給 Authenticator 進行身份驗證;
  3. Authenticator 才是真正的身份驗證者,Shiro API 中核心的身份認證入口點,此處可以自定義插入自己的實現;
  4. Authenticator 可能會委託給相應的 AuthenticationStrategy 進行多 Realm 身份驗證,默認 ModularRealmAuthenticator 會調用 AuthenticationStrategy 進行多 Realm 身份驗證;
  5. Authenticator 會把相應的 token 傳入 Realm,從 Realm 獲取身份驗證資訊,如果沒有返回 / 拋出異常表示身份驗證失敗了。此處可以配置多個 Realm,將按照相應的順序及策略進行訪問。

Shiro 授權過程

跟認證過程大致相似,下面我們仍然通過程式碼來熟悉一下過程(引入包類似這裡節約篇幅就不貼出來了):

public class AuthenticationTest {        SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();        @Before // 在方法開始前添加一個用戶,讓它具備admin和user兩個角色      public void addUser() {          simpleAccountRealm.addAccount("wmyskxz", "123456", "admin", "user");      }        @Test      public void testAuthentication() {            // 1.構建SecurityManager環境          DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();          defaultSecurityManager.setRealm(simpleAccountRealm);            // 2.主體提交認證請求          SecurityUtils.setSecurityManager(defaultSecurityManager); // 設置SecurityManager環境          Subject subject = SecurityUtils.getSubject(); // 獲取當前主體            UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");          subject.login(token); // 登錄            // subject.isAuthenticated()方法返回一個boolean值,用於判斷用戶是否認證成功          System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 輸出true          // 判斷subject是否具有admin和user兩個角色許可權,如沒有則會報錯          subject.checkRoles("admin","user");  //        subject.checkRole("xxx"); // 報錯      }  }  

運行測試,能夠正確看到效果。

自定義 Realm

從上面我們了解到實際進行許可權資訊驗證的是我們的 Realm,Shiro 框架內部默認提供了兩種實現,一種是查詢.ini文件的IniRealm,另一種是查詢資料庫的JdbcRealm,這兩種來說都相對簡單,感興趣的可以去【這裡】瞄兩眼,我們著重就來介紹介紹自定義實現的 Realm 吧。

有了上面的對認證和授權的理解,我們先在合適的包下創建一個【MyRealm】類,繼承 Shirot 框架的 AuthorizingRealm 類,並實現默認的兩個方法:

package com.wmyskxz.demo.realm;    import org.apache.shiro.authc.*;  import org.apache.shiro.realm.AuthorizingRealm;  import org.apache.shiro.subject.PrincipalCollection;    import java.util.*;    public class MyRealm extends AuthorizingRealm {        /**       * 模擬資料庫數據       */      Map<String, String> userMap = new HashMap<>(16);        {          userMap.put("wmyskxz", "123456");          super.setName("myRealm"); // 設置自定義Realm的名稱,取什麼無所謂..      }        /**       * 授權       *       * @param principalCollection       * @return       */      @Override      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {          String userName = (String) principalCollection.getPrimaryPrincipal();          // 從資料庫獲取角色和許可權數據          Set<String> roles = getRolesByUserName(userName);          Set<String> permissions = getPermissionsByUserName(userName);            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();          simpleAuthorizationInfo.setStringPermissions(permissions);          simpleAuthorizationInfo.setRoles(roles);          return simpleAuthorizationInfo;      }        /**       * 模擬從資料庫中獲取許可權數據       *       * @param userName       * @return       */      private Set<String> getPermissionsByUserName(String userName) {          Set<String> permissions = new HashSet<>();          permissions.add("user:delete");          permissions.add("user:add");          return permissions;      }        /**       * 模擬從資料庫中獲取角色數據       *       * @param userName       * @return       */      private Set<String> getRolesByUserName(String userName) {          Set<String> roles = new HashSet<>();          roles.add("admin");          roles.add("user");          return roles;      }        /**       * 認證       *       * @param authenticationToken 主體傳過來的認證資訊       * @return       * @throws AuthenticationException       */      @Override      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {          // 1.從主體傳過來的認證資訊中,獲得用戶名          String userName = (String) authenticationToken.getPrincipal();            // 2.通過用戶名到資料庫中獲取憑證          String password = getPasswordByUserName(userName);          if (password == null) {              return null;          }          SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("wmyskxz", password, "myRealm");          return authenticationInfo;      }        /**       * 模擬從資料庫取憑證的過程       *       * @param userName       * @return       */      private String getPasswordByUserName(String userName) {          return userMap.get(userName);      }  }  

然後我們編寫測試類,來驗證是否正確:

import com.wmyskxz.demo.realm.MyRealm;  import org.apache.shiro.SecurityUtils;  import org.apache.shiro.authc.UsernamePasswordToken;  import org.apache.shiro.mgt.DefaultSecurityManager;  import org.apache.shiro.subject.Subject;  import org.junit.Test;    public class AuthenticationTest {        @Test      public void testAuthentication() {            MyRealm myRealm = new MyRealm(); // 實現自己的 Realm 實例            // 1.構建SecurityManager環境          DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();          defaultSecurityManager.setRealm(myRealm);            // 2.主體提交認證請求          SecurityUtils.setSecurityManager(defaultSecurityManager); // 設置SecurityManager環境          Subject subject = SecurityUtils.getSubject(); // 獲取當前主體            UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");          subject.login(token); // 登錄            // subject.isAuthenticated()方法返回一個boolean值,用於判斷用戶是否認證成功          System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 輸出true          // 判斷subject是否具有admin和user兩個角色許可權,如沒有則會報錯          subject.checkRoles("admin", "user");  //        subject.checkRole("xxx"); // 報錯          // 判斷subject是否具有user:add許可權          subject.checkPermission("user:add");      }  }  

運行測試,完美。

Shiro 加密

在之前的學習中,我們在資料庫中保存的密碼都是明文的,一旦資料庫數據泄露,那就會造成不可估算的損失,所以我們通常都會使用非對稱加密,簡單理解也就是不可逆的加密,而 md5 加密演算法就是符合這樣的一種演算法。

如上面的 123456 用 Md5 加密後,得到的字元串:e10adc3949ba59abbe56e057f20f883e,就無法通過計算還原回 123456,我們把這個加密的字元串保存在資料庫中,等下次用戶登錄時我們把密碼通過同樣的演算法加密後再從資料庫中取出這個字元串進行比較,就能夠知道密碼是否正確了,這樣既保留了密碼驗證的功能又大大增加了安全性,但是問題是:雖然無法直接通過計算反推回密碼,但是我們仍然可以通過計算一些簡單的密碼加密後的 Md5 值進行比較,推算出原來的密碼

比如我的密碼是 123456,你的密碼也是,通過 md5 加密之後的字元串一致,所以你也就能知道我的密碼了,如果我們把常用的一些密碼都做 md5 加密得到一本字典,那麼就可以得到相當一部分的人密碼,這也就相當於「破解」了一樣,所以其實也沒有我們想像中的那麼「安全」。

加鹽 + 多次加密

既然相同的密碼 md5 一樣,那麼我們就讓我們的原始密碼再加一個隨機數,然後再進行 md5 加密,這個隨機數就是我們說的鹽(salt),這樣處理下來就能得到不同的 Md5 值,當然我們需要把這個隨機數鹽也保存進資料庫中,以便我們進行驗證。

另外我們可以通過多次加密的方法,即使黑客通過一定的技術手段拿到了我們的密碼 md5 值,但它並不知道我們到底加密了多少次,所以這也使得破解工作變得艱難。

在 Shiro 框架中,對於這樣的操作提供了簡單的程式碼實現:

String password = "123456";  String salt = new SecureRandomNumberGenerator().nextBytes().toString();  int times = 2;  // 加密次數:2  String alogrithmName = "md5";   // 加密演算法    String encodePassword = new SimpleHash(alogrithmName, password, salt, times).toString();    System.out.printf("原始密碼是 %s , 鹽是: %s, 運算次數是: %d, 運算出來的密文是:%s ",password,salt,times,encodePassword);  

輸出:

原始密碼是 123456 , 鹽是: f5GQZsuWjnL9z585JjLrbQ==, 運算次數是: 2, 運算出來的密文是:55fee80f73537cefd6b3c9a920993c25  

SpringBoot 簡單實例

通過上面的學習,我們現在來著手搭建一個簡單的使用 Shiro 進行許可權驗證授權的一個簡單系統

第一步:新建SpringBoot項目,搭建基礎環境

pom包:

<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>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-web</artifactId>  </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.apache.shiro</groupId>      <artifactId>shiro-spring</artifactId>      <version>1.4.0</version>  </dependency>  

application.properties文件:

#thymeleaf 配置  spring.thymeleaf.mode=HTML5  spring.thymeleaf.encoding=UTF-8  spring.thymeleaf.servlet.content-type=text/html  #快取設置為false, 這樣修改之後馬上生效,便於調試  spring.thymeleaf.cache=false    #資料庫  spring.datasource.url=jdbc:mysql://127.0.0.1:3306/testdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC  spring.datasource.username=root  spring.datasource.password=123456  spring.datasource.driver-class-name=com.mysql.jdbc.Driver  spring.jpa.properties.hibernate.hbm2ddl.auto=update  #顯示SQL語句  spring.jpa.show-sql=true  #不加下面這句則不會默認創建MyISAM引擎的資料庫  spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect  #自己重寫的配置類,默認使用utf8編碼  spring.jpa.properties.hibernate.dialect=com.wmyskxz.demo.shiro.config.MySQLConfig  

第二步:新建實體類

新建一個【entity】包,在下面創建以下實體:

用戶資訊:

@Entity  public class UserInfo {      @Id      @GeneratedValue      private Long id; // 主鍵.      @Column(unique = true)      private String username; // 登錄賬戶,唯一.      private String name; // 名稱(匿名或真實姓名),用於UI顯示      private String password; // 密碼.      private String salt; // 加密密碼的鹽      @JsonIgnoreProperties(value = {"userInfos"})      @ManyToMany(fetch = FetchType.EAGER) // 立即從資料庫中進行載入數據      @JoinTable(name = "SysUserRole", joinColumns = @JoinColumn(name = "uid"), inverseJoinColumns = @JoinColumn(name = "roleId"))      private List<SysRole> roles; // 一個用戶具有多個角色        /** getter and setter */  }  

角色資訊:

@Entity  public class SysRole {      @Id      @GeneratedValue      private Long id; // 主鍵.      private String name; // 角色名稱,如 admin/user      private String description; // 角色描述,用於UI顯示        // 角色 -- 許可權關係:多對多      @JsonIgnoreProperties(value = {"roles"})      @ManyToMany(fetch = FetchType.EAGER)      @JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "permissionId")})      private List<SysPermission> permissions;        // 用戶 -- 角色關係:多對多      @JsonIgnoreProperties(value = {"roles"})      @ManyToMany      @JoinTable(name = "SysUserRole", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "uid")})      private List<UserInfo> userInfos;// 一個角色對應多個用戶        /** getter and setter */  }  

許可權資訊:

@Entity  public class SysPermission {      @Id      @GeneratedValue      private Long id; // 主鍵.      private String name; // 許可權名稱,如 user:select      private String description; // 許可權描述,用於UI顯示      private String url; // 許可權地址.      @JsonIgnoreProperties(value = {"permissions"})      @ManyToMany      @JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "permissionId")}, inverseJoinColumns = {@JoinColumn(name = "roleId")})      private List<SysRole> roles; // 一個許可權可以被多個角色使用        /** getter and setter */  }  

注意:這裡有一個坑,還纏了我蠻久感覺,就是當我們想要使用RESTful風格返回給前台JSON數據的時候,這裡有一個關於多對多無限循環的坑,比如當我們想要返回給前台一個用戶資訊時,由於一個用戶擁有多個角色,一個角色又擁有多個許可權,而許可權跟角色也是多對多的關係,也就是造成了 查用戶→查角色→查許可權→查角色→查用戶… 這樣的無限循環,導致傳輸錯誤,所以我們根據這樣的邏輯在每一個實體類返回JSON時使用了一個@JsonIgnoreProperties註解,來排除自己對自己無線引用的過程,也就是打斷這樣的無限循環。

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

INSERT INTO `user_info` (`id`,`name`,`password`,`salt`,`username`) VALUES (1, '管理員','951cd60dec2104024949d2e0b2af45ae', 'xbNIxrQfn6COSYn1/GdloA==', 'wmyskxz');  INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (1,'查詢用戶','userInfo:view','/userList');  INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (2,'增加用戶','userInfo:add','/userAdd');  INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (3,'刪除用戶','userInfo:delete','/userDelete');  INSERT INTO `sys_role` (`id`,`description`,`name`) VALUES (1,'管理員','admin');  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_user_role` (`role_id`,`uid`) VALUES (1,1);  

第三步:配置 Shiro

新建一個【config】包,在下面創建以下文件:

MySQLConfig:

public class MySQLConfig extends MySQL5InnoDBDialect {      @Override      public String getTableTypeString() {          return "ENGINE=InnoDB DEFAULT CHARSET=utf8";      }  }  

這個文件關聯的是配置文件中最後一個配置,是讓 Hibernate 默認創建 InnoDB 引擎並默認使用 utf-8 編碼

MyShiroRealm:

public class MyShiroRealm extends AuthorizingRealm {      @Resource      private UserInfoService userInfoService;        @Override      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {          // 能進入這裡說明用戶已經通過驗證了          UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal();          SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();          for (SysRole role : userInfo.getRoles()) {              simpleAuthorizationInfo.addRole(role.getName());              for (SysPermission permission : role.getPermissions()) {                  simpleAuthorizationInfo.addStringPermission(permission.getName());              }          }          return simpleAuthorizationInfo;      }        @Override      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {          // 獲取用戶輸入的賬戶          String username = (String) authenticationToken.getPrincipal();          System.out.println(authenticationToken.getPrincipal());          // 通過username從資料庫中查找 UserInfo 對象          // 實際項目中,這裡可以根據實際情況做快取,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法          UserInfo userInfo = userInfoService.findByUsername(username);          if (null == userInfo) {              return null;          }            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(                  userInfo, // 用戶名                  userInfo.getPassword(), // 密碼                  ByteSource.Util.bytes(userInfo.getSalt()), // salt=username+salt                  getName() // realm name          );          return simpleAuthenticationInfo;      }  }  

自定義的 Realm ,方法跟上面的認證授權過程一致

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;      }        /**       * 憑證匹配器       * (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了)       *       * @return       */      @Bean      public HashedCredentialsMatcher hashedCredentialsMatcher() {          HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();          hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 散列演算法:這裡使用MD5演算法;          hashedCredentialsMatcher.setHashIterations(2); // 散列的次數,比如散列兩次,相當於 md5(md5(""));          return hashedCredentialsMatcher;      }        @Bean      public MyShiroRealm myShiroRealm() {          MyShiroRealm myShiroRealm = new MyShiroRealm();          myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());          return myShiroRealm;      }          @Bean      public SecurityManager securityManager() {          DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();          securityManager.setRealm(myShiroRealm());          return securityManager;      }        /**       * 開啟shiro aop註解支援.       * 使用代理方式;所以需要開啟程式碼支援;       *       * @param securityManager       * @return       */      @Bean      public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {          AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();          authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);          return authorizationAttributeSourceAdvisor;      }        @Bean(name = "simpleMappingExceptionResolver")      public SimpleMappingExceptionResolver      createSimpleMappingExceptionResolver() {          SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();          Properties mappings = new Properties();          mappings.setProperty("DatabaseException", "databaseError"); // 資料庫異常處理          mappings.setProperty("UnauthorizedException", "403");          r.setExceptionMappings(mappings);  // None by default          r.setDefaultErrorView("error");    // No default          r.setExceptionAttribute("ex");     // Default is "exception"          //r.setWarnLogCategory("example.MvcLogger");     // No default          return r;      }  }  

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

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:配置記住我或認證通過可以訪問

第四步:準備 DAO 層和 Service 層

新建【dao】包,在下面創建【UserInfoDao】介面:

public interface UserInfoDao extends JpaRepository<UserInfo, Long> {      /** 通過username查找用戶資訊*/      public UserInfo findByUsername(String username);  }  

新建【service】包,創建【UserInfoService】介面:

public interface UserInfoService {      /** 通過username查找用戶資訊;*/      public UserInfo findByUsername(String username);  }  

並在該包下再新建一個【impl】包,新建【UserInfoServiceImpl】實現類:

@Service  public class UserInfoServiceImpl implements UserInfoService {        @Resource      UserInfoDao userInfoDao;        @Override      public UserInfo findByUsername(String username) {          return userInfoDao.findByUsername(username);      }  }  

第五步:controller層

新建【controller】包,然後在下面創建以下文件:

HomeController:

@Controller  public class HomeController {        @RequestMapping({"/","/index"})      public String index(){          return"/index";      }        @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";      }        @RequestMapping("/403")      public String unauthorizedRole(){          System.out.println("------沒有許可權-------");          return "403";      }  }  

這裡邊的地址對應我們在設置 Shiro 時設置的地址

UserInfoController:

@RestController  public class UserInfoController {        @Resource      UserInfoService userInfoService;        /**       * 按username賬戶從資料庫中取出用戶資訊       *       * @param username 賬戶       * @return       */      @GetMapping("/userList")      @RequiresPermissions("userInfo:view") // 許可權管理.      public UserInfo findUserInfoByUsername(@RequestParam String username) {          return userInfoService.findByUsername(username);      }        /**       * 簡單模擬從資料庫添加用戶資訊成功       *       * @return       */      @PostMapping("/userAdd")      @RequiresPermissions("userInfo:add")      public String addUserInfo() {          return "addUserInfo success!";      }        /**       * 簡單模擬從資料庫刪除用戶成功       *       * @return       */      @DeleteMapping("/userDelete")      @RequiresPermissions("userInfo:delete")      public String deleteUserInfo() {          return "deleteUserInfo success!";      }  }  

第六步:準備頁面

新建三個頁面用來測試:

index.html:首頁

<!DOCTYPE html>  <head>      <meta charset="UTF-8">      <title>首頁</title>  </head>  <body>  index - 首頁  </body>  </html>  

login.html:登錄頁

<!DOCTYPE html>  <html xmlns:th="http://www.w3.org/1999/xhtml">  <head>      <meta charset="UTF-8">      <title>登錄頁</title>  </head>  <body>  錯誤資訊:<h4 th:text="${msg}"></h4>  <form action="" method="post">      <p>帳號:<input type="text" name="username" value="wmyskxz"/></p>      <p>密碼:<input type="text" name="password" value="123456"/></p>      <p><input type="submit" value="登錄"/></p>  </form>  </body>  </html>  

403.html:沒有許可權的頁面

<!DOCTYPE html>  <head>      <meta charset="UTF-8">      <title>403錯誤頁</title>  </head>  <body>  錯誤頁面  </body>  </html>  

第七步:測試

  1. 編寫好程式後就可以啟動,首先訪問http://localhost:8080/userList?username=wmyskxz頁面,由於沒有登錄就會跳轉到我們配置好的http://localhost:8080/login頁面。登陸之後就會看到正確返回的JSON數據,上面這些操作時候觸發MyShiroRealm.doGetAuthenticationInfo()這個方法,也就是登錄認證的方法。
  2. 登錄之後,我們還能訪問http://localhost:8080/userAdd頁面,因為我們在資料庫中提前配置好了許可權,能夠看到正確返回的數據,但是我們訪問http://localhost:8080/userDelete時,就會返回錯誤頁面.

注意:以上測試需要在REST工具中測試,因為在Controller層中配置了方法,大家也可以不用REST風格來測試一下看看!