Shiro探索与笔记
核心概念
Shiro 里的几乎所有组件可用POJO
兼容的任何配置机制进行配置实现:普通的Java
代码、Spring XML
、YAML
、.properties
和.ini
文件。
Subject
Subject
一词是一个安全术语,其意味着“当前跟软件交互的东西”。主体可以包括第三方进程、后台帐户或其他类似事物。
在代码的任何地方,你都能轻易的获得Shiro Subject
:
Subject currentUser = SecurityUtils.getSubject();
SecurityManager
SecurityManager
管理所有Subject
的安全操作,它是Shiro
框架的核心,充当“保护伞”。
一旦SecurityManager
及其内部对象图配置好,它就会退居幕后,应用开发人员几乎把他们的所有时间都花在Subject API
调用上。一个应用几乎总是只有一个 SecurityManager 实例。
SecurityManager
负责真正的身份验证逻辑;它会委托给Authenticator
进行身份验证;而Realm
中实现了Authenticator
。
Realms
Realm
充当了Shiro
与应用安全数据间的“桥梁”或者“连接器”,它封装了数据源的连接细节,并在需要时将相关数据(如用户、角色、权限)提供给Shiro
,至少需要一个Realm
用于认证和(或)授权。
org.apache.shiro.realm.Realm
接口如下:
String getName(); //返回一个唯一的 Realm 名字
boolean supports(AuthenticationToken token); //判断此 Realm 是否支持此 Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException; //根据 Token 获取认证信息
Maven导入
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.4.0</version>
<type>pom</type>
</dependency>
</dependencies>
Shiro异常
ShiroException的子类如下,常用的是AuthenticationException
下的子级异常:
类 | 描述 |
---|---|
AuthenticationException | 由于身份验证过程中的错误而引发的常规异常。 |
AuthorizationException | 如果在授权期间出现问题(访问控制检查),则抛出异常。 |
CacheException | Shiro中与缓存操作相关的所有异常的根类。 |
CodecException | 与编码或解码期间的问题相关的根异常。 |
ConfigurationException | 表示解析或处理Shiro配置时出现问题的根异常。 |
CryptoException | 在加密操作期间遇到的基本Shiro异常。 |
DataAccessException | 代表在尝试访问数据时的通用异常。 |
EnvironmentException | 针对与环境实例或配置相关的错误抛出异常。 |
ExecutionException | 包含在Subject执行Callable时抛出的任何异常。 |
InstantiationException | 当无法通过反射实例化类时,框架抛出的运行时异常。 |
InvalidPermissionStringException | 当正在解析的String对该解析程序无效时。 |
SerializationException | 对数据进行序列化或反序列化问题的根异常。 |
SessionException | 在会话期间与系统交互期间的一般安全异常。 |
UnavailableSecurityManagerException | 尝试获取应用程序的SecurityManager实例时抛出异常。 |
UnknownClassException | 相当于JDK的ClassNotFoundException。 |
常用异常
只列举AuthenticationException
下的异常
父类 | 类 | 简述 | 描述 |
---|---|---|---|
AccountException | ConcurrentAccessException | 超出并发登录人数 | 配置了并发登录人数后,接收到已验证或登录的帐户的身份验证尝试时抛出异常。 |
AccountException | DisabledAccountException | 账号已被禁用 | 尝试进行身份验证时抛出,并且由于某种原因已禁用相应的帐户。 |
AccountException | ExcessiveAttemptsException | 身份验证错误次数过多 | 在一定时间内尝试身份验证超过一定次数,且无法通过验证时抛出的异常 |
AccountException | UnknownAccountException | 未知账号异常 | 尝试使用系统中不存在的主体进行身份验证时抛出 |
CredentialsException | ExpiredCredentialsException | 过期的凭证异常 | 当系统确定提交的凭据已过期且不允许登录时,在身份验证过程中抛出。 |
CredentialsException | IncorrectCredentialsException | 错误的凭证异常 | 尝试使用与帐户主体关联的实际凭据不匹配的凭据进行身份验证时抛出此异常。 |
示例
读取ini文件进行登录
// 获取 SecurityManager
IniRealm iniRealm = new IniRealm(IniFilterChainResolverFactory.DEFAULT_INI_RESOURCE_PATH);
SecurityManager securityManager = new DefaultSecurityManager(iniRealm);
// SecurityManager 绑定给 SecurityUtils
SecurityUtils.setSecurityManager(securityManager);
// 获取交互主体,创建令牌
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
try {
// 身份验证
subject.login(token);
} catch (AuthenticationException e) {
// 验证失败
System.out.println(e.getMessage());
}
// 断言用户已经登录
Assert.assertEquals(true, subject.isAuthenticated());
// 登出
subject.logout();
INI知识
1、对象名=全限定类名 相对于调用 public 无参构造器创建对象
2、对象名.属性名=值 相当于调用 setter 方法设置常量值
3、对象名.属性名=$对象引用 相当于调用 setter 方法设置对象引用
shiro的ini都在根对象SecurityManager
下
[main]
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
authenticationStrategy=org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy
authenticator.authenticationStrategy=$authenticationStrategy
源码分析
Realm
AuthenticatingRealm
AuthenticatingRealm
的官方描述中,有如下几个重点:
- 仅实现身份验证支持(登录)操作并将授权(访问控制)行为留给子类。
- 通过设置
authenticationCachingEnabled = true
启用身份验证缓存,可以减轻任何后端数据源的持续负载。 - 如果可以,请始终以安全形式(散列+盐或加密)表示和存储凭据,以消除明文可见性。
那么AuthenticatingRealm
是如何实现身份验证的?
在源码中匹配身份实际使用的是成员变量CredentialsMatcher
,它的默认实现是通过SimpleCredentialsMatcher
类实现的,当然也可以通过构造函数或setter
方法来自定义CredentialsMatcher
。
而在SimpleCredentialsMatcher
类中,如果相比较的两个值通过了CodecSupport.isByteSource
方法的验证,则使用java.security.MessageDigest
的isEqual
方法比较两个值是否相等,否则使用Object.equals
方法比较他们是否相等。
CodecSupport.isByteSource
源码:
protected boolean isByteSource(Object o) {
return o instanceof byte[]
|| o instanceof char[]
|| o instanceof String
|| o instanceof ByteSource
|| o instanceof File
|| o instanceof InputStream;
}
如果getAuthenticationCacheKey(AuthenticationToken)
和getAuthenticationCacheKey(PrincipalCollection)
的返回值不同,将会根据缓存提供程序设置(Cache provider settings)来进行删除,例如基于timeToIdle或timeToLive(TTL)值删除缓存条目.
AuthorizingRealm
AuthorizingRealm
继承于AuthenticatingRealm
,也可以通过authorizationCachingEnabled
启用缓存,其实现了所有角色和权限检查。
角色检查和权限检查的所有方法都实现自Authorizer
接口。角色检查有hasRole
,hasRoles
,hasAllRoles
,checkRole
,checkRoles
方法,这些方法最终调用hasRole
方法:
protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) {
return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier);
}
has和check开头的方法的区别是has返回的是boolean
值,而check则是抛出异常。这个方法最终使用的是java.util.Collection
的contains
方法进行比较。
权限检查有checkPermission
,checkPermissions
,isPermitted
,isPermittedAll
方法,这些方法最终调用isPermitted
方法:
protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
Collection<Permission> perms = getPermissions(info);
if (perms != null && !perms.isEmpty()) {
for (Permission perm : perms) {
if (perm.implies(permission)) {
return true;
}
}
}
return false;
}
在isPermitted
方法中,最终是通过我们传入的Permission
的implies
实现比较的,在1.4.0
版的shiro
中只有在WildcardPermission
(通配符许可)类里面有一个实现方法。
JdbcRealm
JdbcRealm
内部查询语句对表名和列名的定义如下:
表 | 列 |
---|---|
users | password, password_salt, username |
user_roles | role_name, username |
其内部的成员变量authenticationQuery
、userRolesQuery
、permissionsQuery
都定义了setter
方法,通过这些方法可以自定义查询语句。
JdbcRealm
定义了三个参数:
dataSource
:JDBC数据源。permissionsLookupEnabled
:定义是否需要查询权限,仅将permissionsLookupEnabled
设置为true
时才会检索权限。JdbcRealm.SaltStyle
:定义密码salt,总共有四个值:
public static enum SaltStyle {
NO_SALT, // 密码哈希没有salt。
CRYPT, // 密码哈希以unix crypt格式存储。
COLUMN, // salt位于数据库的单独列中。
EXTERNAL; // salt未存储在数据库中。 将调用JdbcRealm.getSaltForUser(String)来获取salt
}
注:
CRYPT 是一种密码加密方式,只适用于密码的使用,不适合用于资料加密。其一般出现在unix/linux平台上。
salt 是在密码加密前,为密码添加一个随机生成的字符。
SaltStyle
的值默认为NO_SALT
,其只会影响到密码查询,当值为COLUMN
且authenticationQuery
的值未被更改时,它会查询password_salt
字段,源码如下:
public void setSaltStyle(JdbcRealm.SaltStyle saltStyle) {
this.saltStyle = saltStyle;
if (saltStyle == JdbcRealm.SaltStyle.COLUMN
&& this.authenticationQuery.equals("select password from users where username = ?")) {
this.authenticationQuery = "select password, password_salt from users where username = ?";
}
}
需要注意的是getSaltForUser
如不重写的话,实际上是将用户名当salt
:
protected String getSaltForUser(String username) {
return username;
}
Permission
WildcardPermission
通配符权限的表现形式为:资源标识符:操作:对象实例ID,“操作”和“对象实例ID”可以省略,默认为“*”字符来匹配所有“操作”和“对象实例ID”。
官方对通配符规则的描述:
例如,您可以授予用户“
newsletter:edit:12,13,18
”。在本例中,假设第三个令牌是时事通讯的系统ID。这将允许用户编辑时事通讯12、13和18。这是一种非常强大的权限表达方式,因为您现在可以这样说:“newsletter:*:13
”(为newsletter 13授予用户所有操作)、“newsletter:view,create,edit:*
”(允许用户查看、创建或编辑任何时事通讯)或“newsletter:*:*
(允许用户对任何时事通讯执行任何操作)。
WildcardPermission
类的核心方法只有两个:解析方法setParts
和比对方法implies
。setParts
方法源码如下:
protected void setParts(String wildcardString, boolean caseSensitive) {
wildcardString = StringUtils.clean(wildcardString);
if (wildcardString == null || wildcardString.isEmpty()) {
throw new IllegalArgumentException("Wildcard string cannot be null or empty. Make sure permission strings are properly formatted.");
}
if (!caseSensitive) {
wildcardString = wildcardString.toLowerCase();
}
List<String> parts = CollectionUtils.asList(wildcardString.split(PART_DIVIDER_TOKEN));
this.parts = new ArrayList<Set<String>>();
for (String part : parts) {
Set<String> subparts = CollectionUtils.asSet(part.split(SUBPART_DIVIDER_TOKEN));
if (subparts.isEmpty()) {
throw new IllegalArgumentException("Wildcard string cannot contain parts with only dividers. Make sure permission strings are properly formatted.");
}
this.parts.add(subparts);
}
if (this.parts.isEmpty()) {
throw new IllegalArgumentException("Wildcard string cannot contain only dividers. Make sure permission strings are properly formatted.");
}
}
方法参数caseSensitive
属性用于区分大小写,如果为false
则会全部转换为小写字母。
整个方法的逻辑也很简单,首先对wildcardString
进行“:
”分割,并转成List
数据结构;再进行“,
分割然后放入List<Set<String>>
中”,其逻辑的简单写法如下:
List<Set<String>> partSets = new ArrayList<>();;
String wildcardString = "newsletter:view,create,edit:*";
// 以":"分割
String[] wildcardStrings = wildcardString.split(":");
List<String> parts = CollectionUtils.asList(wildcardStrings);
for (String part : parts) {
// 以","分割
Set<String> subparts = CollectionUtils.asSet(part.split(","));
partSets.add(subparts);
}
最后是比对方法implies
,只需要判断传入的Permission
类规则是当前类规则的子集就可了,即当前类规则集合包含了整个传入类规则集合,值得注意的是WILDCARD_TOKEN
常量的*
通配符能够匹配任意字符:
public boolean implies(Permission p) {
if (!(p instanceof WildcardPermission)) {
return false;
}
WildcardPermission wp = (WildcardPermission) p;
List<Set<String>> otherParts = wp.getParts();
int i = 0;
for (Set<String> otherPart : otherParts) {
if (getParts().size() - 1 < i) {
return true;
} else {
Set<String> part = getParts().get(i);
if (!part.contains(WILDCARD_TOKEN) && !part.containsAll(otherPart)) {
return false;
}
i++;
}
}
for (; i < getParts().size(); i++) {
Set<String> part = getParts().get(i);
if (!part.contains(WILDCARD_TOKEN)) {
return false;
}
}
return true;
}
【待续】且无限延期