Shiro基础教程
Shiro介绍
什么Shiro
Shiro
是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。使用Shiro易于理解的API,您可以快速轻松的保护任何应用程序——从最小的移动应用程序到最大的Web和企业应用程序。
Shiro核心架构
认证流程
Subject
Subject
即主体,外部应用与subject进行交互,subject记录了当前的操作用户,将用户的概念理解为当前操作的主体。外部程序通过subject进行认证授权,而subject是通过SecurityManager安全管理器进行认证授权
SecurityManager
SecurityManager
即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等
SecurityManager
是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口
Authenticator
Authenticator
即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器
Authorizer
Authorizer
即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限
Realm
即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息
不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码
SessionManager
sessionManager
即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录
SessionDAO
SessionDAO
即会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库
CacheManager
CacheManager
即缓存管理,将用户权限数据存储在缓存,这样可以提高性能
Cryptography
Cryptography
即密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。
Shiro中的认证
什么是认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确
三个概念
Subject
访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体
Principal
身份信息,是主体(subject)进行身份认证的标识,标识必须具有唯一性
,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)
credential
凭证信息,是只有主体自己知道的安全信息,如密码、证书等
认证的实现
创建一个普通的maven项目,引入shiro的pom依赖
1 2 3 4 5 6
| <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.5.3</version> </dependency> 12345
|
引入shiro配置文件shiro.ini
,并加入以下配置
1 2 3 4 5 6
| # 约定写法 [users] # 用户名=密码 christy=123456 tide=654321 12345
|
shiro的配置文件是一个.ini文件,类似于.txt文件
.ini文件经常用作某些软件的特定的配置文件,可以支持一些复杂的数据格式,shiro可以按照内部约定的某种格式读取配置文件中的数据
之所以提供这个配置文件是用来学习shiro时书写我们系统中相关的权限数据,从而减轻配置数据库并从数据库读取数据的压力,降低学习成本,提高学习效率
测试Java代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.text.IniRealm; import org.apache.shiro.subject.Subject;
public class ShiroAuthenticatorTest { public static void main(String[] args){ DefaultSecurityManager securityManager = new DefaultSecurityManager(); securityManager.setRealm(new IniRealm("classpath:shiro.ini")); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); AuthenticationToken token = new UsernamePasswordToken("christy","123456");
try { System.out.println("认证状态:"+subject.isAuthenticated()); subject.login(token); System.out.println("认证状态:"+subject.isAuthenticated()); } catch (UnknownAccountException e){ e.printStackTrace(); System.out.println("认证失败:用户不存在!"); } catch (IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("认证失败:密码不正确!"); } catch (Exception e){ e.printStackTrace(); } } }
|
认证的几种状态
UnknownAccountException:用户名错误
IncorrectCredentialsException:密码错误
DisabledAccountException:账号被禁用
LockedAccountException:账号被锁定
ExcessiveAttemptsException:登录失败次数过多
ExpiredCredentialsException:凭证过期
Shiro认证的源码分析
上面我们已经简单实现了shiro的认证,但是shiro内部认证的具体流程是怎么样的,这次我们通过追踪源码的方式具体分析一下。我们在认证处打上断点,点击debug模式运行,然后一步步运行到最后,中间经过的类我们都记录下来
至此啊,在SimpleAccountRealm中完成了用户名的认证。
那么密码呢?在哪里校验的呢?我们继续点击下一步,直到这里
我们看到这里断言密码是否匹配的方法,点进去
我们看到了这里拿的是我们输入的密码和根据token取出的用户中的密码做的比较来验证密码是否正确,这是系统帮我们完成的
上面我们说了用户的认证是在SimpleAccountRealm
的doGetAuthenticationInfo
的方法中完成的,而SimpleAccountRealm继承自AuthorizingRealm
,而AuthorizingRealm
中有一个抽象方法
1 2
| protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection var1); 1
|
SimpleAccountRealm就是复写了AuthorizingRealm
中的这个抽象方法实现的用户认证,所以后面我们需要自定义认证的时候我们就可以自定义一个realm继承自AuthorizingRealm
来复写doGetAuthorizationInfo,在这个方法里面实现我们自己的认证逻辑
不仅认证,有意思的是AuthorizingRealm
是继承自AuthenticatingRealm
,而AuthenticatingRealm中有个抽象方法
1 2
| protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken var1) throws AuthenticationException; 1
|
这个方法是实现用户授权的方法。
也就是说通过我们自定义realm继承AuthorizingRealm
就可以同时复写认证和授权两个方法
Realm的继承关系如下
4、身份认证流程
(1)首先调用 Subject.login(token) 进行登录,其会自动委托给 SecurityManager
(2)SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份
验证;
(3)Authenticator 才是真正的身份验证者,Shiro API 中核心的身份 认证入口点,此
处可以自定义插入自己的实现;
(4)Authenticator 可能会委托给相应的 AuthenticationStrategy 进 行多 Realm 身份
验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm
身份验证;
(5) Authenticator 会把相应的 token 传入 Realm,从 Realm 获取 身份验证信息,如
果没有返回/抛出异常表示身份验证失败了。此处 可以配置多个Realm,将按照相应的顺序
及策略进行访问。
Shiro使用自定义Relam实现认证
上面我们实现了简单的认证并且分析了认证的基本流程,通常情况下shiro的认证都是通过自定义relam来实现的
CustomerRealm
首先我们编写自定义realm的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection;
public class CustomerRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String principal = (String) token.getPrincipal(); System.out.println(principal); if("christy".equals(principal)){ SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,"123456",this.getName()); return simpleAuthenticationInfo; }
return null; } }
|
CustomerRealmAuthenticatorTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import com.christy.shiro.realm.CustomerRealm; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.subject.Subject;
public class CustomerRealmAuthenticatorTest { public static void main(String[] args) { DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(new CustomerRealm()); SecurityUtils.setSecurityManager(defaultSecurityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("christy", "123456"); try { subject.login(token); System.out.println(subject.isAuthenticated()); } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("用户名错误"); } catch (IncorrectCredentialsException e) { e.printStackTrace(); System.out.println("密码错误"); } } }
|
测试
以上代码编写完成后,我们运行CustomerRealmAuthenticatorTest
里面的main方法,执行结果如下
Shiro的加密和随机盐
Shiro中密码的加密策略
实际应用中用户的密码并不是明文存储在数据库中的,而是采用一种加密算法将密码加密后存储在数据库中。Shiro中提供了一整套的加密算法,并且提供了随机盐。shiro使用指定的加密算法将用户密码和随机盐进行加密,并按照指定的散列次数将散列后的密码存储在数据库中。由于随机盐每个用户可以不同,这就极大的提高了密码的安全性。
Shiro中的加密方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import org.apache.shiro.crypto.hash.Md5Hash;
public class ShiroMD5Test { public static void main(String[] args){ Md5Hash md5Hash01 = new Md5Hash("123456"); System.out.println(md5Hash01.toHex());
Md5Hash md5Hash02 = new Md5Hash("123456","1q2w3e"); System.out.println(md5Hash02.toHex());
Md5Hash md5Hash03 = new Md5Hash("123456","1q2w3e",1024); System.out.println(md5Hash03.toHex()); } }
|
运行结果如下
1 2 3 4
| e10adc3949ba59abbe56e057f20f883e 9eab7472e164bb8c1b823ae960467f74 41a4e25bcf1272844e38b19047dd68a0 123
|
Shiro中自定义加密Realm
CustomerMD5Realm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource;
public class CustomerMD5Realm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String principal = (String) token.getPrincipal(); if("christy".equals(principal)){
return new SimpleAuthenticationInfo(principal, "41a4e25bcf1272844e38b19047dd68a0", ByteSource.Util.bytes("1q2w3e"), this.getName()); } return null; } }
|
CustomerMD5AuthenticatorTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| import com.christy.shiro.realm.CustomerMD5Realm; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.subject.Subject;
public class CustomerMD5AuthenticatorTest { public static void main(String[] args) { DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); CustomerMD5Realm realm = new CustomerMD5Realm(); HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); credentialsMatcher.setHashAlgorithmName("md5"); credentialsMatcher.setHashIterations(1024); realm.setCredentialsMatcher(credentialsMatcher); defaultSecurityManager.setRealm(realm); SecurityUtils.setSecurityManager(defaultSecurityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("christy", "123456"); try { subject.login(token); System.out.println("认证成功"); } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("用户名错误"); } catch (IncorrectCredentialsException e) { e.printStackTrace(); System.out.println("密码错误"); } } }
|
测试
Shiro中的授权
什么是授权
授权可简单理解为who对what(which)进行How操作:
Who,即主体(Subject)
,主体需要访问系统中的资源。
What,即资源(Resource)
,如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型
和资源实例
,比如商品信息为资源类型
,类型为t01的商品为资源实例
,编号为001的商品信息也属于资源实例。
How,权限/许可(Permission)
,规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。
授权方式
基于角色的访问控制
RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制
1 2 3 4
| if(subject.hasRole("admin")){ } 123
|
基于资源的访问控制
RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制
1 2 3 4 5 6 7
| if(subject.isPermission("user:update:01")){ } if(subject.isPermission("user:update:*")){ } 123456
|
权限字符串
权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。
例子:
- 用户创建权限:user:create,或user:create:*
- 用户修改实例001的权限:user:update:001
- 用户实例001的所有权限:user:*:001
权限的编码方式
编程式
1 2 3 4 5 6 7
| Subject subject = SecurityUtils.getSubject(); if(subject.hasRole("admin")) { } else { } 123456
|
注解式
1 2 3 4 5
| @RequiresRoles("admin") public void hello() { } 1234
|
标签式
1 2 3 4 5 6
| JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成: <shiro:hasRole name="admin"> <!— 有权限—> </shiro:hasRole> 注意: Thymeleaf 中使用shiro需要额外集成!
|
授权的实现
我们基于上面MD5加密的例子进行修改
CustomerMD5Realm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource;
public class CustomerMD5Realm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String primaryPrincipal = (String) principals.getPrimaryPrincipal(); System.out.println("用户名: "+primaryPrincipal);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("admin"); simpleAuthorizationInfo.addRole("user");
simpleAuthorizationInfo.addStringPermission("user:*:01"); simpleAuthorizationInfo.addStringPermission("product:create");
return simpleAuthorizationInfo; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String principal = (String) token.getPrincipal(); if("christy".equals(principal)){
return new SimpleAuthenticationInfo(principal, "41a4e25bcf1272844e38b19047dd68a0", ByteSource.Util.bytes("1q2w3e"), this.getName()); } return null; } }
|
CustomerMD5AuthenticatorTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| import com.christy.shiro.realm.CustomerMD5Realm; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.subject.Subject;
public class CustomerMD5AuthenticatorTest { public static void main(String[] args) { DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); CustomerMD5Realm realm = new CustomerMD5Realm(); HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); credentialsMatcher.setHashAlgorithmName("md5"); credentialsMatcher.setHashIterations(1024); realm.setCredentialsMatcher(credentialsMatcher); defaultSecurityManager.setRealm(realm); SecurityUtils.setSecurityManager(defaultSecurityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("christy", "123456"); try { subject.login(token); System.out.println("认证成功"); } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("用户名错误"); } catch (IncorrectCredentialsException e) { e.printStackTrace(); System.out.println("密码错误"); } if(subject.isAuthenticated()){ System.out.println(subject.hasRole("super"));
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "super")));
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "super", "user")); for (boolean aBoolean : booleans) { System.out.println(aBoolean); } System.out.println("==============================================");
System.out.println("权限:"+subject.isPermitted("user:update:01")); System.out.println("权限:"+subject.isPermitted("product:create:02"));
boolean[] permitted = subject.isPermitted("user:*:01", "order:*:10"); for (boolean b : permitted) { System.out.println(b); }
boolean permittedAll = subject.isPermittedAll("user:*:01", "product:create:01"); System.out.println(permittedAll); } } } 12
|
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 用户名: christy false 用户名: christy 用户名: christy false 用户名: christy 用户名: christy 用户名: christy true false true ============================================== 用户名: christy 权限:true 用户名: christy 权限:true 用户名: christy 用户名: christy true false 用户名: christy 用户名: christy true
|