1. 程式人生 > 實用技巧 >Spring Boot 中整合 Shiro

Spring Boot 中整合 Shiro

Shiro 是一個強大、簡單易用的 Java 安全框架,可使認證、授權、加密,會話過程更便捷,並可為應用提供安全保障。本節課重點介紹下 Shiro 的認證和授權功能。

文章目錄

16.1 Shiro 三大核心元件

Shiro 有三大核心元件,即Subject、SecurityManager 和 Realm。先來看一下它們之間的關係。

16.1.1 Subject 為認證主體

包含Principals 和 Credentials兩個資訊。我們看下兩者的具體含義。

Principals:代表身份。可以是使用者名稱、郵件、手機號碼等等,用來標識一個登入主體的身份。

Credentials:代表憑證。常見的有密碼,數字證書等等。

說白了,兩者代表了需要認證的內容,最常見的便是使用者名稱、密碼了。比如使用者登入時,通過 Shiro 進行身份認證,其中就包括主體認證。

16.1.2 SecurityManager 為安全管理員

這是 Shiro 架構的核心,是 Shiro 內部所有原件的保護傘。專案中一般都會配置 SecurityManager,開發人員將大部分精力放在了 Subject 認證主體上,與 Subject 互動背後的安全操作,則由 SecurityManager 來完成。

16.1.3 Realm 是一個域

它是連線 Shiro 和具體應用的橋樑。當需要與安全資料互動時,比如使用者賬戶、訪問控制等,Shiro 將會在一個或多個 Realm 中查詢。我們可以把 Realm 看作 DataSource,即安全資料來源。一般,我們會自己定製 Realm,下文會詳細說明。

16.2 Shiro 身份和許可權認證

16.2.1 Shiro 身份認證

我們分析下 Shiro 身份認證的過程,首先看一下官方給出的認證圖。

從圖中可以看到,這個過程包括五步。

Step1:應用程式程式碼呼叫 Subject.login(token) 方法後,傳入代表終端使用者身份的 AuthenticationToken 例項 Token。

Step2:將 Subject 例項委託給應用程式的 SecurityManager(Shiro 的安全管理)並開始實際的認證工作。這裡開始了真正的認證工作。

Step3、4、5:SecurityManager 根據具體的 Realm 進行安全認證。從圖中可以看出,Realm 可進行自定義(Custom Realm)。

16.2.2 Shiro 許可權認證

許可權認證,也就是訪問控制,即在應用中控制誰能訪問哪些資源。在許可權認證中,最核心的三個要素是:許可權、角色和使用者。

許可權(Permission):即操作資源的權利,比如訪問某個頁面,以及對某個模組的資料進行新增、修改、刪除、檢視操作的權利。
角色(Role):指的是使用者擔任的角色,一個角色可以有多個許可權。
使用者(User):在 Shiro 中,代表訪問系統的使用者,即上面提到的 Subject 認證主體。

它們之間的的關係可以用下圖來表示:

一個使用者可以有多個角色,而不同的角色可以有不同的許可權,也可有相同的許可權。比如說現在有三個角色,1 是普通角色,2 也是普通角色,3 是管理員,角色 1 只能檢視資訊,角色 2 只能新增資訊,管理員對兩者皆有許可權,而且還可以刪除資訊。

16.3 Spring Boot 整合 Shiro

16.3.1 依賴匯入

Spring Boot 2.0.3 整合 Shiro 需要匯入如下 starter 依賴:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
16.3.2 資料庫表的建立及初始化

這裡主要涉及到三張表:使用者表、角色表和許可權表。其實在 Demo 中,我們完全可以自己來模擬資料庫操作,不用建表,但為了更加接近實際情況,我們還是引入了 MyBatis 來操作資料庫。下面是資料庫各表的建立指令碼。

