SpringBoot Shiro,解決Shiro中自定義Realm Autowired屬性為空問題
SpringBoot作為主體框架,使用Shiro框架作為鑑權與授權模組。
之前弄SpringBoot+Shiro+密碼加密還是踩了不少坑,於是把Shiro流程走了一遍,做個記錄。
1.先介紹Shiro
用過Shiro的都知道,shiro內部使用裝飾者模式,大頭SecurityManager介面繼承Authenticator認證、Authorizer授權、SessionManager會話管理 三個介面,
其實現類根據名字很好理解,需要注意的就是RealmSecurityManager、WebSecurityManager。其中WebSecurityManager是一個介面,其實現類Shiro只提供了一個:DefaultWebSecurityManager
建構函式中,該類要了一個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比較。
以上