Spring Boot (十四): Spring Boot 整合 Shiro-登入認證和許可權管理

純潔的微笑發表於2017-06-28

這篇文章我們來學習如何使用 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 (十四): 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 (十四): Spring Boot 整合 Shiro-登入認證和許可權管理

  • 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 方法
 }

根據以上的程式碼會自動生成 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);

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

登入認證實現

在認證、授權內部實現機制中都有提到,最終處理都將交給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、編寫好後就可以啟動程式,訪問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 2.x

示例程式碼-github

示例程式碼-碼雲

參考:

Apache Shiro中文手冊
Spring Boot Shiro許可權管理【從零開始學Spring Boot】
SpringBoot+shiro整合學習之登入認證和許可權控制

相關文章