SpringBoot Shiro,解决Shiro中自定义Realm Autowired属性为空问题

SpringBoot作为主体框架,使用Shiro框架作为鉴权与授权模块。

 

之前弄SpringBoot+Shiro+密码加密还是踩了不少坑,于是把Shiro流程走了一遍,做个记录。

 

1.先介绍Shiro

 

用过Shiro的都知道,shiro内部使用装饰者模式,大头SecurityManager接口继承Authenticator认证、Authorizer授权、SessionManager会话管理 三个接口,

SpringBoot Shiro,解决Shiro中自定义Realm Autowired属性为空问题

 

 其实现类根据名字很好理解,需要注意的就是RealmSecurityManager、WebSecurityManager。其中WebSecurityManager是一个接口,其实现类Shiro只提供了一个:DefaultWebSecurityManager,通常这一个也足够用了,打开这个类查看,可以发现一个很熟悉的Realm

SpringBoot Shiro,解决Shiro中自定义Realm Autowired属性为空问题

 

构造函数中,该类要了一个Realm,再查看setRealm方法,发现走到了RealmSecurityManager里了,大致可以联想到,DefaultWebSecurityManager继承自RealmSecurityManager。

SpringBoot Shiro,解决Shiro中自定义Realm Autowired属性为空问题

 

 

实际上也的确如此,RealmSecurityManager是一个抽象类且RealmSecurityManager的父类CachingSecurityManager同样也是抽象类。我们都知道抽象类定义了一类事物或行为流程的规范,再来看RealmSecurityManager的子类实现:

SpringBoot Shiro,解决Shiro中自定义Realm Autowired属性为空问题

 那心里就有数了,授权管理、认证管理、会话管理、Shiro提供的DefaultWebSecurityManager都依赖于Realm。

那继续来看Realm:

SpringBoot Shiro,解决Shiro中自定义Realm Autowired属性为空问题

 

 Realm作为一个接口,其麾下皆是实现类,再结合之前看到的Shiro有关SecurityManager的设计,容易想到这些类中必定有抽象类,默认实现类。又看到CachingRealm,在SecurityManager的设计中Cache便作为RealmManager的抽象父类,想必这里也是

SpringBoot Shiro,解决Shiro中自定义Realm Autowired属性为空问题

 再看其子类,因为Shiro是认证鉴权的安全框架,又因为鉴权应当在认证的后一步,所以先点开AuthenticatingRealm:

SpringBoot Shiro,解决Shiro中自定义Realm Autowired属性为空问题

 是个抽象类很好理解,该抽象类肯定是规范了Shiro的认证步骤或者行为,再看鉴权AuthorizingRealm:

SpringBoot Shiro,解决Shiro中自定义Realm Autowired属性为空问题

依然是个抽象类,且继承自认证Realm:

SpringBoot Shiro,解决Shiro中自定义Realm Autowired属性为空问题

 可以看到Realm继承了授权Realm----AuthorizingRealm

实际开发中也的确是如此,我们增加自定义Realm编写认证、授权逻辑,登陆模块通过org.apache.shiro.subject.Subject#login 作为入口,由大头SecurityManager来负责调用Realm,最终认证、鉴权模块便会走到我们自定义的Realm中。

SpringBoot Shiro,解决Shiro中自定义Realm Autowired属性为空问题

 

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

 

以上

 

上一篇:shiro SimpleSession的序列化


下一篇:Shiro流程简析 过滤器