springboot(十四):springboot整合shiro-登陸認證和權限管理

這篇文章咱們來學習如何使用Spring Boot集成Apache Shiro。安全應該是互聯網公司的一道生命線,幾乎任何的公司都會涉及到這方面的需求。在Java領域通常有Spring Security、Apache Shiro等安全框架,可是因爲Spring Security過於龐大和複雜,大多數公司會選擇Apache Shiro來使用,這篇文章會先介紹一下Apache Shiro,在結合Spring Boot給出使用案例。html

Apache Shiro

What is Apache Shiro?

Apache Shiro是一個功能強大、靈活的,開源的安全框架。它能夠乾淨利落地處理身份驗證、受權、企業會話管理和加密。java

Apache Shiro的首要目標是易於使用和理解。安全一般很複雜,甚至讓人感到很痛苦,可是Shiro卻不是這樣子的。一個好的安全框架應該屏蔽複雜性,向外暴露簡單、直觀的API,來簡化開發人員實現應用程序安全所花費的時間和精力。mysql

Shiro能作什麼呢?git

  • 驗證用戶身份
  • 用戶訪問權限控制,好比:一、判斷用戶是否分配了必定的安全角色。二、判斷用戶是否被授予完成某個操做的權限
  • 在非 web 或 EJB 容器的環境下能夠任意使用Session API
  • 能夠響應認證、訪問控制,或者 Session 生命週期中發生的事件
  • 可將一個或以上用戶安全數據源數據組合成一個複合的用戶 "view"(視圖)
  • 支持單點登陸(SSO)功能
  • 支持提供「Remember Me」服務,獲取用戶關聯信息而無需登陸

等等——都集成到一個有凝聚力的易於使用的API。github

Shiro 致力在全部應用環境下實現上述功能,小到命令行應用程序,大到企業應用中,並且不須要藉助第三方框架、容器、應用服務器等。固然 Shiro 的目的是儘可能的融入到這樣的應用環境中去,但也能夠在它們以外的任何環境下開箱即用。web

Apache Shiro Features 特性

Apache Shiro是一個全面的、蘊含豐富功能的安全框架。下圖爲描述Shiro功能的框架圖:算法

Authentication(認證), Authorization(受權), Session Management(會話管理), Cryptography(加密)被 Shiro 框架的開發團隊稱之爲應用安全的四大基石。那麼就讓咱們來看看它們吧:spring

  • Authentication(認證):用戶身份識別,一般被稱爲用戶「登陸」
  • Authorization(受權):訪問控制。好比某個用戶是否具備某個操做的使用權限。
  • Session Management(會話管理):特定於用戶的會話管理,甚至在非web 或 EJB 應用程序。
  • Cryptography(加密):在對數據源使用加密算法加密的同時,保證易於使用。

還有其餘的功能來支持和增強這些不一樣應用環境下安全領域的關注點。特別是對如下的功能支持:sql

  • 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定義說明:

  • 一、一個URL能夠配置多個Filter,使用逗號分隔
  • 二、當設置多個過濾器時,所有驗證經過,才視爲經過
  • 三、部分過濾器可指定參數,如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)方法。

該方法主要執行如下操做:

  • 一、檢查提交的進行認證的令牌信息
  • 二、根據令牌信息從數據源(一般爲數據庫)中獲取用戶信息
  • 三、對用戶信息進行匹配驗證。
  • 四、驗證經過將返回一個封裝了用戶信息的AuthenticationInfo實例。
  • 五、驗證失敗則拋出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的代碼就不貼出來了你們直接看代碼。

測試

一、編寫好後就能夠啓動程序,訪問index頁面,因爲沒有登陸就會跳轉到login頁面。登陸以後就會跳轉到index頁面,登陸後,有直接在瀏覽器中輸入index頁面訪問,又會跳轉到login頁面。上面這些操做時候觸發MyShiroRealm.doGetAuthenticationInfo()這個方法,也就是登陸認證的方法。

二、登陸admin帳戶,訪問:http://127.0.0.1:8080/userInfo/userAdd顯示用戶添加界面,訪問http://127.0.0.1:8080/userInfo/userDel顯示403沒有權限。上面這些操做時候觸發MyShiroRealm.doGetAuthorizationInfo()這個方面,也就是權限校驗的方法。

三、修改admin不一樣的權限進行測試

shiro很強大,這僅僅是完成了登陸認證和權限管理這兩個功能,更多內容之後有時間再作探討。

示例代碼

參考:

Apache Shiro中文手冊
Spring Boot Shiro權限管理【從零開始學Spring Boot】

SpringBoot+shiro整合學習之登陸認證和權限控制

喜歡個人文章,請關注個人公衆號

做者:純潔的微笑
出處:www.ityouknow.com/
版權歸做者全部,轉載請註明出處

相關文章
相關標籤/搜索