Shiro學習筆記(一) 基本概念與使用

北冥有隻魚發表於2022-06-11

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 官網》

Apache Shiro 是Java領域內的一款簡單易用而又強大的一款安全框架,主要用於登入驗證、授權、加密、會話管理。Shiro擁有簡單應用的API,您可以快送的使用它來保護您的應用,不管是小到手機應用還是到大的Web企業級應用。

從上面的一段話我們可以提取到以下資訊:

  • Shiro簡單易用而又強大
  • 主要應用於登入驗證、授權、加密、會話管理。

第一點需要在中使用中慢慢體會,第二點是我們主要關注的,這裡我們一點一點的講。

登入驗證 authentication 與 會話管理

這裡我們回憶一下HTTP協議和Servlet, 早期的HTTP協議是無狀態的, 這個無狀態我們可以這麼理解,你一分鐘前訪問和現在訪問一個網站,服務端並不認識你是誰,但是對於Web服務端開發者來說, 這便無法實現訪問控制,某些資訊只能登入使用者才能看,各自看各自的。這著實的有點限制住了Web的發展,為了讓HTTP協議有狀態,RFC-6235提案被獲得批准,這個提案引入了Cookie, 是伺服器傳送到使用者瀏覽器並儲存在本地的一小塊資料,它會再瀏覽器下次向同一伺服器再發起請求時被攜帶併傳送到伺服器上,服務端就可以實現“識別”使用者了。

在原生的Servlet場景中如下:

登入流程圖

程式碼示例:

/**
 * 攔截所有的請求
 */
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

    /**
     * 不重寫init方法,過濾器無法起作用
     * @param filterConfig
     * @throws ServletException
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("----login check filter init-----");
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest servletRequest = (HttpServletRequest) request;
        HttpSession session = servletRequest.getSession();
        // 這裡採取了暫時寫死,目前只放行登入請求的URL
        String requestUri = servletRequest.getRequestURI();
        if ("/login".equals(requestUri)){
            // 放行,此請求進入下一個過濾器
            chain.doFilter(request,response);
        }else {
            Object attribute = session.getAttribute("currentUser");
            if (Objects.nonNull(attribute)){
                chain.doFilter(request,response);
            }else {
                request.getRequestDispatcher("/login.jsp").forward(request,response);
            }
        }
    }
}
@WebFilter(urlPatterns = "/login")
public class LoginFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("------login filter init-----");
    }

    /**
     * 由登入過濾器來執行登入驗證操作
     * 要求使用者名稱和密碼不為空的情況下才進行下一步操縱
     * 此處省略判空操作
     * 只做模擬登入
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest servletRequest = (HttpServletRequest) request;
        String userName = (String) servletRequest.getAttribute("userName");
        String password = (String)servletRequest.getAttribute("password");
        // 這裡假裝去資料庫去查賬號和密碼
        HttpSession session = servletRequest.getSession();
        // 生成session,
        session.setAttribute("currentUser",userName + password);
        // 放到下一個過濾器,如果這是最後一個過濾器,那麼這個請求會被放行到對應的Servlet中
        chain.doFilter(request,response);
    }
}
@WebServlet(value = "/hello")
public class HttpServletDemo extends HttpServlet {
   
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("hello world");
        super.doGet(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
}

前面提到, 為了實現讓HTTP協議有“狀態”, 瀏覽器引入了Cookie, 存放服務端傳送給客戶端的資料, 為了使服務端區分不同的使用者, 服務端程式引入了Session這個概念,瀏覽器首次請求服務端,服務端會在HTTP協議中告訴客戶端,需要再Cooke裡面記錄一個SessionID,以後每次請求把這個SessionId傳送到伺服器,這樣伺服器就能區分不同的客戶端了。

JSessionId

有了這樣的對應關係,我們就可以在Session中儲存當前使用者的資訊。其實我們這裡只做了登入,還沒有做登出,登出應該將對應的Session中清除掉。這套邏輯Shiro幫我們做好了,那自然是極好的,但僅僅是如此的話,還不足以讓我們使用Shiro, 我們接著翻官方文件。

一個小插曲

因為很久沒寫Servlet了,這次去下載Tomcat,下了一個10.0的, 但是發現10.0版本跟JDK 8不太相容,折騰了許久, 請求都沒辦法到達Servlet,要不就是報Servlet初始化失敗,最高只好退回8.5版本,但是還是發現請求到達不了我寫的Servlet中, 我是基於註解的形式配置的對映,但是web.xml的有個屬性我忘記了,那就是metadata-complete, 此屬性為true, 不會掃描基於註解的Servlet,此註解為false, 才會啟用此Servlet。

meta-complete = false

Shiro的特性

ShiroFeatures

Authentication和Session Management: Shiro幫我們做好了, 那自然是極好的。

Cryptography: 加密, 其實Java標準庫也提供了實現,那Shiro能提供一套更簡單易用的API更好。

Authorization:授權, 這個值得我們注意一下,寫到這裡我想起我第一個構建的Web系統,當時只考慮了普通的使用者,沒有考慮資源控制的問題,當時已經快到預定的時間了,說來慚愧, 只好做了一個非常粗糙的許可權控制,為使用者新增了一個許可權標識欄位,1代表什麼角色,2代表什麼角色能看哪些頁面,也就是說資源是固定的,相當的僵硬。後面才瞭解到在這方面有一套比較成熟而自然的RBAC(Role-Based Access Controle 基於角色的訪問控制)模型,即角色-使用者-許可權(資源),也就是說一個使用者擁有若干角色,每一個角色擁有若干許可權和資源,這樣我們就實現了許可權和使用者的解耦合。

基本概念我們大致論述完之後,我們來進一步深入的看這些特性:

  • Authentication 登入

    • Subject Based – Almost everything you do in Shiro is based on the currently executing user, called a Subject.
      And you can easily retrieve the Subject anywhere in your code. This makes it easier for you to understand and work with Shiro in your applications.

      基於主體,在Shiro中做的任何事情都基於當前正在活動的使用者,在Shiro中稱之為主體,你可以在程式碼的任何地方取到當前的主體。

      這將讓你在使用和理解shiro變的輕鬆起來。

    • Single Method call – The authentication process is a single method call.
      Needing only one method call keeps the API simple and your application code clean, saving you time and effort.

      簡單的方法呼叫,非常簡單,節省時間和精力。

    • Rich Exception Hierarchy – Shiro offers a rich exception hierarchy to offered detailed explanations for why a login failed.
      The hierarchy can help you more easily diagnose code bugs or customer services issues related to authentication. In addition, the richness can help you create more complex authentication functionality if needed.

      豐富的異常體系,Shiro提供了完備的異常體系來解釋登入為什麼失敗。這個異常體系可以幫助診斷定製服務的相關bug和issue。除此之外,還可以幫助建立功能更加豐富的應用。

    • ‘Remember Me’ built in – Standard in the Shiro API is the ability to remember your users if they return to your application.
      You can offer a better user experience to them with minimal development effort.

      記住我,標準的Shiro API提供了記錄密碼的功能,只需少量的配置,就能給使用者提供更佳的體驗。

    • Pluggable data sources – Shiro uses pluggable data access objects (DAOs), called Realms, to connect to security data sources like LDAP and Active Directory.
      To help you avoid building and maintaining integrations yourself, Shiro provides out-of-the-box realms for popular data sources like LDAP, Active Directory, and JDBC. If needed, you can also create your own realms to support specific functionality not included in the basic realms.

      可插拔的資料來源,Shiro提供了一個可插拔的資料許可權物件,在shiro中我們稱之為Realms,我們用這個去安全的連線像LDAP、Active Directory的資料來源。

    為了避免開發者做重複的工作,Shiro 提供了開箱即用的連線指定資料來源的Realm是,像LDAP、Active Directory 、JDBC。 如果你需要你也可以建立自定義的Realms。

    • Login with one or more realms – Using Shiro, you can easily authenticate a user against one or more realms and return one unified view of their identity.
      In addition, you can customize the authentication process with Shiro’s notion of an authentication strategy. The strategies can be setup in configuration files so changes don’t require source code modifications – reducing complexity and maintenance effort.

      支援一個和多個Realm登入,使用Shiro 你可以輕鬆的完成使用者使用多個Realm登入,並且方式統一。除此之外,你也可以可以使用Shiro的身份驗證策略自定義登入過程,驗證策略支援寫在配置檔案中,所以當驗證策略改變的時候,不需要更改程式碼。

看來上面的特性概述,你會發現原來登入需要考慮這麼多,原先我們的視角可能只在資料庫的資料來源,事實上對於WEB系統來說還可以引入其他資料來源,但是你不用擔心,這麼多需要考慮的東西,原先你自己來寫登入,可能還會考慮遺漏的地方,但是Shiro都幫你寫好。我們還有什麼理由不學它呢。這裡還是需要重點講一下Shiro的Realms概念,我們回憶一下JDBC,可以讓我們Java程式設計師寫一套API就能做到跨資料庫,那多個資料來源呢,我們能否也抽象出一個介面,做到登入的時候跨資料來源呢? 其實Realms就是這個思路,也抽象出了一個介面,對接不同的資料來源來實現登入認證:

Realm繼承圖

本質上是一個特定的安全DAO(Data Access Object), 封裝與資料來源連線的細節,得到Shiro所需要的相關資料。在配置Shiro的時候,我們必須指定至少Realm來實現認證(authentication) 和授權(authorization).

我們只重點介紹一個特性來體會Shiro的強大,其他特性我們只簡單介紹轉眼的點:

  • Cryptography:Shiro指出Java的密碼體系比較難用(The Java Cryptography Extension (JCE) can be complicated and difficult to use unless you’re a cryptography expert, Java的擴充套件非常難用,當然你要是個密碼專家就當我沒說),Shiro設計的密碼學的API更簡單易用。

    PS: 其實還指出了Java密碼學擴充套件的一些問題,算是對Java密碼學相關庫的吐槽了。有興致可以去看看,我們這裡不做過多介紹。

  • SessionManagement

    可以被用於SSO,獲取使用者登入和退出都相當方便。SSO是單點登入,方便的和各種應用系統做整合。
  • Authorization

    下面是授權的幾個經典問題:

    這個使用者是否可以編輯這個賬號

    這個使用者是否有許可權看這頁面

    這個使用者是否有許可權使用這個按鈕

    Shiro回答了以上問題,並且非常靈活、簡單、容易使用。

    Shiro幫我們做了這麼多,而且簡單,我們可以省了很多工作,這就是學習Shiro的理由。

Shiro核心概念概述

在Shiro中的架構中有三個主要概念:

  • subject(當前主體, 可以理解為當前登入使用者,)

    在Shiro中可以使用下面程式碼來獲取當前登入使用者:

  Subject currentUser = SecurityUtils.getSubject();
  • SecurityManager
為所有使用者提供安全保護,內嵌了很多安全元件,那麼如何設定它呢? 也取決於不同的環境, Web 程式中通常是在Web.xml中指定Shiro的Servlet過濾器,這就完成了一個SecurityManager 例項,其他型別的應用程式我們也有其他選項。
  • Realms
Realm 事實上是Shiro和你應用安全資料的橋樑或聯結器。

Realms

三者之間的關係:

三個驗證狀態

用起來

首先引入maven依賴:

  <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>1.9.0</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>1.7.21</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
      <version>1.7.21</version>
      <scope>test</scope>
    </dependency>

當然是從Hello World開始

    public static void testAuthentication(){
        // 設定SecurityManager 
        // SecurityManager 負責將使用者提交過來的username、password和realm中的進行對比
        // 判斷是否可以登入
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
    
        // 這是一個簡單的Realm,直接在程式碼裡面儲存賬號和密碼
        SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
        simpleAccountRealm.addAccount("hello world","hello world");
        // 將realm 納入到 DefaultManager的管轄之下
        defaultSecurityManager.setRealm(simpleAccountRealm);
        // 通過此方法設定SecurityManager
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        
        // 使用者登入和密碼憑證
        UsernamePasswordToken token = new UsernamePasswordToken("hello world", "hello world");
        
        // 獲取subject
        Subject subject = SecurityUtils.getSubject();
        
        // 由subject將token提交給SecurityManager
        subject.login(token);
        // 登入成功會返回true
        System.out.println("login status:"+subject.isAuthenticated());
        // 退出
        subject.logout();
        // 退出之後是false
        System.out.println("login status:"+subject.isAuthenticated());
    }

加密示例

Cryptography is the process of hiding or obfuscating data so prying eyes can’t understand it. Shiro’s goal in cryptography is to simplify and make usable the JDK’s cryptography support.

密碼學是隱藏或混淆資料的過程,防止資料被竊取。Shiro在密碼學方面的目標是簡化JDK標準密碼學庫的使用

接下來讓我們下用Shiro的密碼學相關的API有多簡單易用.

  • MD5 JDK標準庫的實現
 private static void testMD5JDK() {
        try {
            String code = "hello world";
            // MD5 是 MessageDigest的第五個版本
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] targetBytes = md.digest(code.getBytes());
            //輸出的是MD5的十六進位制形式
            System.out.println(targetBytes);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }
  • Shiro的實現:
private static void testMD5Shiro() {
    String hex = new Md5Hash("hello world").toHex();
    System.out.println(hex.getBytes());
}

這麼一看Shiro的實現確實簡單一些,更直觀。

授權示例

在Shiro中將授權分成以下兩種:

  • Permission Defined

Permissions are the most atomic level of a security policy and they are statements of functionality. Permissions represent what can be done in your application. A well formed permission describes a resource types and what actions are possible when you interact with those resources. Can you open a door? Can you read a file? Can you delete a customer record? Can you push a button?

Common actions for data-related resources are create, read, update, and delete, commonly referred to as CRUD.

It is important to understand that permissions do not have knowledge of who can perform the actions– they are just statements of what actions can be performed.

准許是安全策略的原子級別,表現為功能的宣告。准許的意思是你可以在這個系統中做什麼。形式良好的准許描述了資源型別以及可以對這些資源進行的操作。比如: 你是否可以開啟這扇門? 你是否可以讀一個檔案? 你是否可以刪除一個客戶記錄? 你是否能點選按鈕?

一般來說對資源的操作有新增、讀取、更新、刪除,這些操作通常被稱作為CRUD。

一定要理解,准許是不知道是誰可以操作這些資源的,它們只是說明這些資源可以執行哪些操作。

​ 許可權的粒度:

  1. Resource Level - This is the broadest and easiest to build. A user can edit customer records or open doors. The resource is specified but not a specific instance of that resource.
資源級別: 這是最廣泛的而且是最容易構建的。使用者可以編輯客戶記錄或者開啟一扇門。資源指定了,但是沒有指定到具體的人禍角色上。
  1. Instance Level - The permission specifies the instance of a resource. A user can edit the customer record for IBM or open the kitchen door.
例項級別: 某個准許指定了具體的人或者角色, 某個使用者可以編輯IBM的使用者記錄或開啟廚房的門。
  1. Attribute Level - The permission now specifies an attribute of an instance or resource. A user can edit the address on the IBM customer record.
屬性級別: 某人被允許編輯資源的某個屬性,某個使用者可以編輯IVM使用者記錄的地址。
  • Role Defined

In the context of Authorization, Roles are effectively a collection of permissions used to simplify the management of permissions and users. So users can be assigned roles instead of being assigned permissions directly, which can get complicated with larger user bases and more complex applications. So, for example, a bank application might have an administrator role or a bank teller role.

在授權的上下門,角色實際上是許可權的集合,簡化許可權和使用者的管理,因此使用者可以被分配角色,而不是直接被分配許可權,因為直接分配許可權這對於更大的使用者群和更復雜的應用程式來說會比較複雜。

Shiro支援如下兩種角色:

  • Implicit Roles 隱式角色
大多數人的角色在我們眼中屬於是隱式角色,隱含了一組許可權,通常的說,如果你具有管理員的角色,你可以檢視患者資料。如你具有“銀行出納員”的角色,那麼你可以建立賬號。
  • Explicit Roles 顯式角色
顯式角色就是系統明顯分配的許可權,如果你可以檢視患者資料,那是因為你被分配到了“管理員角色”的“檢視患者資料”許可權。
小小總結一下

上面是我翻譯的Shiro對角色和許可權的論述,許可權的話可以理解為對某個資源的CRUD, 粒度級別有整個資源,比如一行記錄的操縱許可權,這行記錄只有某個人才能操縱,某個人只能操縱這行記錄的一部分。而角色則是許可權的集合, 在我看來是實現許可權和使用者的解耦,比如我想對一批使用者授權,我可以選一個角色,進行批量授權。那麼問題又來了,我該怎麼做許可權控制呢,或者在Shiro中判斷某個是否有這個角色呢?

  • 我們可以從Subject這個類的方法中判斷當前使用者是否具備某個角色或者具備某個許可權

    subject.hasRole("admin") // 當前使用者是否有admin這個角色
    subject.isPermitted("user:create") // 判斷當前使用者是否允許新增使用者
  • JDK 的註解, 在方法中加註解
// 判斷當前角色是否有admin 這個角色
@RequiresRoles("admin")
private static void requireSpecialRole() {

}
 // 判斷當前使用者是否允許新增使用者
@RequiresPermissions("user:create")
private static void requireSpecialRole() {
 }
  • JSP 標籤(前後端分離時代了, 這個不做介紹)

Shiro 定義了一組許可權描述語法來描述許可權,格式為: 資源:(create || update || delete ||query)。

示例:

user:create,update // 表示具備建立和更新使用者的許可權
user:create,update:test110 // ID為test110有 建立和更新使用者的許可權   

自定義Realm

我們前面一直再說Realm是Shiro和使用者資料之間的橋樑, 我們大致看下這個登入過程,重點來體會一下橋樑:

  1. subject.login(token). 目前Subject只有一個實現類,那就是DelegatingSubject。

DelegatingSubject

我們上面用的是DefaultSecurityManager, 所以我們需要進入該類的方法來看是怎麼執行登入的, DefaultSecurityManager.login呼叫authenticate(token)來獲取使用者資訊,authenticate()方法來自DefaultSecurityManager的父類AuthenticatingSecurityManager, 該類擁有一個Authenticator成員變數,由該成員變數呼叫authenticate,獲取使用者資訊

AuthenticatingSecurityManager

這在某種成都上很像是模板方法模式, 父類定義骨架或通用方法,子類做呼叫。Authenticator是一個介面有許多實現類,但是呼叫的authenticate就只有兩個實現:

AbstractAuthenticator

doAuthenticate

獲取初始化進來Realms

最終調到了對應的Realms

Realm這個介面太頂層了,我們要做自己的Realm的話還是找一個抽象類,我們上面的SimpleAccountRealm就是繼承自AuthorizingRealm,重寫了doGetAuthenticationInfo用於登入驗證,doGetAuthorizationInfo用於做許可權驗證。我們自己寫的如下:

public class MyRealm extends AuthorizingRealm {

    /**
     * 設定Realm的名稱
     */
    public MyRealm() {
        super.setName("myRealm");
    }

    /**
     * 我們這個自定義reamls就能實現許可權控制了
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 獲取登入憑證
        String userName = (String) principals.getPrimaryPrincipal();
        // 假裝這roles 是從資料庫中查的
        Set<String> roles  = new HashSet<>();
        // 假裝是從資料庫查的
        Set<String> permissions  = new HashSet<>();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }

    /**
     * 隨便寫個空實現
     * @param userName
     * @return
     */
    private String getPassWordByUserName(String userName) {
        return "";
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 獲取賬號
        String userName = (String) token.getPrincipal();
        String password = null;
        if (userName != null && userName != ""){
            password = getPassWordByUserName(userName);
        }else {
            return null;
        }
        if (Objects.isNull(password)){
            return null;
        }
        // 假裝去查了資料庫
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userName,password,"myRealm");
        return simpleAuthenticationInfo;
    }
}

寫在最後

我記得剛開始學Shiro的時候是去B站搜對應的視訊, 但是視訊大多都是從Shiro的基本元件開始講,我其實只是想知道Shiro能幫我幹啥,去看教程也是先從Shiro是一個安全框架講起,然後講架構,看了半天視訊我只收穫了一堆名詞,我想看的東西都沒看到,實在消耗我的耐心。所以本篇文章是以問題為導向,即Shiro最終幫我們做了什麼,同時也調整了一些文章介紹風格,省略掉一些比較大的詞,這篇文章獻給當初在網上找Shiro,找了半天也沒找到合心意的教程的自己。

參考資料

相關文章