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比較。

 

以上

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM