Shiro探索与笔记

Shiro探索与笔记

核心概念

Shiro 里的几乎所有组件可用POJO兼容的任何配置机制进行配置实现:普通的Java代码、Spring XMLYAML.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如果在授权期间出现问题(访问控制检查),则抛出异常。
CacheExceptionShiro中与缓存操作相关的所有异常的根类。
CodecException与编码或解码期间的问题相关的根异常。
ConfigurationException表示解析或处理Shiro配置时出现问题的根异常。
CryptoException在加密操作期间遇到的基本Shiro异常。
DataAccessException代表在尝试访问数据时的通用异常。
EnvironmentException针对与环境实例或配置相关的错误抛出异常。
ExecutionException包含在Subject执行Callable时抛出的任何异常。
InstantiationException当无法通过反射实例化类时,框架抛出的运行时异常。
InvalidPermissionStringException当正在解析的String对该解析程序无效时。
SerializationException对数据进行序列化或反序列化问题的根异常。
SessionException在会话期间与系统交互期间的一般安全异常。
UnavailableSecurityManagerException尝试获取应用程序的SecurityManager实例时抛出异常。
UnknownClassException相当于JDK的ClassNotFoundException。

常用异常

只列举AuthenticationException下的异常

父类简述描述
AccountExceptionConcurrentAccessException超出并发登录人数配置了并发登录人数后,接收到已验证或登录的帐户的身份验证尝试时抛出异常。
AccountExceptionDisabledAccountException账号已被禁用尝试进行身份验证时抛出,并且由于某种原因已禁用相应的帐户。
AccountExceptionExcessiveAttemptsException身份验证错误次数过多在一定时间内尝试身份验证超过一定次数,且无法通过验证时抛出的异常
AccountExceptionUnknownAccountException未知账号异常尝试使用系统中不存在的主体进行身份验证时抛出
CredentialsExceptionExpiredCredentialsException过期的凭证异常当系统确定提交的凭据已过期且不允许登录时,在身份验证过程中抛出。
CredentialsExceptionIncorrectCredentialsException错误的凭证异常尝试使用与帐户主体关联的实际凭据不匹配的凭据进行身份验证时抛出此异常。

示例

读取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.MessageDigestisEqual方法比较两个值是否相等,否则使用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.Collectioncontains方法进行比较。

权限检查有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方法中,最终是通过我们传入的Permissionimplies实现比较的,在1.4.0版的shiro中只有在WildcardPermission(通配符许可)类里面有一个实现方法。

JdbcRealm

JdbcRealm内部查询语句对表名和列名的定义如下:

userspassword, password_salt, username
user_rolesrole_name, username

其内部的成员变量authenticationQueryuserRolesQuerypermissionsQuery都定义了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,其只会影响到密码查询,当值为COLUMNauthenticationQuery的值未被更改时,它会查询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和比对方法impliessetParts方法源码如下:

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;
}

【待续】且无限延期

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值