【Shiro】1.快速入门
Shiro官网地址:Shiro 官网:https://shiro.apache.org/
Shiro简介
概述
Apache Shiro是Java的一个安全框架。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。使用 Shiro 易于理解的 API,您可以快速轻松地保护任何应用程序—从最小的移动应用程序到最大的 web 和企业应用程序。
Shiro 的架构图如下:
优点
- 易于理解的 Java Security API
- 简单的身份认证,支持多种数据源
- 对角色的简单的授权,支持细粒度的授权
- 不跟任何的框架或者容器捆绑,可以独立运行
特性
特性 | 说明 |
Authentication |
身份认证/登录。验证用户是不是拥有相应的身份 |
Authorization |
授权(验证授权)。验证某个已认证的用户是否拥有某个权限,即判断用户是否能做事情 |
SessionManagement |
会话管理。即用户登录后就是一次会话,在没退出之前,它的所有信息都在会话中 |
Cryptography |
加密。保护数据的安全性,如密码加密存储到数据库,而不是明文存储。 |
Caching |
缓存。比如用户登录后,其用户信息,拥有的角色/权限不必每次都去查,提高效率。 |
Concurrency |
支持多线程应用的并发验证。即如在一个线程中开启另一个线程,能把权限自动传播过去。 |
Testing |
提供测试支持。 |
RunAs | 允许一个用户假装另一个用户(如果他们允许)的身份进行访问 |
RememberMe |
记住我。这是非常常见的功能。即一次登录后,下次在进入系统是不用登录了。 |
架构
架构 | 说明 |
Subject |
主体。代表了当前的“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫, 机器人等;即一个抽象概念;所有Subject都绑定到SercurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者。 |
SecurityManage |
安全管理器。即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject; 可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互。 |
Realm |
域。Shiro从Realm获取安全数据(如用户,角色,权限),就是说SecurityManager要验证用户身份, 那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以有1个或多个Realm,我们一般在应用中都需要实现自己的Realm。 |
SessionManager |
会话管理。如果写过Servlet就应该知道Session的概念,Session需要有人去管理它的生命周期,这个组件就是SessionManager。 |
SessionDAO |
即会话 dao。是对 session 会话操作的一套接口,比如要将 session 存储到数据库,可以通过 jdbc 将会话存储到数据库。 |
CacheManager |
缓存控制器。来管理如用户,角色,权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能。 |
Shiro 认证、授权
认证
身份认证:就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确
在认证过程中,涉及三个概念:
- Subject:主体。主体可以是用户、程序等,进行认证的都称为主体
- Principal:身份信息,是主体(Subject)进行身份认证的标识,标识必须具有唯一性。如:用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。
- Credential:凭证信息。只有主体自己知道的安全信息,如密码、证书等
授权
授权方式:
- 基于角色
- 基于资源
权限字符串
权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是:对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用 * 通配符。如:
- 用户创建权限:user:create,或user:create:*
- 用户修改实例 001 的权限:user:update:001
- 用户实例 001 的所有权限:user:*:001
快速入门
引入POM依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.13.0</version> </dependency>
创建初始化文件
shiro.ini
:位于 resources
资源目录下。
# 约定写法 [users] # 用户名=密码(,角色) christy=123456,admin zzc=666666,super tide=654321,guest [roles] super=* admin=guest:* guest=user:delete:zhangsan
建立测试类
这里,分为3个步骤:准备、认证、授权。
1 @Test 2 public void testShiroAuthor() { 3 //准备阶段// 4 // 1.创建安全管理器对象 5 DefaultSecurityManager securityManager = new DefaultSecurityManager(); 6 // 2.给安全管理器设置 Realm(域) 7 securityManager.setRealm(new IniRealm("classpath:shiro.ini")); 8 // 3.给全局安全工具类 SecurityUtils 设置安全管理器 9 SecurityUtils.setSecurityManager(securityManager); 10 // 4.拿到当前的 Subject 11 Subject subject = SecurityUtils.getSubject(); 12 // 5.创建令牌 13 AuthenticationToken token = new UsernamePasswordToken("christy","123456"); 14 System.out.println("token=" + token.getPrincipal() + " " + token.getCredentials() ); // token=christy [C@7adda9cc 15 try { 16 //认证阶段// 17 // 6.用户认证 18 System.out.println("认证状态:" + subject.isAuthenticated()); // 认证状态:false 19 subject.login(token); 20 System.out.println("认证状态:" + subject.isAuthenticated()); // 认证状态:true 21 } catch (UnknownAccountException e){ 22 e.printStackTrace(); 23 System.out.println("认证失败:用户不存在!"); 24 } catch (IncorrectCredentialsException e){ 25 e.printStackTrace(); 26 System.out.println("认证失败:密码不正确!"); 27 } catch (Exception e){ 28 e.printStackTrace(); 29 } 30 //授权// 31 // 7.角色授权 32 System.out.println("角色授权:" +subject.hasRole("admin")); // 角色授权:true 33 // 8.资源授权 34 System.out.println("资源授权"+subject.isPermitted("guest:update:01")); //资源授权true 35 }
如果项目中,创建安全管理器和授权的配置方法如下:
1 @Configuration 2 public class ShiroConfiguratin { 3 // 1.创建 shiroFilter,负责拦截所有请求 4 @Bean 5 public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){ 6 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); 7 //给filter设置安全管理器 8 shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager); 9 //配置系统受限资源 10 //配置系统公共资源 11 Map<String,String> map = new HashMap<>(); 12 // authc 请求这个资源需要认证和授权 13 map.put("/index", "authc"); 14 //默认认证界面路径 15 shiroFilterFactoryBean.setLoginUrl("/login"); 16 shiroFilterFactoryBean.setFilterChainDefinitionMap(map); 17 return shiroFilterFactoryBean; 18 } 19 20 //2.创建安全管理器 21 @Bean 22 public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){ 23 DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); 24 //给安全管理器设置 25 defaultWebSecurityManager.setRealm(realm); 26 return defaultWebSecurityManager; 27 } 28 29 //3.创建自定义realm 30 @Bean 31 public Realm getRealm(){ 32 return new SysUserRealm(); 33 } 34 }
自定义Realm
前面的测试类。用户名、密码、角色授权都是放在.ini的初始文件中。实际业务中,我们的用户名、密码和角色授权等基础信息。可能存储在数据库中。此时,我们可以自定义Realm。
查看源代码知:有两个抽象方法:
认证:AuthenticatingRealm#doGetAuthenticationInfo(AuthenticationToken)
授权:AuthorizingRealm#doGetAuthorizationInfo(PrincipalCollection)
且:AuthorizingRealm 继承于 AuthenticatingRealm。意味着自定义的 Reaml 只要继承 AuthorizingRealm,便可重写认证、授权方法
自定义 Reaml
1 public class CustomerRealm extends AuthorizingRealm { 2 3 // 授权 4 @Override 5 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { 6 return null; 7 } 8 9 /** 10 * 认证方法 11 * @param authenticationToken the authentication token containing the user's principal and credentials. 12 * @return 13 * @throws AuthenticationException 14 */ 15 @Override 16 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { 17 String username = "christy"; // 返回查询到的结果 18 String password = "123456"; // 返回查询到的结果 19 // 在token中获取用户名 20 String principal = (String) authenticationToken.getPrincipal(); 21 System.out.println("获取到用户输入的用户名"+principal); 22 // 模拟根据身份信息从数据库查询,假设根据用户名,查询到 用户名:principal 密码 123456 23 if(username.equals(principal)){ 24 // 参数说明:用户名 | 密码 | 当前realm的名字 25 return new SimpleAuthenticationInfo(principal,password, this.getName()); 26 } 27 return null; 28 } 29 30 }
测试
1 DefaultSecurityManager securityManager = new DefaultSecurityManager(); 2 securityManager.setRealm(new CustomerRealm());
实现授权
前面给Realm认证。并未授权。这里重写授权方法。
/** * 授权 * @param principalCollection the primary identifying principals of the AuthorizationInfo that should be retrieved. * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 从系统返回的身份信息集合中获取主身份信息(用户名) String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal(); System.out.println("用户名" + primaryPrincipal); // TODO 根据用户名获取当前用户的角色信息,以及权限信息 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); // 将数据库中查询到的角色信息赋值给权限对象 simpleAuthorizationInfo.addRole("admin"); simpleAuthorizationInfo.addRole("user"); // 将数据库中查询权限信息赋值给权限对象 simpleAuthorizationInfo.addStringPermission("user:*:01"); simpleAuthorizationInfo.addStringPermission("product:create"); return simpleAuthorizationInfo; }
授权打印的测试方法,这里省略。
加密
按照前面的操作。密码直接存于数据库中,这样并不安全。我们希望数据库存储的是加密后的密码。
Shiro 中提供了一整套的加密算法,并且提供了随机盐。Shiro 使用指定的加密算法将用户密码和随机盐进行加密,并按照指定的散列次数将散列后的密码存储在数据库中。由于随机盐每个用户可以不同,这就极大的提高了密码的安全性。
Md5Hash:有三个构造方法:
- public Md5Hash(Object source):只加密
- public Md5Hash(Object source, Object salt):加密、加盐
- public Md5Hash(Object source, Object salt, int hashIterations):加密、加盐、加散列次数
加密方法示例:
1 String passwod = "123456"; 2 // 只加密 3 System.out.println(new Md5Hash(passwod).toHex()); // e10adc3949ba59abbe56e057f20f883e 4 // 加密、加盐 5 System.out.println(new Md5Hash(passwod,"1q2w3e").toHex()); // 9eab7472e164bb8c1b823ae960467f74 6 // 加密、加盐、加散列次数 7 System.out.println(new Md5Hash(passwod,"1q2w3e",1024).toHex()); // 41a4e25bcf1272844e38b19047dd68a0
加密加盐后的认证方法:
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String username = "christy"; // 返回到查询的用户名 String passwordToMd5 = "41a4e25bcf1272844e38b19047dd68a0"; // 返回加密后的密码 // 在token中获取用户名 String principal = (String) authenticationToken.getPrincipal(); System.out.println("获取到用户输入的用户名"+principal); // 模拟根据身份信息从数据库查询,假设根据用户名,查询到 用户名:principal 密码 123456 if(username.equals(principal)){ // 参数说明:用户名 | 密码 | 当前realm的名字 return new SimpleAuthenticationInfo(principal,passwordToMd5, this.getName()); } return null; }