Spring Boot 安全框架 Shiro 入門

一、前言

在所有的開發(fā)的系統(tǒng)中,都必須做認(rèn)證(authentication)和授權(quán)(authorization),以保證系統(tǒng)的安全性??紤]到很多讀者對認(rèn)證和授權(quán)有點(diǎn)分不清楚。

authentication [?,θ?nt?'ke??n] 認(rèn)證
authorization [,?θ?r?'ze??n] 授權(quán)

1.1 以坐飛機(jī)舉例子:

  • 【認(rèn)證】你要登機(jī),你需要出示你的 passport 和 ticket,passport 是為了證明你張三確實(shí)是你張三,這就是 authentication。
  • 【授權(quán)】而機(jī)票是為了證明你張三確實(shí)買了票可以上飛機(jī),這就是 authorization。

1.2 以論壇舉例子:

  • 【認(rèn)證】你要登錄論壇,輸入用戶名張三,密碼 1234,密碼正確,證明你張三確實(shí)是張三,這就是 authentication。
  • 【授權(quán)】再一 check 用戶張三是個版主,所以有權(quán)限加精刪別人帖,這就是 authorization 。
    所以簡單來說:認(rèn)證解決“你是誰”的問題,授權(quán)解決“你能做什么”的問題。另外,在推薦閱讀下《認(rèn)證、授權(quán)、鑒權(quán)和權(quán)限控制》 文章,更加詳細(xì)明確

在 Java 生態(tài)中,目前有 Spring SecurityApache Shiro 兩個安全框架,可以完成認(rèn)證和授權(quán)的功能。本文,我們再來學(xué)習(xí)下 Apache Shiro 。其官方對自己介紹如下:
Apache Shiro? 是一個功能強(qiáng)大且易于使用的 Java 安全框架,它可以提供身份驗(yàn)證、授權(quán)、加密和會話管理的功能。
通過 Shiro 易于理解的 API ,你可以快速、輕松地保護(hù)任何應(yīng)用程序 —— 從最小的移動端應(yīng)用程序到大型的的 Web 和企業(yè)級應(yīng)用程序。

二、 快速入門

2.1 引入依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>Apache-shiro</artifactId>

    <dependencies>
        <!-- 實(shí)現(xiàn)對 Spring MVC 的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 實(shí)現(xiàn)對 Shiro 的自動化配置 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.5.3</version>
        </dependency>
    </dependencies>

</project>

shiro-spring-boot-starter依賴對 Shiro 的自動化配置基本沒啥用,需要下面的這個類ShiroConfig自己來主動實(shí)現(xiàn)對 Shiro 的配置。

2.2 ShiroConfig

實(shí)現(xiàn) Shiro 的自定義配置。代碼如下:

package com.erbadagang.springboot.shiro.config;

import org.apache.shiro.realm.Realm;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    @Bean
    public Realm realm() {
        // 創(chuàng)建 SimpleAccountRealm 對象
        SimpleAccountRealm realm = new SimpleAccountRealm();
        // 添加兩個用戶。參數(shù)分別是 username、password、roles 。
        realm.addAccount("admin", "admin", "ADMIN");
        realm.addAccount("normal", "normal", "NORMAL");
        return realm;
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        // 創(chuàng)建 DefaultWebSecurityManager 對象
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 設(shè)置其使用的 Realm
        securityManager.setRealm(this.realm());
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        // 創(chuàng)建 ShiroFilterFactoryBean 對象,用于創(chuàng)建 ShiroFilter 過濾器
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();

        // 設(shè)置 SecurityManager
        filterFactoryBean.setSecurityManager(this.securityManager());

        // 設(shè)置 URL 們
        filterFactoryBean.setLoginUrl("/login"); // 登陸 URL
        filterFactoryBean.setSuccessUrl("/login_success"); // 登陸成功 URL
        filterFactoryBean.setUnauthorizedUrl("/unauthorized"); // 無權(quán)限 URL

        // 設(shè)置 URL 的權(quán)限配置
        filterFactoryBean.setFilterChainDefinitionMap(this.filterChainDefinitionMap());

        return filterFactoryBean;
    }

    private Map<String, String> filterChainDefinitionMap() {
        Map<String, String> filterMap = new LinkedHashMap<>(); // 注意要使用有序的 LinkedHashMap ,順序匹配
        filterMap.put("/test/echo", "anon"); // 允許匿名訪問
        filterMap.put("/test/admin", "roles[ADMIN]"); // 需要 ADMIN 角色
        filterMap.put("/test/normal", "roles[NORMAL]"); // 需要 NORMAL 角色
        filterMap.put("/logout", "logout"); // 退出
        filterMap.put("/**", "authc"); // 默認(rèn)剩余的 URL ,需要經(jīng)過認(rèn)證
        return filterMap;
    }

}

一共有三個 Bean 的配置,我們逐個來看看。

2.2.1 Realm

我們先來看看 Realm 的定義?!吧矸蒡?yàn)證”(認(rèn)證)和“授權(quán)”,這個就是 Realm 的職責(zé)。

Realm 整體的類圖如下:
Realm 類圖
  • Realm 接口,主要定義了“認(rèn)證”方法。代碼如下:
// Realm.java

    AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
  • AuthorizingRealm 抽象類,主要額外定義了授權(quán)方法。代碼如下:
// AuthorizingRealm.java
    protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
  • AuthorizingRealm 同時實(shí)現(xiàn)了 Authorizer 接口,提供判斷經(jīng)過認(rèn)證過的 Subject 是否具有指定的角色、權(quán)限的方法。
  • 從圖中我們可以看出,Shiro 提供了多種 AuthorizingRealm 的實(shí)現(xiàn)類,提供從不同的數(shù)據(jù)源獲取數(shù)據(jù)。不過一般在項目中,我們會自定義實(shí)現(xiàn) AuthorizingRealm ,從自己定義的表結(jié)構(gòu)中讀取用戶、角色、權(quán)限等數(shù)據(jù)。雖然說,Shiro 提供了 JdbcRealm 可以訪問數(shù)據(jù)庫,但是它的表結(jié)構(gòu)是固定的,所說我們才要自定義定義實(shí)現(xiàn) AuthorizingRealm

本示例中,在 #realm() 方法,我們創(chuàng)建了 SimpleAccountRealm Bean 對象。代碼如上所示:

  • SimpleAccountRealm 是使用內(nèi)存作為數(shù)據(jù)源,我們可以手動往里面添加用戶、角色、權(quán)限等數(shù)據(jù)。畢竟作為一個示例,不想引入數(shù)據(jù)庫,增加復(fù)雜性。不過我們在后續(xù)文章中,我們會看到我們使用自定義的 AuthorizingRealm 實(shí)現(xiàn)類。
  • 在該方法里,我們添加了「admin/admin」和「normal/normal」兩個用戶,分別對應(yīng) ADMIN 和 NORMAL 角色。

2.2.2 SecurityManager

我們再來看看 SecurityManager 的定義,SecurityManager 是 Shiro 架構(gòu)的核心,配合內(nèi)部安全組件共同組成安全傘。
本示例中,在 #securityManager() 方法,我們創(chuàng)建了 DefaultWebSecurityManager Bean 對象。代碼如下:

// ShiroConfig.java

@Bean
public DefaultWebSecurityManager securityManager() {
 // 創(chuàng)建 DefaultWebSecurityManager 對象
 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
 // 設(shè)置其使用的 Realm
 securityManager.setRealm(this.realm());
 return securityManager;
}
  • 不用特別去糾結(jié) SecurityManager ,創(chuàng)建好 DefaultWebSecurityManager Bean 就完事了~等后續(xù)我們?nèi)腴T完 Shiro 之后,胖友可以在慢慢細(xì)細(xì)去研究。

2.2.3 ShiroFilter

通過 AbstractShiroFilter 過濾器,實(shí)現(xiàn)對請求的攔截,從而實(shí)現(xiàn) Shiro 的功能。AbstractShiroFilter 整體的類圖如下:

AbstractShiroFilter 類圖

本示例中,在 #shiroFilterFactoryBean() 方法,我們創(chuàng)建了 ShiroFilterFactoryBean Bean 對象。代碼如下:

