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