SpringBoot Shiro,解决Shiro中自定义Realm Autowired属性为空问题
SpringBoot作为主体框架,使用Shiro框架作为鉴权与授权模块。
之前弄SpringBoot+Shiro+密码加密还是踩了不少坑,于是把Shiro流程走了一遍,做个记录。
1.先介绍Shiro
用过Shiro的都知道,shiro内部使用装饰者模式,大头SecurityManager接口继承Authenticator认证、Authorizer授权、SessionManager会话管理 三个接口,
其实现类根据名字很好理解,需要注意的就是RealmSecurityManager、WebSecurityManager。其中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比较。
以上