// ShiroConfig.java

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
 // <1> 創(chuàng)建 ShiroFilterFactoryBean 對象,用于創(chuàng)建 ShiroFilter 過濾器
 ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();

 // <2> 設(shè)置 SecurityManager
 filterFactoryBean.setSecurityManager(this.securityManager());

 // <3> 設(shè)置 URL 們
 filterFactoryBean.setLoginUrl("/login"); // 登錄 URL
 filterFactoryBean.setSuccessUrl("/login_success"); // 登錄成功 URL
 filterFactoryBean.setUnauthorizedUrl("/unauthorized"); // 無權(quán)限 URL

 // <4> 設(shè)置 URL 的權(quán)限配置
 filterFactoryBean.setFilterChainDefinitionMap(this.filterChainDefinitionMap());

 return filterFactoryBean;
}
  • <1> 處,創(chuàng)建 ShiroFilterFactoryBean 對象,用于創(chuàng)建 SpringShiroFilter 過濾器。

  • <2> 處,設(shè)置其 SecurityManager 屬性。

  • <3> 處,設(shè)置各種 URL 。

    • #setLoginUrl(String loginUrl) 方法,設(shè)置登錄 URL 。在 Shiro 中,約定 GET loginUrl 為登錄頁面,POST loginUrl 為登錄請求。
    • #setSuccessUrl(String successUrl) 方法,設(shè)置登錄成功 URL 。在登錄成功時,會重定向到該 URL 上。
    • #etUnauthorizedUrl(String unauthorizedUrl) 方法,設(shè)置無權(quán)限的 URL 。在請求校驗(yàn)權(quán)限不通過時,會重定向到該 URL 上。
    • 上述的 URL 對應(yīng)的接口,都需要我們自己來實(shí)現(xiàn)。具體可見「2.3 SecurityController」小節(jié)。
  • <4> 處,調(diào)用 #setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) 方法,設(shè)置 URL 的權(quán)限配置。

在看 #filterChainDefinitionMap() 方法的具體 URL 的權(quán)限配置之前,我們先來了解下 Shiro 內(nèi)置的過濾器們。在 Shiro DefaultFilter 枚舉類中,枚舉了這些過濾器,以及其配置名。整理表格如下:

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
logout org.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
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 :AnonymousFilter :允許匿名訪問,即無需登錄。
  • authc :FormAuthenticationFilter :需要經(jīng)過認(rèn)證的用戶,才可以訪問。如果是匿名用戶,則根據(jù) URL 不同,會有不同的處理:
    • 如果攔截的 URL 是 GET loginUrl 登錄頁面,則進(jìn)行該請求,跳轉(zhuǎn)到登錄頁面。
    • 如果攔截的 URL 是 POST loginUrl 登錄請求,則基于請求表單的 username、password 進(jìn)行認(rèn)證。認(rèn)證通過后,默認(rèn)重定向到 GET loginSuccessUrl 地址。
    • 如果攔截的 URL 是其它 URL 時,則記錄該 URL 到 Session 中。在用戶登錄成功后,重定向到該 URL 上。
  • logout :LogoutFilter :攔截的 URL ,執(zhí)行退出操作。退出完成后,重定向GET loginUrl 登錄頁面。
  • roles :RolesAuthorizationFilter :擁有指定角色的用戶可訪問。
  • perms :PermissionsAuthorizationFilter :擁有指定權(quán)限的用戶可以訪問。

