最簡明的Shiro教程

東瓜東瓜發表於2020-03-22

後端管理系統登入一般都涉及到許可權控制,許可權管理元件用的最多的就是Apache的Shiro了,任何系統的登入模組,基本都可以使用shiro來實現我們的功能。

什麼是Shiro

相信看到這篇文章的人都知道Shiro是什麼吧,Apache Shiro是Java的一個安全(許可權)框架,Shiro可以非常容易的開發出足夠好的應用,JavaSE和Java EE環境都可以使用。Shiro可以完成:認證、授權、加密、會話管理、與web整合、快取等。官方圖如下:

8RIiRK.png

這裡說下shiro安全的四大基石(上圖中上層4個黃色部分)

  • Authentication:認證,身份驗證,有時稱為“登入”,這是證明使用者“我是誰”的行為。
  • Authorization:授權,訪問控制的過程,即確定“誰”有權訪問“什麼”。
  • Session Management:會話管理,管理特定於使用者的會話,即使在非web或EJB應用程式中也是如此。
  • Cryptography:加密,使用密碼演算法保持資料安全,同時仍然易於使用。

使用shiro

shiro的使用也很簡單,如下圖,主要有三個類

  • Subject:主體,代表當前使用者,當前登入使用者的所有屬性都可以通過該類獲取
  • SecurityManager:安全管理器,判斷Subject是否能登入、是否具有某些許可權等等操作
  • Realm:資料訪問器,shiro通過Realm訪問資料庫獲取許可權資訊

8RTp36.png

三者在程式碼中的體現是: 簡單的說,當你對當前登入使用者Subject進行操作時,Subject會與SecurityManager互動,SecurityManager會使用Realm查詢許可權。

當然,還有另外一個類ShiroFilterFactoryBean也比較重要,下面講。

Spring Boot整合Shiro

話不多說,上程式碼。首先來思考一下正常的網頁登入流程是什麼樣的?我整理的一下,大概流程如下。

  • 頁面攔截的設定,系統中有些頁面可以直接訪問,有些頁面需要登入才能訪問,有些頁面不僅需要登入還需要登入的使用者有相關許可權才能訪問
  • 如果使用者沒登入直接訪問需要登入或需要許可權的頁面,則調轉到登入頁面讓使用者登入
  • 使用者登入認證後,訪問需要許可權的頁面時,系統可以識別該使用者是誰並鑑權。

如果解決以上三個問題,上程式碼。

匯入shiro-spring-boot-web-starter

maven中匯入如下Shiro的座標。

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.5.1</version>
</dependency>
複製程式碼

攔截所有頁面

步驟都在下面程式碼的註釋了,總結一句話就是:通過ShiroFilterFactoryBean配置攔截的頁面,攔截後調轉到登入頁,登入過程中,SecurityManager呼叫Realm來獲取認證、授權的資訊。

package io.github.zebinh.zmall.mall.admin.config;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * Shiro配置類
 */
@Configuration
public class ShiroConfig {


    /**
     *  Shiro自帶的過濾器,可以在這裡配置攔截頁面
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Autowired DefaultWebSecurityManager securityManager){

        // 1. 初始化一個ShiroFilter工程類
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        // 2. 我們知道Shiro是通過SecurityManager來管理整個認證和授權流程的,這個SecurityManager可以在下面初始化
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 3. 上面我們講過,有的頁面不需登入任何人可以直接訪問,有的需要登入才能訪問,有的不僅要登入還需要相關許可權才能訪問
        Map<String, String> filterMap = new LinkedHashMap<>();

        // 4. Shiro過濾器常用的有如下幾種
        // 4.1. anon 任何人都能訪問,如登入頁面
        filterMap.put("/api/user/login", "anon");
        // 4.2. authc 需要登入才能訪問,如系統內的全體通知頁面
        filterMap.put("/api/user/notics", "authc");
        // 4.3. roles 需要相應的角色才能訪問
        filterMap.put("/api/user/getUser", "roles[admin]");
        // 5. 讓ShiroFilter按這個規則攔截
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);

        // 6. 使用者沒登入被攔截後,當然需要調轉到登入頁了,這裡配置登入頁
        shiroFilterFactoryBean.setLoginUrl("/api/user/login");
        return shiroFilterFactoryBean;
    }

    /**
     * SecurityManager管理認證、授權整個流程
     */
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(@Autowired UserRealm userRealm){

        // 7. 新建一個SecurityManager供ShiroFilterFactoryBean使用
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 8. SecurityManager既然管理認證等資訊,那他就需要一個類來幫他查資料庫吧。這就是Realm類
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    /**
     * 自定義Realm,當SecurityBean需要來查詢相關許可權資訊時,需要有Realm代勞
     */
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }


    /**
     * 為了方便觀看,我將UserRealm類的實現寫在這裡了
     */
    class UserRealm extends AuthorizingRealm {

        // 9. 前面被authc攔截後,需要認證,SecurityBean會呼叫這裡進行認證
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            System.out.println("執行認證邏輯");
            UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;

            // 9.1. 為了方便演示,我這裡寫死了使用者admin-name密碼admin-pwd才能登入
            if (token.getUsername() == null || !token.getUsername().equals("admin-name")){
                return null;
            }

            return new SimpleAuthenticationInfo("", "admin-pwd", "");
        }

        // 10. 前面被roles攔截後,需要授權才能登入,SecurityManager需要呼叫這裡進行許可權查詢
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            System.out.println("執行授權邏輯");

            // 10.1. 為了方便演示,這裡直接寫死返回了admin角色,所有能登入的角色都是admin了
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            info.addRole("admin");
            return info;
        }
    }

}

複製程式碼

調轉到登入頁後,使用者登入,直接呼叫Subject類的login方法即可,Subject類會呼叫SecurityManager進行認證。看到這裡,應該就能理解本文開頭的兩幅圖了吧。

package io.github.zebinh.zmall.mall.admin.user.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("api/user")
public class UserController {

    @PostMapping("login")
    public String login(@RequestBody Map<String, String> user){

        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(user.get("username"), user.get("password"));

        try{
            subject.login(token);
        }catch (UnknownAccountException e){
            System.out.println("使用者名稱不存在");
            return "使用者名稱不存在";
        }catch (AuthenticationException e) {
            System.out.println("認證失敗");
            return "認證失敗";
        }
        return "登入成功";
    }
    
    @GetMapping("getUser")
    public Object getUser() {
        return SecurityUtils.getSubject();
    }

    @GetMapping("notics")
    public String notics(){
        return "通知";
    }
}

複製程式碼

Shiro如何識別使用者

在登入完成後,shiro會發一個cookie給客戶端,包含了sessionId,因此能識別當前使用者,當訪問需要登入的頁面時,如發現cookie中有sessionId,判斷該使用者已登入,則不需要呼叫Realm再進行認證了。cookie如下所示。

8hXc5T.png

那麼問題來了,cookie只有pc端才有,移動端並沒有cookie,該怎麼解決移動端的shiro認證問題呢,同時這種方式無法解決單點登入的問題,應為cookie只能在同源的網站被髮送到伺服器。

解決方案就是,這時候就要定義自己的Session管理器了。使用者登入後,自定義Session管理器生成session並生成一個token給到前端,前端每次訪問都傳遞token,shiro會呼叫自定義的Session管理器做校驗。Session中可以放置當前登入使用者的資訊並放置在redis中。自定義的Session管理器每次從redis中讀取資料即可。

JWT(JSON Web Token)

上面我們說到了使用token + redis來儲存使用者資訊。此次使用者資訊是儲存在伺服器的redis中的,還有另外一種方式JWT,它是將客戶資訊加密後返回給客戶端,客戶端每次使用該加密字串訪問伺服器,伺服器加密即可獲得使用者資訊。這種方式則是將使用者資訊存放在客戶端了。可以瞭解一下這種方式。

本篇文章討論的只是Shiro的使用,對於設計一個安全的登入(單點)系統還需要很多討論。

以上。

相關文章