shiro认证授权
核心组件介绍
- Authentication
身份认证,验证用户是否拥有相应的身份。 - Authorization
授权/权限校验,验证用户是否拥有某个权限。 - Session Mannagement
会话管理,shiro拥有自己的session(不需要web环境下就能使用) - Cryptography
加密 - Web Support
web支持,支队web应用提供一些功能。 - Caching
缓存 - Concurrentcy
shiro支持多线程应用的并发验证。即在一个线程开启另一个线程,能把授权自动传播过去。 - Testing
测试支持 - Run As
允许一个用户假装成为另外一个用户的省份进行访问。 - Rememeber Me
记住用户身份
主要使用类
- Subject
与应用交互的用户。Subject在shiro中是一个接口,定义了很多认证授权的方法,外部程序通过Subject进行认证授权,而Subject通过SecurityManager进行认证授权。Subject相当于一个交互的门面,底层逻辑有SecurityManager执行。 - SecurityManager
安全管理器,所有与安全有关的操作都会经过SecurityManager。是shiro的核心,负责与shiro其他组件进行交互。比如:通过Authenticator进行认证,通过Authorizer进行授权,通过sessionManager进行会话管理。 - Realm
Shiro从Realm获取安全数据。SecurityManager要验证身份或操作权限,需要从Realm获取响应数据来判断。
认证流程
- 创建token令牌,token中有用户提交的认证信息即账号和密码。
- Subject.login(token)。Subject实例通常是DelegatingSubject类(或子类)的实例对象。在认证的时候,通过SecurityManager实例来调用securityManager.login(token)方法。
- SecurityManager接收到token后,委托
Authenticator
实例进行认证。Authenticator通过实现类ModularRealmAuthenticator.authenticate(token)方法。ModularRealmAuthenticator在认证过程中会对一个或多个Realm实例进行适配。(可插拔) - 如果配置了多个Realm,ModularRealmAuthenticator会根据配置的AuthenticationStrategy(认证策略)来进行多Realm的认证过程。在Realm被调用后,AuthenticationStrategy将对每一个Realm的结果做出响应。
如果只有一个Realm,Realm将直接调用而无需再配置认证策略。
- 判断每一个Realm是否都支持提交的token,如果支持,Realm调用getAuthenticationInfo(token),改方法就是对认证的处理。我们通过覆盖Realm的doGetAuthenticationInfo方法来编写我们自定义的认证处理。
- shiro中有三种认证策略的具体实现。
①AtleastOneSuccessfulStrategy:只要有一个Realm验证成功,则成功。
②FirstSuccessfulStrategy:第一个realm验证成功,则成功,后续realm将被忽略。
③AllSuccessfulStrategy:所有realm成功,验证才成功。 - 补充: 认证失败后抛出的一些异常:
UnknownAccountException 帐号不存在
IncorrectCredentialsException 密码错误
DisabledAccountException 帐号被禁用
LockedAccountException 帐号被锁定
ExcessiveAttemptsException 登录失败次数过多
ExpiredCredentialsException 凭证过期
Realm
shiro自带的realm接口,CachingRealm负责缓存处理。AuthenticationRealm负责认证,AuthorizingRealm负责授权等。通常用户信息都需要通过数据库里面去取,所有需要自定义Realm,通常自定义的realm继承AuthorizingRealm,认证是重写doGetAuthenticationInfo(AuthenticationToken token)方法,授权是重写doGetAuthorizationInfo(PrincipalCollectionprincipals)方法
。
自定义Realm中注入的实例为什么要加@Lazy注解
通过将注入的实体类,增加无参构造函数,打dug观察。发现这个这个bean实例化是在registerBeanPostProcessors中。在 refresh 方法的后置处理器注册步骤就已经创建好了。按照IOC容器的初始化流程,一定是先把后置处理器都注册好了,再创建单实例Bean。在这里很明显是后置处理器还没完全处理完,就引发单实例Bean的创建了。
解决方案:如何让Realm创建时不立即创建依赖的对象。
shiro为什么要提早创建Realm?
Shiro的后置处理器。 shiroEventBusAwareBeanPostProcessor。它依赖了一个EventBus。后面又依赖SecurityManager,SecurityManager依赖Realm。Realm的创建又依赖需要的实例。而实例的创建时由于事务控制增强器话没有创建好,所以无法代理这个Realm依赖的实例,最终注入到Realm的实例就是不带事务的。总结如下:
①ApplicaitonContext的refresh方法要创建BeanPostProcessor。
②ShiroEventBusBeanPostProcessor的创建需要依赖EventBus。
③EventBus创建时需要被AOP增强,触发AOP增强器的创建逻辑。此时AOP增强器有2个,分别是事务控制增强器和shiro的增强器。
④首先创建事务控制的AOP增强器TransactionAttributeSourceAdvisor,由于它定义在配置类中,又触发配置类的创建。
⑤配置类创建时也要被AOP增强,再一次触发AOP增强器的创建逻辑。此时事务控制增强器正在被创建,所以被跳过了。
⑥触发shiro增强器的创建,而shiro增强器又依赖SecurityManager
⑦SecurityManage又依赖authorizer,也就是自定义的Realm
⑧自定义Realm依赖业务实例,触发业务实例的创建。
⑨业务实例创建后要被事务AOP增强,但此时事务控制器还没完全创建好,所以无法代理,导致业务实例不带事务。
@Lazy注解原理
@Lazy标注在业务实体类上是不可行的。@Lazy要标注在Realm中依赖的业务实体类上。
。@Lazy要标注在Realm中依赖的业务实体类上后,在dubug可以发现,创建这个业务实体的实际是正常的实例化单例bean的时机。
@Lazy 的使用规则和对应的原理:
(1)@Lazy 标注在 Bean 的类上:告诉 IOC 容器,在容器初始化阶段不要实例化我。
(2)@Lazy 标注在其他 Bean 的依赖上:告诉 IOC 容器,在创建这个标注了 @Lazy 的 Bean 时,不要立即处理我标注的这个依赖。
授权
- 调用授权验证方法。Subject.isPermitted() 或者 Subject.hasRole()等。
- Subject实例通常是DelegatingSubject类或子类的实例对象。在认证开始时,通过SecurityManager实例来调用securityManager.isPermitted(string)方法或者security.hasRole(string)方法。
- SecurityManager委托
Authorizer
的实例调用响应的授权方法。 - 每一个Realm将检查是否实现了相同的Authorizer接口,然后调用Realm自己的相应的授权验证方法。
- 使用多个Realm时,不同于认证策略处理方式,授权处理过程中:
①当调用Realm出现异常时,立即抛出,结束授权验证
②只要一个Realm验证成功,则认为授权成功,立即返回,结束验证。
在shiro中提供了一些好用的注解
// 表示Subject已经通过login身份验证
@RequiresAuthentication
// 表示当前是用户身份
@RequiresUser
// 表示当是游客身份。
@RequiresGuest
// 表示当前需要的角色
@RequiresRoles(value={"admin", "user"}, logical= Logical.AND)
// 表示当前需要的权限
@RequiresPermissions (value={"p:admin", "p:user"}, logical= Logical.OR)
使用它之前,我们需要开启它的注解,怎么做呢?在ShiroConfig中加入:
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
然后就可以直接使用
@RestController
public class TestController {
@RequiresPermissions("p:user")
@RequestMapping("/index")
public String index(Model model) {
// 登录成后,即可通过Subject获取登录的用户信息
User user = (User) SecurityUtils.getSubject().getPrincipal();
model.addAttribute("user", user);
return "index --->" + user.getUsername();
}
}
权限异常捕获
对于没有权限的情况,想要自定义返回错误,可以通过异常捕获。
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler(value = AuthorizationException.class)
public String handleAuthorizationException() {
return "您当前没有权限访问~ 请联系管理员";
}
}