下面,讓我們回過頭來看看 #filterChainDefinitionMap() 方法的具體 URL 的權(quán)限配置。代碼如下:

    private Map<String, String> filterChainDefinitionMap() {
        Map<String, String> filterMap = new LinkedHashMap<>(); // 注意要使用有序的 LinkedHashMap ,順序匹配
        filterMap.put("/test/echo", "anon"); // 允許匿名訪問
        filterMap.put("/test/admin", "roles[ADMIN]"); // 需要 ADMIN 角色
        filterMap.put("/test/normal", "roles[NORMAL]"); // 需要 NORMAL 角色
        filterMap.put("/logout", "logout"); // 退出
        filterMap.put("/**", "authc"); // 默認(rèn)剩余的 URL ,需要經(jīng)過認(rèn)證
        return filterMap;
    }
  • /test/echo :我們設(shè)置為 anon ,允許匿名訪問。
  • /test/admin/test/normal :我們設(shè)置為 roles[...] ,需要指定角色的用戶可以訪問。其中 ... 處為需要添加的角色名。
  • /logout :我們設(shè)置為 logout ,實(shí)現(xiàn)退出操作。
  • /** :剩余的 URL ,我們設(shè)置為 authc ,需要登錄的用戶才可以訪問。同時,對于 loginUrl 需要執(zhí)行登錄相關(guān)的攔截。

另外,這里在補(bǔ)充一點(diǎn),請求在 ShiroFilter 攔截之后,會根據(jù)該請求的情況,匹配到配置的內(nèi)置的 Shiro Filter 們,逐個進(jìn)行處理。也就是說,ShiroFilter 實(shí)際內(nèi)部有一個由 內(nèi)置的 Shiro Filter 組成的過濾器。

至此,我們已經(jīng)完成了 Shiro 的自定義配置。雖然篇幅有點(diǎn)長,但是可以等我們跑完整個示例之后,再自己回過頭來看看,會發(fā)現(xiàn)還是比較清晰明了的。

2.3 SecurityController

提供登錄、登錄成功等接口。代碼如下:

package com.erbadagang.springboot.shiro.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequestMapping("/")
public class SecurityController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @GetMapping("/login")
    public String loginPage() {
        return "login.html";
    }

    @ResponseBody
    @PostMapping("/login")
    public String login(HttpServletRequest request) {
        // 判斷是否已經(jīng)登陸
        Subject subject = SecurityUtils.getSubject();
        if (subject.getPrincipal() != null) {
            return "你已經(jīng)登陸賬號:" + subject.getPrincipal();
        }

        // 獲得登陸失敗的原因
        String shiroLoginFailure = (String) request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
        // 翻譯成人類看的懂的提示
        String msg = "";
        if (UnknownAccountException.class.getName().equals(shiroLoginFailure)) {
            msg = "賬號不存在";
        } else if (IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)) {
            msg = "密碼不正確";
        } else if (LockedAccountException.class.getName().equals(shiroLoginFailure)) {
            msg = "賬號被鎖定";
        } else if (ExpiredCredentialsException.class.getName().equals(shiroLoginFailure)) {
            msg = "賬號已過期";
        } else {
            msg = "未知";
            logger.error("[login][未知登陸錯誤:{}]", shiroLoginFailure);
        }
        return "登陸失敗,原因:" + msg;
    }

    @ResponseBody
    @GetMapping("/login_success")
    public String loginSuccess() {
        return "登陸成功";
    }

    @ResponseBody
    @GetMapping("/unauthorized")
    public String unauthorized() {
        return "你沒有權(quán)限";
    }

}

2.3.1 登錄頁面

GET /login 地址,跳轉(zhuǎn)登錄頁面。代碼如下:

// SecurityController.java

@GetMapping("/login")
public String loginPage() {
 return "login.html";
}
<!DOCTYPE html>
  <html lang="en">
  <head>
   <meta charset="UTF-8">
   <title>登錄頁面</title>
  </head>
  <body>
   <form action="/login" method="post">
   用戶名:<input type="text" name="username"/> <br />
   密碼:<input type="password" name="password"/> <br />
   <input type="submit" value="登錄"/>
   </form>
  </body>
  </html>
  • 一個簡單的登錄的表單,POST 提交登錄請求到 /login 地址上。

2.3.2 登錄請求

對于登錄請求,會被我們配置的 Shiro FormAuthenticationFilter 過濾器進(jìn)行攔截,進(jìn)行用戶的身份認(rèn)證。整個過程如下:

  • FormAuthenticationFilter 解析請求的 username、password 參數(shù),創(chuàng)建 UsernamePasswordToken 對象。
  • 然后,調(diào)用 SecurityManager 的 #login(Subject subject, AuthenticationToken authenticationToken) 方法,執(zhí)行登錄操作,進(jìn)行“身份驗(yàn)證”(認(rèn)證)。
  • 在這內(nèi)部中,調(diào)用 Realm 的 #getAuthenticationInfo(AuthenticationToken token) 方法,進(jìn)行認(rèn)證。此時,根據(jù)認(rèn)證的是否成功,會有不同的處理:
    • 如果認(rèn)證通過,則 FormAuthenticationFilter 會將請求重定向GET loginSuccess 地址上。
    • 【重要】如果認(rèn)證失敗,則會將認(rèn)證失敗的原因設(shè)置到請求的 attributes 中,后續(xù)該請求會繼續(xù)請求到 POST login 地址上。這樣,在 POST loginUrl 地址上,我們可以從 attributes 中獲取到失敗的原因,提示給用戶。

所以,POST loginUrl 的目的,實(shí)際是為了處理認(rèn)真失敗的情況。也因此,POST login 地址,實(shí)現(xiàn)代碼如下:

// SecurityController.java

@ResponseBody
@PostMapping("/login")
public String login(HttpServletRequest request) {
 // <1> 判斷是否已經(jīng)登錄
 Subject subject = SecurityUtils.getSubject();
 if (subject.getPrincipal() != null) {
 return "你已經(jīng)登錄賬號:" + subject.getPrincipal();
 }

 // <2> 獲得登錄失敗的原因
 String shiroLoginFailure = (String) request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
 // 翻譯成人類看的懂的提示
 String msg = "";
 if (UnknownAccountException.class.getName().equals(shiroLoginFailure)) {
 msg = "賬號不存在";
 } else if (IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)) {
 msg = "密碼不正確";
 } else if (LockedAccountException.class.getName().equals(shiroLoginFailure)) {
 msg = "賬號被鎖定";
 } else if (ExpiredCredentialsException.class.getName().equals(shiroLoginFailure)) {
 msg = "賬號已過期";
 } else {
 msg = "未知";
 logger.error("[login][未知登錄錯誤:{}]", shiroLoginFailure);
 }
 return "登錄失敗,原因:" + msg;
}
  • <1> 處,對于已經(jīng)登錄成功的用戶,如果我們再次請求 POST loginUrl 地址,依然會直接跳轉(zhuǎn)到該地址上。此處,我們是提供用戶已經(jīng)的登錄。可能會希望重新進(jìn)行一次登錄的邏輯,那么就需要重寫 FormAuthenticationFilter 過濾器。
  • <2> 處,從請求的 attributes 中,獲取 FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME 對應(yīng)的值,即登錄失敗的原因。從代碼中,我們可以看出,失敗原因?yàn)楫惓5娜惷?,我們需要進(jìn)行翻譯成人類可讀的提示。

2.3.3 登錄成功

GET login_success 地址,登錄成功響應(yīng)。代碼如下:

// SecurityController.java

@ResponseBody
@GetMapping("/login_success")
public String loginSuccess() {
 return "登錄成功";
}
  • 如果是 AJAX 請求的情況下,我們可以返回 JSON 字符串。例如說,用戶、角色、權(quán)限等等信息。
  • 如果非 AJAX 請求的情況下,重定向到登錄成功的頁面。例如說,管理后臺的 HOME 頁面。

2.3.4 未授權(quán)

GET unauthorized 地址,未授權(quán)響應(yīng)。代碼如下:

// SecurityController.java

@ResponseBody
@GetMapping("/unauthorized")
public String unauthorized() {
 return "你沒有權(quán)限";
}
  • 如果是 AJAX 請求的情況下,我們可以返回 JSON 字符串。例如說,你沒有權(quán)限。
  • 如果非 AJAX 請求的情況下,重定向到登錄成功的頁面。例如說,未授權(quán)的頁面。

2.4 TestController

在 [controller]包路徑下,創(chuàng)建 TestController 類,提供測試 API 接口。代碼如下:

// TestController.java

@RestController
@RequestMapping("/test")
public class TestController {

 @GetMapping("/demo")
 public String demo() {
 return "示例返回";
 }

 @GetMapping("/home")
 public String home() {
 return "我是首頁";
 }

 @GetMapping("/admin")
 public String admin() {
 return "我是管理員";
 }

 @GetMapping("/normal")
 public String normal() {
 return "我是普通用戶";
 }

}
  • 對于 /test/demo 接口,直接訪問,無需登錄。
  • 對于 /test/home 接口,無法直接訪問,需要進(jìn)行登錄。
  • 對于 /test/admin 接口,需要登錄「admin/admin」用戶,因?yàn)樾枰?ADMIN 角色。
  • 對于 /test/normal 接口,需要登錄「user/user」用戶,因?yàn)樾枰?USER 角色。

胖友可以按照如上的說明,進(jìn)行各種測試。例如說,登錄「user/user」用戶后,去訪問 /test/admin 接口,會返回?zé)o權(quán)限的提示~

2.5 Application

創(chuàng)建 Application.java 類,配置 @SpringBootApplication 注解即可。代碼如下:

// Application.java

@SpringBootApplication
public class Application {

 public static void main(String[] args) {
 SpringApplication.run(Application.class, args);
 }

}

至此,我們已經(jīng)完成了 Shiro 的入門。可以自己多多測試一下。

三、Shiro注解

在 Shiro 中,提供了如下五個注解,可以直接添加在 SpringMVC 的 URL 對應(yīng)的方法上,實(shí)現(xiàn)權(quán)限配置。下面,我們來分別看看。

3.1 @RequiresGuest

@RequiresGuest 注解,和 anon 等價。

3.2 @RequiresAuthentication

@RequiresAuthentication 注解,和 authc 等價。

3.3 @RequiresUser

@RequiresUser 注解,和 user 等價,要求必須登錄。

3.4 @RequiresRoles

@RequiresRoles 注解,和 roles 等價。代碼如下:

// RequiresRoles.java

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRoles {

 /**
 * A single String role name or multiple comma-delimited role names required in order for the method
 * invocation to be allowed.
 */
 String[] value();

 /**
 * The logical operation for the permission check in case multiple roles are specified. AND is the default
 * @since 1.1.0
 * 當(dāng)有多個角色時,AND 表示要擁有全部角色,OR 表示擁有任一角色即可
 */
 Logical logical() default Logical.AND; 
}

