1. 程式人生 > 其它 >SpringBoot Shiro,解決Shiro中自定義Realm Autowired屬性為空問題

SpringBoot Shiro,解決Shiro中自定義Realm Autowired屬性為空問題

SpringBoot Shiro,解決Shiro中自定義Realm Autowired屬性為空問題

SpringBoot作為主體框架,使用Shiro框架作為鑑權與授權模組。

之前弄SpringBoot+Shiro+密碼加密還是踩了不少坑,於是把Shiro流程走了一遍,做個記錄。

1.先介紹Shiro

用過Shiro的都知道,shiro內部使用裝飾者模式,大頭SecurityManager介面繼承Authenticator認證、Authorizer授權、SessionManager會話管理 三個介面,

其實現類根據名字很好理解,需要注意的就是RealmSecurityManagerWebSecurityManager。其中WebSecurityManager是一個介面,其實現類Shiro只提供了一個:DefaultWebSecurityManager

,通常這一個也足夠用了,開啟這個類檢視,可以發現一個很熟悉的Realm

建構函式中,該類要了一個Realm,再檢視setRealm方法,發現走到了RealmSecurityManager裡了,大致可以聯想到,DefaultWebSecurityManager繼承自RealmSecurityManager

實際上也的確如此,RealmSecurityManager是一個抽象類且RealmSecurityManager的父類CachingSecurityManager同樣也是抽象類。我們都知道抽象類定義了一類事物或行為流程的規範,再來看RealmSecurityManager的子類實現:

那心裡就有數了,授權管理、認證管理、會話管理、Shiro提供的DefaultWebSecurityManager都依賴於Realm。

那繼續來看Realm:

Realm作為一個介面,其麾下皆是實現類,再結合之前看到的Shiro有關SecurityManager的設計,容易想到這些類中必定有抽象類,預設實現類。又看到CachingRealm,在SecurityManager的設計中Cache便作為RealmManager的抽象父類,想必這裡也是

再看其子類,因為Shiro是認證鑑權的安全框架,又因為鑑權應當在認證的後一步,所以先點開AuthenticatingRealm

是個抽象類很好理解,該抽象類肯定是規範了Shiro的認證步驟或者行為,再看鑑權AuthorizingRealm

依然是個抽象類,且繼承自認證Realm:

可以看到Realm繼承了授權Realm----AuthorizingRealm

實際開發中也的確是如此,我們增加自定義Realm編寫認證、授權邏輯,登陸模組通過org.apache.shiro.subject.Subject#login 作為入口,由大頭SecurityManager來負責呼叫Realm,最終認證、鑑權模組便會走到我們自定義的Realm中。

Shiro介紹五五渣渣暫時到這裡。

2. 那開始弄整合的內容:

新增maven依賴:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>${shiro-spring}</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>${spring-boot.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

<shiro-spring>1.8.0</shiro-spring>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
                

新增ShiroConfig配置類:

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * User: Pfatman
 * Date: 2021/11/9
 * Time: 16:31
 * Description: ShiroConfig
 */
@Slf4j
@Configuration
public class ShiroConfig {

    @Value("shiro_loginPage:login")
    private String loginPage;



    /**
     * 許可權管理 主要是配置realm的管理認證
     * @return
     */
    @Bean
    public SecurityManager securityManager(){
        return new DefaultWebSecurityManager();

    }

    /**
     * 處理攔截資源問題
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
        ShiroFilterFactoryBean factoryBean=new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        factoryBean.setLoginUrl(loginPage);
        Map<String,String> map=new LinkedHashMap<>();
        map.put("/static/**","anon");
        map.put("/logout","logout");
        factoryBean.setFilterChainDefinitionMap(map);
        return factoryBean;
    }

    /**
     * Shiro Bean生命週期
     * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    /**
     * Shiro 提供的代理增強
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 授權屬性增強
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
        AuthorizationAttributeSourceAdvisor attributeSourceAdvisor=new AuthorizationAttributeSourceAdvisor();
        attributeSourceAdvisor.setSecurityManager(securityManager());
        return attributeSourceAdvisor;
    }

}

2.1 丟擲問題:

上面有關Shiro的Config嚴格意義上其實少了一點,那就是自定義的Realm,之前介紹Shiro的時候,我們便看到SecurityManager中建構函式有Realm,但上述配置中配置SecurityManager這裡是直接return new

DefaultWebSecurityManager();

    @Bean
    public Realm realm(){
       Realm realm = new MyRealm();
        return realm;
    }
@Bean public SecurityManager securityManager(Realm realm){ return new DefaultWebSecurityManager(realm); }

但是上述方式為SecurityManager設定Realm可能會產生一個問題,就是如果自定義Realm中有依賴其它注入Bean的物件或者引數,可能導致Realm中通過@Autowired注入的屬性為null,這是因為Shiro的bean在初始化完成之後才開始初始化其它Bean,即SecurityManager、Realm在初始化Bean的時候其它Bean並未初始化,為null。如果通過上述方式在構造SecurityManager這個Bean的時候我們直接塞一個new Realm的話,那其實MyRealm中通過如@Autowired注入的屬性便為null了。

2.2 如何解決:

出現這種Realm中注入屬性為空的問題通常是Shiro的Bean在其它Bean載入完成之前就已完全完成初始化了,那從這點考慮,將我們自定義的Realm作為一個Bean,由Spring容器來初始化,但這樣會導致我們在ShiroConfig中配置的SecurityManager這個Bean中沒有Realm屬性。那問題就變成解決SecurityManager中注入我們Realm的問題了:

1. 在自定義Realm中注入SecurityManager,對SecurityManager設定屬性Realm為this:

@Slf4j
@Service("wencharRealm")
public class WencharRealm extends AuthorizingRealm {


    @Autowired
    ILoginUserInfoService loginUserInfoService;
    
    @Autowired
    public WencharRealm(WencharCredentialsMatcher matcher){
        super.setCredentialsMatcher(matcher);
    }

    
    
    @Autowired
    private void webSecurityManager(SecurityManager securityManager) {
        if (securityManager instanceof DefaultWebSecurityManager) {
            log.info("==為DefaultWebSecurityManager 設定Realm==");
            DefaultWebSecurityManager webSecurityManager = (DefaultWebSecurityManager) securityManager;
            webSecurityManager.setRealm(this);
        }
    }
    
    
    /**
     * 授權
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    }
    
    
    /**
     * 認證
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
    }

    
}

2.個人不推薦,Realm作為Bean,在Spring容器完全初始化完成後對SecurityManager設定Realm,或者使用@PostConstruct註解。

ShiroConfig 實現implements ApplicationListener<ContextRefreshedEvent> 介面,重新整理時為SecurityManager賦值,但這樣不如第一種來的直接。

個人感覺雖然能實現功能,但也的確破壞了Bean流程。

以上。

3. 密碼比對器:CredentialsMatcher

補充介紹另外一個內容,Shiro提供的密碼驗證器,包括加密演算法、加密次數

自定義一個密碼驗證器:

@Component
public class WencharCredentialsMatcher extends HashedCredentialsMatcher {


    @Value("${REAL_SALTCOUNT:1024}")
    private int saltCount;

    @Override
    public int getHashIterations() {
        return saltCount;
    }

    @Override
    public void setHashAlgorithmName(String hashAlgorithmName) {
        super.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
    }
}

說明:上述自定義密碼比對器繼承自HashedCredentialsMatcher,設定加密次數預設為1024次,加密演算法為Md5

這樣需要Realm與登陸入口subject.login() 相對應,如密碼、鹽 等。

登陸入口校驗:

        Subject subject = SecurityUtils.getSubject();
        try {
            UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
                    loginUser.getLoginName(),
                    loginUser.getLoginPwd());
            subject.login(usernamePasswordToken);
        } catch (AuthenticationException e) {
            log.debug("===loginUser failed login==【{}】",loginUser);
            return ResponseVo.failResponse("使用者名稱或密碼不正確");
        }

Realm中認證校驗:

    /**
     * 認證
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String userName = authenticationToken.getPrincipal().toString();
        LoginUserVo loginUserVo = userInfoService.queryUserLoginInfo(userName);
        return new SimpleAuthenticationInfo(loginUserVo.getAccountId(),
                loginUserVo.getPassword(),
                ByteSource.Util.bytes(loginUserVo.getSalt()),
                getWencharRealmName());
    }

Realm中認證和Subject.login(token); 可以這樣區分,token中傳使用者名稱、加密前的密碼、鹽, 這些資料會根據SecurityManager中密碼比較器中的引數,以及Realm中傳遞的AuthenticationInfo中鹽值,過一遍加鹽加密演算法然後與 Realm中userName、password比較。

以上