Shiro實現Basic認證

王子發表於2020-12-17

 

前言

今天跟小夥伴們分享一個實戰內容,使用Spring Boot+Shiro實現一個簡單的Http認證。

場景是這樣的,我們平時的工作中可能會對外提供一些介面,如果這些介面不做一些安全認證,什麼人都可以訪問,安全性就太低了,所以我們的目的就是增加一個介面的認證機制,防止別人通過介面攻擊伺服器。

至於Shiro是什麼,Http的Basic認證是什麼,王子就簡單介紹一下,詳細內容請自行了解。

Shiro是一個Java的安全框架,可以簡單實現登入、鑑權等等的功能。

Basic認證是一種較為簡單的HTTP認證方式,客戶端通過明文(Base64編碼格式)傳輸使用者名稱和密碼到服務端進行認證,通常需要配合HTTPS來保證資訊傳輸的安全。

 

實踐部分

首先說明一下測試環境。

王子已經有了一套整合好Shiro的Spring Boot框架,這套框架詳細程式碼就不做展示了,我們只來看一下測試用例。

要測試的介面程式碼如下:

/**
 * @author liumeng
 */
@RestController
@RequestMapping("/test")
@CrossOrigin
public class TestAppController extends BaseController {
    /**
     * 資料彙總
     */
    @GetMapping("/list")
    public AjaxResult test()
    {
        return success("測試介面!");
    }
}

使用Shiro,一定會有Shiro的攔截器配置,這部分程式碼如下:

  /**
     * Shiro過濾器配置
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager)
    {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // Shiro的核心安全介面,這個屬性是必須的
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 身份認證失敗,則跳轉到登入頁面的配置
        shiroFilterFactoryBean.setLoginUrl(loginUrl);
        // 許可權認證失敗,則跳轉到指定頁面
        shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
        // Shiro連線約束配置,即過濾鏈的定義
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 對靜態資源設定匿名訪問
        filterChainDefinitionMap.put("/favicon.ico**", "anon");
        filterChainDefinitionMap.put("/lr.png**", "anon");
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/docs/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/ajax/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/lr/**", "anon");
        filterChainDefinitionMap.put("/captcha/captchaImage**", "anon");
        // 退出 logout地址,shiro去清除session
        filterChainDefinitionMap.put("/logout", "logout");
        // 不需要攔截的訪問
        filterChainDefinitionMap.put("/login", "anon,captchaValidate");
        filterChainDefinitionMap.put("/ssoLogin", "anon");
     // 開啟Http的Basic認證 filterChainDefinitionMap.put(
"/test/**", "authcBasic"); // 註冊相關 filterChainDefinitionMap.put("/register", "anon,captchaValidate"); Map<String, Filter> filters = new LinkedHashMap<String, Filter>(); filters.put("onlineSession", onlineSessionFilter()); filters.put("syncOnlineSession", syncOnlineSessionFilter()); filters.put("captchaValidate", captchaValidateFilter()); filters.put("kickout", kickoutSessionFilter()); // 登出成功,則跳轉到指定頁面 filters.put("logout", logoutFilter()); shiroFilterFactoryBean.setFilters(filters); // 所有請求需要認證authcBasic filterChainDefinitionMap.put("/**", "user,kickout,onlineSession,syncOnlineSession"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; }

這裡我們要關注的是程式碼中的

filterChainDefinitionMap.put("/test/**", "authcBasic"); 

這部分程式碼,它指定了我們的測試介面啟動了Http的Basic認證,這就是我們的第一步。

做到這裡我們可以嘗試的去用瀏覽器訪問一下介面,會發現如下情況:

這就代表Basic認證已經成功開啟了,這個時候我們輸入系統的使用者名稱和密碼,你以為它就能成功訪問了嗎?

答案是否定的,我們只是開啟了認證,但並沒有實現認證的邏輯。

王子通過閱讀部分Shiro原始碼,發現每次傳送請求後,都會呼叫ModularRealmAuthenticator這個類的doAuthenticate方法,原始碼如下:

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        Collection<Realm> realms = getRealms();
        if (realms.size() == 1) {
            return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
        } else {
            return doMultiRealmAuthentication(realms, authenticationToken);
        }
    }

可以看出,這個方法主要就是對Realm進行了管理,因為我們的系統本身已經有兩個Ream了,針對的是不同情況的許可權驗證,所以為了使用起來不衝突,我們可以繼承這個類來實現我們自己的邏輯,在配置類中增加如下內容即可:

    @Bean
    public ModularRealmAuthenticator modularRealmAuthenticator(){
        //用自己重新的覆蓋
        UserModularRealmAuthericator modularRealmAuthericator = new UserModularRealmAuthericator();
        modularRealmAuthericator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return modularRealmAuthericator;
    }

然後在我們自己的UserModularRealmAuthericator類中重寫doAuthenticate方法就可以了,這裡面的具體實現邏輯就要看你們自己的使用場景了。

我們可以自己新建立一個Realm來單獨校驗Basic認證的情況,或者共用之前的Realm,這部分就自由發揮了。

大概內容如下:

public class UserModularRealmAuthericator extends ModularRealmAuthenticator {
    private static final Logger logger = LoggerFactory.getLogger(UserModularRealmAuthericator.class);

    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        //強制轉換返回的token
        UsernamePasswordToken  usernamePasswordToken = (UsernamePasswordToken) authenticationToken;//所有Realm
        Collection<Realm> realms = getRealms();
        //最終選擇的Realm
        Collection<Realm> typeRealms = new ArrayList<>();
        for(Realm realm:realms){
            if(...){ //這部分是自己的邏輯判斷,過濾出想要使用的Realm
                typeRealms.add(realm);
            }
        }
        //判斷是單Realm 還是多Realm
        if(typeRealms.size()==1){
            return doSingleRealmAuthentication(typeRealms.iterator().next(),usernamePasswordToken);
        }else{
            return doMultiRealmAuthentication(typeRealms,usernamePasswordToken);
        }
    }
}

Realm的具體實現程式碼這裡就不做演示了,無非就是判斷使用者名稱密碼是否能通過校驗的邏輯。如果不清楚,可以自行了解Realm的實現方式。

Realm校驗實現後,Basic認證就已經實現了。

 

測試部分

接下來我們再次使用瀏覽器對介面進行測試,輸入使用者名稱和密碼,就會發現介面成功響應了。

我們來抓取一下請求情況

 

 

可以發現,Request Header中有了Basic認證的資訊Authorization: Basic dGVzdDoxMjM0NTY=

這部分內容是這樣的,Basic為一個固定的寫法,dGVzdDoxMjM0NTY=這部分內容是userName:Password組合後的Base64編碼,所以我們只要給第三方提供這個編碼,他們就可以通過編碼訪問我們的介面了。

使用PostMan測試一下

 

 可以發現介面是可以成功訪問的。

 

總結

到這裡本篇文章就結束了,王子向大家仔細的介紹瞭如何使用Shiro實現一個Http請求的Basic認證,是不是很簡單呢。

給大家留下一個思考題,你認為這種Basic認證能夠保證介面的安全嗎?

歡迎大家留言討論。

 

往期文章推薦:

JVM專欄

訊息中介軟體專欄

併發程式設計專欄

 

相關文章