使用示例如下:

// 屬于 NORMAL 角色
@RequiresRoles("NORMAL")

// 要同時擁有 ADMIN 和 NORMAL 角色
@RequiresRoles({"ADMIN", "NORMAL"})

// 擁有 ADMIN 或 NORMAL 任一角色即可
@RequiresRoles(value = {"ADMIN", "NORMAL"}, logical = Logical.OR)

如果驗(yàn)證權(quán)限不通過,則會拋出 AuthorizationException 異常。此時,我們可以基于 Spring MVC 提供的 @RestControllerAdvice + @ExceptionHandler 注解,實(shí)現(xiàn)全局異常的處理。不了解的胖友,可以看看《芋道 Spring Boot SpringMVC 入門》「5. 全局異常處理」小節(jié)。

3.5 @RequiresPermissions

@RequiresPermissions 注解,和 perms 等價。代碼如下:

// RequiresPermissions.java

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermissions {

 /**
 * The permission string which will be passed to {@link org.apache.shiro.subject.Subject#isPermitted(String)}
 * to determine if the user is allowed to invoke the code protected by this annotation.
 */
 String[] value();

 /**
 * The logical operation for the permission checks in case multiple roles are specified. AND is the default
 * @since 1.1.0
 * 當(dāng)有多個權(quán)限時,AND 表示要擁有全部權(quán)限,OR 表示擁有任一權(quán)限即可
 */
 Logical logical() default Logical.AND; 

}

使用示例如下:

// 擁有 user:add 權(quán)限
@RequiresPermissions("user:add")

// 要同時擁有 user:add 和 user:update 權(quán)限
@RequiresPermissions({"user:add", "user:update"})

// 擁有 user:add 和 user:update 任一權(quán)限即可
@RequiresPermissions(value = {"user:add", "user:update"}, logical = Logical.OR)

如果驗(yàn)證權(quán)限不通過,則會拋出 AuthorizationException 異常。此時,我們可以基于 Spring MVC 提供的 @RestControllerAdvice + @ExceptionHandler 注解,實(shí)現(xiàn)全局異常的處理。不了解的胖友,可以看看另外一篇文章的全局異常處理小節(jié)。

底線


本文源代碼使用 Apache License 2.0開源許可協(xié)議,這里是本文源碼Gitee地址,可通過命令git clone+地址下載代碼到本地,也可直接點(diǎn)擊鏈接通過瀏覽器方式查看源代碼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容