CREATE TABLE `t_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `rolename` varchar(20) DEFAULT NULL COMMENT '角色名稱',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8

CREATE TABLE `t_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '使用者主鍵',
  `username` varchar(20) NOT NULL COMMENT '使用者名稱',
  `password` varchar(20) NOT NULL COMMENT '密碼',
  `role_id` int(11) DEFAULT NULL COMMENT '外來鍵關聯role表',
  PRIMARY KEY (`id`),
  KEY `role_id` (`role_id`),
  CONSTRAINT `t_user_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8

CREATE TABLE `t_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `permissionname` varchar(50) NOT NULL COMMENT '許可權名',
  `role_id` int(11) DEFAULT NULL COMMENT '外來鍵關聯role',
  PRIMARY KEY (`id`),
  KEY `role_id` (`role_id`),
  CONSTRAINT `t_permission_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

其中,t_user、t_role和t_permission分別儲存了使用者資訊、角色資訊和許可權資訊,表建立好後,我們往表裡插入一些測試資料,比如下面這些資料。

t_user 表:

id	username	password	role_id
1	csdn1	123456	1
2	csdn2	123456	2
3	csdn3	123456	3
t_role 表:

id	rolename
1	admin
2	teacher
3	student
t_permission 表:

id	permissionname	role_id
1	user:*	1

2	student:*	2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

解釋一下這裡的許可權:user:* 表示許可權可以是 user:create 或其他,* 表示一個佔位符,可以自己定義,下文介紹 Shiro 配置時會對其再做詳細說明。

16.3.3 自定義 Realm

有了資料庫表和資料,我們開始自定義 Realm。自定義 Realm 需要繼承 AuthorizingRealm 類,該類封裝了很多方法,且繼承自 Realm 類。

繼承 AuthorizingRealm 類後,我們需要重寫以下兩個方法。

doGetAuthenticationInfo() 方法:用來驗證當前登入的使用者,獲取認證資訊。
doGetAuthorizationInfo() 方法:為當前登入成功的使用者授予許可權和分配角色。

具體實現如下,相關注解請見程式碼註釋:

/**
 * 自定義realm
 * @author shengwu ni
 */
public class MyRealm extends AuthorizingRealm {

    @Resource
    private UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 獲取使用者名稱
        String username = (String) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 給該使用者設定角色,角色資訊存在 t_role 表中取
        authorizationInfo.setRoles(userService.getRoles(username));
        // 給該使用者設定許可權,許可權資訊存在 t_permission 表中取
        authorizationInfo.setStringPermissions(userService.getPermissions(username));
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 根據 Token 獲取使用者名稱,如果您不知道該 Token 怎麼來的,先可以不管,下文會解釋
        String username = (String) authenticationToken.getPrincipal();
        // 根據使用者名稱從資料庫中查詢該使用者
        User user = userService.getByUsername(username);
        if(user != null) {
            // 把當前使用者存到 Session 中
            SecurityUtils.getSubject().getSession().setAttribute("user", user);
            // 傳入使用者名稱和密碼進行身份認證,並返回認證資訊
            AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), "myRealm");
            return authcInfo;
        } else {
            return null;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

從上面兩個方法中可以看出,驗證身份時需先根據使用者輸入的使用者名稱從資料庫中查出對應的使用者,這時還未涉及到密碼,也就是說即使使用者輸入的密碼不正確,照樣可以查詢出該使用者。

然後,將該使用者的相關資訊封裝到 authcInfo 中並返回給 Shiro。接下來就該 Shiro 上場了,將封裝的使用者資訊與使用者的輸入資訊(使用者名稱、密碼)進行對比、校驗(注意,這裡對密碼也要進行校驗)。校驗通過則允許使用者登入,否則跳轉到指定頁面。

同理,許可權驗證時,也需先根據使用者名稱從資料庫中獲取其對應的角色和許可權,將其封裝到 authorizationInfo 並返回給 Shiro。

16.3.4 Shiro 配置

自定義 Realm 寫好了,接下來需要配置 Shiro。我們主要配置三個東西:自定義 Realm、安全管理器 SecurityManager 和 Shiro 過濾器。

首先,配置自定義的 Realm,程式碼如下:

@Configuration
public class ShiroConfig {

    private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);

    /**
     * 注入自定義的 Realm
     * @return MyRealm
     */
    @Bean
    public MyRealm myAuthRealm() {
        MyRealm myRealm = new MyRealm();
        logger.info("====myRealm註冊完成=====");
        return myRealm;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

接著,配置安全管理器 SecurityManager:

@Configuration
public class ShiroConfig {

    private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);

    /**
     * 注入安全管理器
     * @return SecurityManager
     */
    @Bean
    public SecurityManager securityManager() {
        // 將自定義 Realm 加進來
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(myAuthRealm());
        logger.info("====securityManager註冊完成====");
        return securityManager;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

配置 SecurityManager 時,需要將上面自定義 Realm 新增進來,這樣 Shiro 才可訪問該 Realm。

最後,配置 Shiro 過濾器:

@Configuration
public class ShiroConfig {

    private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);

    /**
     * 注入 Shiro 過濾器
     * @param securityManager 安全管理器
     * @return ShiroFilterFactoryBean
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        // 定義 shiroFactoryBean
        ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();

        // 設定自定義的 securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 設定預設登入的 URL,身份認證失敗會訪問該 URL
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 設定成功之後要跳轉的連結
        shiroFilterFactoryBean.setSuccessUrl("/success");
        // 設定未授權介面,許可權認證失敗會訪問該 URL
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

        // LinkedHashMap 是有序的,進行順序攔截器配置
        Map<String,String> filterChainMap = new LinkedHashMap<>();

        // 配置可以匿名訪問的地址,可以根據實際情況自己新增,放行一些靜態資源等,anon 表示放行
        filterChainMap.put("/css/**", "anon");
        filterChainMap.put("/imgs/**", "anon");
        filterChainMap.put("/js/**", "anon");
        filterChainMap.put("/swagger-*/**", "anon");
        filterChainMap.put("/swagger-ui.html/**", "anon");
        // 登入 URL 放行
        filterChainMap.put("/login", "anon");

        // 以“/user/admin” 開頭的使用者需要身份認證,authc 表示要進行身份認證
        filterChainMap.put("/user/admin*", "authc");
        // “/user/student” 開頭的使用者需要角色認證,是“admin”才允許
        filterChainMap.put("/user/student*/**", "roles[admin]");
        // “/user/teacher” 開頭的使用者需要許可權認證,是“user:create”才允許
        filterChainMap.put("/user/teacher*/**", "perms[\"user:create\"]");

        // 配置 logout 過濾器
        filterChainMap.put("/logout", "logout");

        // 設定 shiroFilterFactoryBean 的 FilterChainDefinitionMap
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
        logger.info("====shiroFilterFactoryBean註冊完成====");
        return shiroFilterFactoryBean;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

配置 Shiro 過濾器時,我們引入了安全管理器。

至此,我們可以看出,Shiro 配置一環套一環,遵循從 Reaml 到 SecurityManager 再到 Filter 的過程。在過濾器中,我們需要定義一個 shiroFactoryBean,然後將 SecurityManager 引入其中,需要配置的內容主要有以下幾項。

  • 預設登入的 URL:身份認證失敗會訪問該 URL。
  • 認證成功之後要跳轉的 URL。
  • 許可權認證失敗後要跳轉的 URL。
  • 需要攔截或者放行的 URL:這些都放在一個 Map 中。

通過上面的程式碼,我們也瞭解到, Map 中針對不同的 URL有不同的許可權要求,下表總結了幾個常用的許可權。

16.3.5 使用 Shiro 進行認證

至此,我們完成了 Shiro 的準備工作。接下來開始使用 Shiro 進行認證。

首先,設計如下幾個介面。

介面一:使用http://localhost:8080/user/admin進行身份認證。
介面二:使用http://localhost:8080/user/student進行角色認證。
介面三:使用http://localhost:8080/user/teacher進行許可權認證。
介面四:使用http://localhost:8080/user/login實現使用者登入。

開始編碼前,我們先了解下認證的流程。

流程一:直接訪問介面一(此時還未登入),認證失敗,跳轉到 login.html 頁面讓使用者登入。登入時請求介面四,實現使用者登入,此時 Shiro 已經儲存了使用者資訊。
流程二:再次訪問介面一(此時使用者已經登入),認證成功,跳轉到 success.html 頁面,展示使用者資訊。
流程三:訪問介面二,測試角色認證是否成功。
流程四:訪問介面三,測試許可權認證是否成功。
接下來,編寫身份、角色、許可權認證介面,程式碼如下所示:

@Controller
@RequestMapping("/user")
public class UserController {

    /**
     * 身份認證測試介面
     * @param request
     * @return
     */
    @RequestMapping("/admin")
    public String admin(HttpServletRequest request) {
        Object user = request.getSession().getAttribute("user");
        return "success";
    }

    /**
     * 角色認證測試介面
     * @param request
     * @return
     */
    @RequestMapping("/student")
    public String student(HttpServletRequest request) {
        return "success";
    }

    /**
     * 許可權認證測試介面
     * @param request
     * @return
     */
    @RequestMapping("/teacher")
    public String teacher(HttpServletRequest request) {
        return "success";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

這三個介面很簡單,直接返回到指定頁面即可。認證成功正常跳轉,如若認證失敗,就會跳轉到上文 ShrioConfig 中配置的頁面。

之後,我們開始編寫使用者登入介面,如下所示:

@Controller
@RequestMapping("/user")
public class UserController {

    /**
     * 使用者登入介面
     * @param user user
     * @param request request
     * @return string
     */
    @PostMapping("/login")
    public String login(User user, HttpServletRequest request) {

        // 根據使用者名稱和密碼建立 Token
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
        // 獲取 subject 認證主體
        Subject subject = SecurityUtils.getSubject();
        try{
            // 開始認證,這一步會跳到我們自定義的 Realm 中
            subject.login(token);
            request.getSession().setAttribute("user", user);
            return "success";
        }catch(Exception e){
            e.printStackTrace();
            request.getSession().setAttribute("user", user);
            request.setAttribute("error", "使用者名稱或密碼錯誤!");
            return "login";
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

我們重點分析下使用者登入介面。整個處理過程是這樣的。

首先,根據前端傳來的使用者名稱和密碼,建立一個 Token。

然後,使用 SecurityUtils 建立認證主體。

緊接著,呼叫 subject.login(token) 進行身份認證——注意,這裡傳入了剛剛建立的 Token,如註釋所述,這一步會跳轉入自定義的 Realm,訪問 doGetAuthenticationInfo 方法,開始身份認證。

最後,啟動專案,測試一下。在瀏覽器中請求:http://localhost:8080/user/admin,首先進行身份認證,此時未登入,會跳轉至 IndexController 中 /login 介面處,呈現出 login.html 頁面供我們登入。

接著,使用使用者名稱(csdn1)、密碼(123456)登入,在瀏覽器中請求:http://localhost:8080/user/student介面,開始角色認證,因為資料庫中 csdn1 的使用者角色是 admin,和配置中的吻合,認證通過。

我們再請求:http://localhost:8080/user/teacher介面,進行許可權認證,因為資料庫中 csdn1 的使用者許可權為 user:*,滿足配置中的 user:create,認證通過。

接下來,我們點選“退出”,系統會將該使用者登出,讓我們重新登入。我們嘗試使用 csdn2 使用者登入,重複上述操作,進行角色認證和許可權認證時,因為資料庫中 csdn2 使用者的角色和許可權與配置中的不同,所以認證失敗。