zoukankan      html  css  js  c++  java
  • 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比较。

    以上

  • 相关阅读:
    Ceph 之RGW Cache
    Ceph 之RGW Pub-Sub Module
    Ceph 之RGW Data Layout
    RocksDB 之Write Ahead Log(WAL)
    Ceph 之 Background on http frontends
    Ceph 之Multisite 下的bucket reshard
    Ceph之PG数调整
    Ceph之对象存储网关RADOS Gateway(RGW)
    window mysql重启、忘记密码等操作
    selenium处理HTML5视频播放未能自动播放解决办法
  • 原文地址:https://www.cnblogs.com/notably/p/15592752.html
Copyright © 2011-2022 走看看