Shiro执行逻辑Shiro的组织架构Shiro身份验证
演示Realm配置 Shiro授权
授权方式演示 Springboot集成Shiro
拦截用户请求认证用户密码加密授权 Shiro执行逻辑
Application Code:编写的应用程序Subject: 主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;Realm:领域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。
最简单的一个Shiro应用
应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。 Shiro的组织架构由图看出,Security Manager是最复杂的系统
Security Manager内部
Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;Authorizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所以呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能Cryptography:密码模块,Shiro 提供了一些常见的加密组件用于如密码加密 / 解密的。
领域Realm的实现
可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;
Shiro身份验证身份验证,即在应用中谁能证明他就是他本人。一般提供如他们的身份 ID 一些标识信息来表明他就是他本人,如提供身份证,用户名 / 密码来证明。
shiro中进行身份验证,需要提供principals(身份)和credentials(证明)
身份告诉shiro你是什么用户,证明用来证明你是这个身份。
最常见的principals和credentials就是用户名和密码了
principals = 用户名/邮箱/手机号等等
credentials = 密码/验证码/数字凭证等等
下面利用springboot环境来演示Shiro如何进行身份验证
演示准备环境依赖
准备用户身份
采用shiro.ini配置文件指定两个subject,一个用户名为wang,密码123,一个为zhang,密码123
[users]wang = 123zhang = 123
测试代码
import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.config.IniSecurityManagerFactory;import org.apache.shiro.mgt.SecurityManager;import org.apache.shiro.subject.Subject;import org.junit.jupiter.api.Test;import org.springframework.boot.test.context.SpringBootTest;@SpringBootTestclass DemoApplicationTests { @Test public void MainTest(){// 初始化SecurityManager工厂,这里用ini文件作为realm IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");// 利用工厂产生一个SecurityManager实例 SecurityManager securityManager = factory.getInstance();// 将sm实例绑定到shiro提供的工具类中,下面的操作要利用此工具类 SecurityUtils.setSecurityManager(securityManager);// 获得主体,这里的主体是ini文件中的两个用户 Subject subject = SecurityUtils.getSubject();// 利用shiro提供的类创建一个虚拟用户用来登录 UsernamePasswordToken token = new UsernamePasswordToken("wang", "123"); try {// 对身份进行验证 subject.login(token); } catch (AuthenticationException e) { System.out.println("用户名和密码错误,错误信息:"+e.getMessage()); }// 判断用户是否验证通过,通过返回true,不通过返回false boolean authenticated = subject.isAuthenticated(); if(authenticated) System.out.println("登录成功"); else System.out.println("登录失败");// 退出登录 subject.logout(); }}
对29行错误信息的解释
如果身份验证失败请捕获 AuthenticationException 或其子类,常见的如: DisabledAccountException(禁用的帐号)、LockedAccountException(锁定的帐号)、UnknownAccountException(错误的帐号)、ExcessiveAttemptsException(登录失败次数过多)、IncorrectCredentialsException (错误的凭证)、ExpiredCredentialsException(过期的凭证)等,具体请查看其继承关系;对于页面的错误消息展示,最好使用如 “用户名 / 密码错误” 而不是 “用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库;
从如上代码可总结出身份验证的步骤:
收集用户身份 / 凭证,即如用户名 / 密码;调用 Subject.login 进行登录,如果失败将得到相应的 AuthenticationException 异常,根据异常提示用户错误信息;否则登录成功;最后调用 Subject.logout 进行退出操作。如上测试的几个问题:
用户名 / 密码硬编码在 ini 配置文件,以后需要改成如数据库存储,且密码需要加密存储;用户身份 Token 可能不仅仅是用户名 / 密码,也可能还有其他的,如登录时允许用户名 / 邮箱 / 手机号同时登录。Realm配置从代码可以看出这些操作好像是subject完成的,但实际上,全部是subject委托给securityManager完成的,而SecurityManager对于身份验证的操作,会交给自己的子模块Authenticator完成,Authenticator将获取到的token(用户信息及凭证)交给realm,realm如果没有抛出异常,那么身份就验证成功了。
可以看出,Realm是验证身份的最终站,因此,我们可以配置多个Realm来多次验证Authenticator交过来的token
Realm是安全数据源,我们刚刚的演示中使用了ini文件作为数据源,那么我们使用的Realm在shiro中就是
org.apache.shiro.realm.text.IniRealm
shiro中定义了Realm接口,他是所有Realm的父类接口,代码如下
String getName(); //返回一个唯一的Realm名字boolean supports(AuthenticationToken token); //判断此Realm是否支持此TokenAuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException; //根据Token获取认证信息
我们可以自定义Realm,然后通过ini配置文件指定securityManager的Realm实现,这样便可实现多Realm验证
创建一个shiro-multi-realm.ini文件,假设现在我们自定义了两个Realm
那么可以通过下面的配置文件进行多Realm配置
#声明一个realmmyRealm1 = com.whr.realm.MyRealm1myRealm2 = com.whr.realm.MyRealm2#$符号表示引用上面已经定义好的对象#指定securityManager的realms实现,这一行可删除,删除后将按照上面声明的顺序加载securityManager.realms=$myRealm1,$myRealm2
securityManager 会按照 realms 指定的顺序进行身份认证。此处我们使用显示指定顺序的方式指定了 Realm 的顺序,如果删除 “securityManager.realms=$myRealm1,$myRealm2”,那么securityManager 会按照 realm 声明的顺序进行使用(即无需设置 realms 属性,其会自动发现),当我们显示指定 realm 后,其他没有指定 realm 将被忽略,如 “securityManager.realms=$myRealm1”,那么 myRealm2 不会被自动设置进去。
Shiro提供的Realm
以后一般继承 AuthorizingRealm(授权)即可;其继承了 AuthenticatingRealm(即身份验证),而且也间接继承了 CachingRealm(带有缓存实现)。
Shiro授权为什么需要授权,因为在页面中,有些资源我们不想让没有权限的人看到,例如在一个后台管理系统登录窗口页面,我们会有几个角色,例如:管理员(admin),用户(user)等等,那么管理员和用户能看到的页面和进行的操作一定是不同的。这时我们就需要对不同的角色进行授权。
在授权中需了解的几个关键对象:
主体(Subject)即访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只有授权后才允许访问相应的资源。
资源(Resource) 在应用中用户可以访问的URL,比如访问 JSP 页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。
权限(Permission) 安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如: 访问用户列表页面 查看/新增/修改/删除用户数据(即很多时候都是 CRUD(增查改删)式权限控制)打印文档等。如上可以看出,权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许,不反映谁去执行这个操作。所以后续还需要把权限赋予给用户,即定义哪个用户允许在某个资源上做什么操作(权限),Shiro 不会去做这件事情,而是由实现人员提供。
角色(Role)。 角色代表了操作集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。
授权方式Shiro 支持三种方式的授权:
编程式:通过写 if/else 授权代码块完成:
Subject subject = SecurityUtils.getSubject();if(subject.hasRole(“admin”)) { //有权限} else { //无权限}
注解式:通过在执行的 Java 方法上放置相应的注解完成:
@RequiresRoles("admin")public void hello() { //有权限}
没有权限将抛出相应的异常;
JSP/GSP 标签:在 JSP/GSP 页面通过相应的标签完成:
基于角色的演示控制
[users]wang = 123 ,admin,userzhang = 123 ,user
给wang用户设置了两个角色,admin和user,zhang用户只有user角色
import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.config.IniSecurityManagerFactory;import org.apache.shiro.mgt.SecurityManager;import org.apache.shiro.subject.Subject;import org.junit.jupiter.api.Test;import org.springframework.boot.test.context.SpringBootTest;@SpringBootTestclass DemoApplicationTests { static { SecurityManager securityManager = new IniSecurityManagerFactory("classpath:shiro.ini"). getInstance(); SecurityUtils.setSecurityManager(securityManager); } @Test public void MainTest(){ Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken zhang = new UsernamePasswordToken("zhang", "123"); subject.login(zhang); if(subject.hasRole("admin")){ System.out.println("have permission"); }else{ System.out.println("don't have permission"); }// 退出登录 subject.logout(); }}
上述示例只演示了测试此用户是否拥有某一角色,下面给此角色赋予权限
[users]zhang=123,admin,userwang=123,user[roles]admin = admin:create,admin:delete,admin:updateuser = user:read
@Test public void MainTest(){ Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken zhang = new UsernamePasswordToken("zhang", "123"); subject.login(zhang); if(subject.hasRole("user")){ System.out.println("have role");// 判断此用户是否拥有删除权限,没有则报错 try{ subject.checkPermission("admin:delete"); }catch(UnauthorizedException e){ System.out.println(e.getMessage()); } }else{ System.out.println("don't have role"); }// 退出登录 subject.logout(); }
//报错信息Subject does not have permission [admin:delete]
Springboot集成Shiro开始之前,所有页面结构
在pom.xml中添加如下三个依赖
在yml文件中进行配置
spring: thymeleaf: #配置模板路径,默认是templates,可以不用配置 prefix: classpath:/templates/ suffix: .html cache: false #关闭页面缓存
自定义一个MyShiroConfig
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;import org.apache.shiro.web.mgt.DefaultWebSecurityManager;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class MyShiroConfig {// 利用securityManager对象来注册shiroFilter @Bean(name = "shiroFilter") public ShiroFilterFactoryBean getShiroFilterFactoryBean (@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();// 关联SecurityManager shiroFilter.setSecurityManager(defaultWebSecurityManager); return shiroFilter; }// 注册SecurityManager对象,其中底层的realm用自己的实现 @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();// 关联realm securityManager.setRealm(userRealm); return securityManager; }// 注册realm,这个realm是自定义的 @Bean public UserRealm userRealm(){ return new UserRealm(); }}
自定义一个Realm
import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.AuthenticationInfo;import org.apache.shiro.authc.AuthenticationToken;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.realm.AuthenticatingRealm;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;public class UserRealm extends AuthorizingRealm {// 给用户授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行doGetAuthorizationInfo-PrincipalCollection"); return null; }// 认证用户的token @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行doGetAuthorizationInfo-AuthenticationToken"); return null; }}
上面的代码自定义了UserRealm,这个说明了SecurityManager需要realm时的访问对象,然后定义了一个过滤器,这个过滤器可以拦截用户的请求并进行页面的跳转
在前端控制器中拿到前端发来的数据后,封装成token,交付给SecurityManager对象,sm再去Realm中去验证用户的身份及权限。
例如,我们拥有下面的一个Controller控制器
@RequestMapping("/userLogin") public String login(String username, String password, Model model){// 获取当前登录的用户 Subject subject = SecurityUtils.getSubject();// 封装用户的登录数据 UsernamePasswordToken token = new UsernamePasswordToken(username, password); try{// 这里的login将会把token交付给securityManager,sm再去UserRealm中认证用户 subject.login(token);// 登录成功跳转 return "index"; }catch (UnknownAccountException e){ //用户名不存在 model.addAttribute("msg","用户名不存在"); return "login"; }catch (IncorrectCredentialsException e){ //密码不正确 model.addAttribute("msg","密码错误"); return "login"; } }
当用户发起/userLogin请求时,前端将用户名和密码封装成一个token,同时调用subject的login方法,会将token交给SM对象,SM对象再访问UserRealm中去验证用户身份
拦截用户请求如果想要拦截用户请求,那么需要在MyShiroConfig类中修改shiroFilter进行配置
@Bean(name = "shiroFilter") public ShiroFilterFactoryBean getShiroFilterFactoryBean (@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(defaultWebSecurityManager);// 添加shiro的内置过滤器,对请求进行拦截 Map
到此springboot的基本操作就演示完毕了,在jsp的页面中,shiro可以利用标签进行一些测试,在thymeleaf中也可以利用标签,只不过需要加载新的jar包,是thymeleaf和shiro整合的jar包,具体的我没有研究,具体用到再说。