Shio-核心

1、认证

认证(Authentication):身份验证的过程–也就是证明一个用户的真实身份。为了证明用户的身份,需要提供系统可以理解和相信的身份信息【principals】和证据【credentials】。

  • Principals(身份)是Subject的“标识属性”,可以是任何与Subject相关的标识,通常用用户名或者邮件地址来作为标识。
  • Credentials(证明):通常是只有Subject直到的机密内容,用来证明他们真正拥有所需的身份,一般有密码,指纹,X.509证书等。

最常见的身份/证明是用户名和密码,用户名是所需的身份说明,密码是证明身份的证据,如果一个提交的密码和系统要求的一致,程序才认为该用户身份正确。

验证Subjects

Subject验证的过程可以分为下面三步:

  1. 收集Subject提交的身份和证明;
  2. 向Authentication提交身份和证明;
  3. 如果提交的内容正确,允许访问,否则重新尝试验证或阻止访问;

第一步:收集用户身份和证明

UssernamePasswordToken token = new UsernamePasswordToken(username,password);

Shiro并不关心我们从哪里获得username和password,我们可以根据自己的需求来构造和引用AuthenticationToken实例。

第二步:提交身份和证明

当身份和证明被收集并实例化为一个AuthenticationToken(认证令牌)后,我们需要向Shiro提交令牌以执行真正的验证尝试:

Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);

在获取当前执行的Subject后,我们执行一个单独的login命令,将之前创建的AuthenticationToken实例传给它。
调用login方法就是为了对用户的身份进行校验。

第三步:处理成功或失败

当login方法没有返回信息时说明验证通过,程序可以继续运行,此时执行前面代码里的currentUser.isAuthenticated()将返回true。
如果校验失败,则Shiro将会捕捉异常,程序员根据异常信息就可以判断为何校验失败。
如果原有的异常不能满足我们的需求,可以自定义AuthenticationExceptions来表示特定的失败场景。

Remembered vs. Authenticated

Shiro支持在登录过程中执行“remember me”,记住,一个已记住的Subject(remembered Subject)和一个正常通过认证的Subject(authenticated Subject)在Shiro中是完全不同的。
已记住(Remembered)和已验证(Authenticated)是互斥的,已记住是说它的身份被先前的认证过程记住,并存在与先前的session中,已验证的Subject是成功验证后存在于当前session中。

Logging Out 退出登录

与验证相对的是释放所有已知的身份信息,当Subject与程序不再交互了,可以调用Subject.logout()来丢掉所有的身份信息。

currentUser.logout();//清除验证信息,使session失效

当调用logout,任何现存的session将变得不可用并且所有的身份信息将消失。
注意:因为在web程序中记住身份信息往往使用cookies,而cookies只能在Response提交时才能被删除,所以强烈要求在为最终用户调用subject.logout()之后立即将用户引到到一个新页面,确保任何与安全相关的cookies如期删除。

认证序列

现在我们看看当一个验证发生时,Shiro内部发生了什么?
第1步:程序代码调用Subject.login方法,向AuthenticationToken(认证令牌)实例的构造函数传递用户的身份和证明;
第2步:Subject实例,通常是一个DelegatingSubject(或其子类)通过调用SecurityManager.login(token)将这个令牌转交给程序的SecurityManager。
第3步:ecurityManager,基本的“安全伞”组件,得到令牌并通过调用 authenticator.authenticate(token)简单地将其转交给它内部的 Authenticator 实例,大部分情况下是一个 ModularRealmAuthenticator 实例,用来支持在验证过程中协调一个或多个Realm实例。
第4步:如果程序配置了多个Realm,ModularRealmAuthenticator实例将使用其配置的AuthenticationStrategy开始一个或多个Realm身份验证的尝试。在Realm被验证调用的整个过程中,AuthenticationStrategy被调用用来回应每个Realm的结果。
注意:如果只有一个Realm被配置,则不需要AuthenticationStrategy。
第5步:每一个配置的Realm都被检验看其是否支持提交的AuthenticationToken,如果支持,则该Realm的getAuthenticationInfo方法随着提交的令牌被调用,getAuthenticationInfo方法为特定的Realm提供一次有效的独立的验证尝试。

Authenticator

如果希望用自定义的Authenticator实现配置SecurityManager,可以在shiro.ini中做这件事:

[main]
...
authenticator = com.foo.bar.CustomAuthenticator

securityManager.authenticator = $authenticator

不过一般在实际操作中,我们还是使用ModularRealmAuthenticator实例。

AuthenticationStrategy

当一个程序中定义了两个或多个realm时,ModularRealmAuthenticator使用一个内部的AuthenticationStrategy组件来决定一个验证是否成功。
AuthenticationStrategy 还有责任从每一个成功的 Realm 中收集结果并将它们“绑定”到一个单独的 AuthenticationInfo,这个AuthenticationInfo 实例是被 Authenticator 实例返回的,并且 shiro 用它来展现一个 Subject 的最终身份(也就是 Principals )。

如果程序中使用大于一个Realm从多个数据源中获取账户数据,程序可看到的是AuthenticationStrategy最终负责Subject身份最终“合并(merged)”的视图。
Shiro有3个具体的AuthenticationStrategy实现:
在这里插入图片描述
ModularRealmAuthenticator 默认使用 AtLeastOneSuccessfulStrategy 实现,这也是最常用的策略,然而你也可以配置你希望的不同的策略。
shiro.ini

[main]

authcStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy

securityManager.authenticator.authenticationStrategy = $authcStrategy

自定义的 AuthenticationStrategy

如果你希望创建你自己的 AuthenticationStrategy 实现,你可以使用 org.apache.shiro.authc.pam.AbstractAuthenticationStrategy 作为起始点。AbstractAuthenticationStrategy 类自动实现 ‘绑定(bundling)’/聚集(aggregation)行为将来自于每个Realm 的结果收集到一个 AuthenticationInfo 实例中。

Realm 验证的顺序

Realm 交互的 ModularRealmAuthenticator 按迭代(iteration)顺序执行。

ModularRealmAuthenticator 可以访问为 SecurityManager 配置的 Realm 实例,当尝试一次验证时,它将在集合中遍历,支持对提交的 AuthenticationToken 处理的每个 Realm 都将执行 Realm 的 getAuthenticationInfo 方法。

我们也可以通过shiro.ini配置文件来按照我们自己期望的顺序来配置Realm,Realm将按照他们在INI文件中定义的顺序执行。

blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm

效果等价于:

securityManager.realms = $blahRealm, $fooRealm, $barRealm

如果你希望明确定义 realm 执行的顺序,不管他们如何被定义,你可以设置 SecurityManager 的 realms 属性,例如,使用上面定义的 realm,但你希望 blahRealm 最后执行而不是第一个:

blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm

securityManager.realms = $fooRealm, $barRealm, $blahRealm

当你明确的配置 securityManager.realms 属性时,只有被引用的 realm 将为 SecurityManager 配置,也就是说你可能在 INI 中定义了5个 realm,但实际上只使用了3个,如果在 realm 属性中只引用了3个,这和隐含的 realm 顺序不同,在那种情况下,所有有效的 realm 都会用到。

2、授权

授权(Authorization):亦为访问控制,是管理资源访问的过程,换言之,也就是控制在一个程序中“谁”有权利访问“什么”。

授权要素

授权有三个核心元素,即:权限(permissions)、角色(roles)和用户(users)

权限(permissions)

权限是一组关于行为的基本指令,以明确标识在一个程序中可以做什么,一个很好的权限指令定义必须描述资源以及当一个Subject与这些资源交互时什么动作可以执行。

下面是一些权限指令的例子:

  • 打开一个文件;
  • 查看“/user/list”页面;
  • 打印文档;
  • 删除“JSmith”用户

权限只描述行为(和资源相关的动作),并不关心“谁”有能力执行这个动作。

定义“谁”(用户)被允许做“什么”(权限)需要用一些方法将权限赋予用户,这通常取决于程序的数据模型而且经常在程序中发生改变。

上面提到的权限示例都是针对资源(门、文件、客户等)指定的动作(打开、读、删除等),在一些场景中,我们也会指定非常细粒度的“实例级别”行为–例如:“删除”(delete)名为“Jsmith”(实例标识)的“用户”(资源类型)。

角色(roles)

角色是一个实体名,代表一组行为或职责,这些行为在程序中转化为你可以或不能做的事情。角色通常赋给用户账户,关联后,用户既可以“做”属于不同角色的事情。

有两种有效的角色指定方式:

  • 权限隐含于角色中;
    隐含的角色可能会增加软件的维护成本和管理问题,比如增加或删除一个角色,重新定义角色的行为等,代码改动太大。
  • 明确为角色指定权限;
    明确为角色指定权限本质上是一组权限指令的名称集,程序(以及 Shiro)准确知道一个特定的角色是什么意思,因为它确切知道某行为是否可以执行,而不用去猜测特定的角色可以或不可以做什么。

用户(users)

一个用户本质上是程序中的“谁”,前面提到的Subject实际上是shiro的“用户”。
用户(Subjects)通过与角色或权限关联确定是否被允许执行程序内特定的动作,程序数据模型确切定义了 Subject 是否允许做什么事情。

例如,在你的数据模型中,你定义了一个普通的用户类并且直接为其设置了权限,或者你只是直接给角色设置了权限,然后将用户与该角色关联,通过这种关联,用户就“有”了角色所具备的权限,或者你也可以通过“组”的概念完成这件事,这取决于你程序的设计。

数据模型定义了如何进行授权,Shiro 依赖一个 Realm 实现将你的数据模型关联转换成 Shiro 可以理解的内容,我们将稍后讨论 Realms。

最终,是 Realm 与你的数据源(RDBMS,LDAP等)做交流,Realm 用来告知Shiro 是否角色或权限是否存在,你可以完全控制你的授权模型如何创建和定义。

授权对象

在 Shiro 中执行授权可以有三种途径:

  • 程序代码–你可以在你的 JAVA 代码中执行用类似于 if 和 else 的结构来执行权限检查。
  • JDK 注解–你可以在你的 JAVA 方法上附加权限注解
  • JSP/GSP 标签–你可以基于角色和权限控制 JSP 或 GSP 页面输出内容。

在程序中检查授权

基于角色的授权

如果你想简单地检查一下当前Subject是否拥有一个角色,你可以在一个实例上调用 hasRole* 方法。

例如,查看一个 Subject 是否有特定(单独)的角色,你可以调用subject.hasRole(roleName))方法,做出相应的反馈。

Subject currentUser = SecurityUtils.getSubject();

if (currentUser.hasRole("administrator")) {
    //显示 admin 按钮
} else {
    //不显示按钮?  灰色吗?
}

在这里插入图片描述
另外可以检测 Subjet 是否是指定的某个角色,你可以在的代码执行之前简单判断他们是否是所要求的角色,如果 Subject 不是所要求的角色, AuthorizationException 异常将被抛出,如果是所要求的角色,判断将安静地执行并按期望顺序执行下面的逻辑。

Subject currentUser = SecurityUtils.getSubject();

//保证当前用户是一个银行出纳员
//因此允许开立帐户:
currentUser.checkRole("bankTeller");
openBankAccount();

在这里插入图片描述

基于权限的授权

通过基于权限的授权执行访问控制是更好的方法。基于权限的授权,因为其与程序功能(以及程序核心资源上的行为)紧密联系,基于权限授权的源代码在程序功能改变时才需要改变,而与安全策略无关。这意味着与同样基于角色的授权相比,对代码的影响更少。

基于对象的权限检查

Permission printPermission = new PrinterPermission("laserjet4400n", "print");

Subject currentUser = SecurityUtils.getSubject();

if (currentUser.isPermitted(printPermission)) {
    //显示 打印 按钮
} else {
    //不显示按钮?  灰色吗?
}

在这里插入图片描述
基于字符串的权限检查
选择用普通的字符串来代表权限:

Subject currentUser = SecurityUtils.getSubject();

if (currentUser.isPermitted("printer:print:laserjet4400n")) {
    //显示 打印 按钮
} else {
    //不显示按钮?  灰色吗?
}

基于字符串的权限有利的一面在于你不需要实现一个接口而且简单的字符串也非常易读,而不利的一面在于不保证类型安全,而且当你需要定义超出字符串表现能力之外的更复杂的行为时,你仍旧需要利用权限接口实现你自己的权限对象。实际上,大部分 Shiro 的终端用户因为其简单而选择基于字符串的方式,但最终你的程序需求决定了哪一种方法会更好。
在这里插入图片描述

权限判断

另一种检查 Subject 是否被允许做某件事的方法是,在逻辑执行之前简单判断他们是否具备所需的权限,如果不允许,AuthorizationException异常被抛出,如果是允许的,判断将安静地执行并按期望顺序执行下面的逻辑。

例如:

Subject currentUser = SecurityUtils.getSubject();

//担保允许当前用户
//开一个银行帐户:
Permission p = new AccountPermission("open");
currentUser.checkPermission(p);
openBankAccount();

或者,同样的判断,可以用字符串形式:

Subject currentUser = SecurityUtils.getSubject();

//担保允许当前用户
//开一个银行帐户:
currentUser.checkPermission("account:open");
openBankAccount();

与 isPermitted* 方法相比较,这种方法的优势是代码更为清晰,如果当前Subject 不符合条件,你不必创建你自己的 AuthorizationExceptions 异常(如果你不想那么做)。
在这里插入图片描述

基于注解的授权

配置

在你使用 JAVA 的注解之前,你需要在程序中启动 AOP 支持,因为有许多AOP 框架,所以很不幸,在这里并没有标准的在程序中启用 AOP 的方法。

RequiresAuthentication 注解

RequiresAuthentication 注解表示在访问或调用被注解的类/实例/方法时,要求 Subject 在当前的 session中已经被验证。

举例:

@RequiresAuthentication
public void updateAccount(Account userAccount) {
    //这个方法只会被调用在
    //Subject 保证被认证的情况下
    ...
}

这基本上与下面的基于对象的逻辑效果相同:

public void updateAccount(Account userAccount) {
    if (!SecurityUtils.getSubject().isAuthenticated()) {
        throw new AuthorizationException(...);
    }

    //这里 Subject 保证被认证的情况下
    ...
}
RequiresGuest 注解

RequiresGuest 注解表示要求当前Subject是一个“guest(访客)”,也就是,在访问或调用被注解的类/实例/方法时,他们没有被认证或者在被前一个Session 记住。

例如:

@RequiresGuest
public void signUp(User newUser) {
    //这个方法只会被调用在
    //Subject 未知/匿名的情况下
    ...
}

这基本上与下面的基于对象的逻辑效果相同:

public void signUp(User newUser) {
    Subject currentUser = SecurityUtils.getSubject();
    PrincipalCollection principals = currentUser.getPrincipals();
    if (principals != null && !principals.isEmpty()) {
        //已知的身份 - 不是 guest(访客):
        throw new AuthorizationException(...);
    }

    //在这里 Subject 确保是一个 'guest(访客)'
    ...
}
RequiresPermissions 注解

RequiresPermissions 注解表示要求当前Subject在执行被注解的方法时具备一个或多个对应的权限。

例如:

@RequiresPermissions("account:create")
public void createAccount(Account account) {
    //这个方法只会被调用在
    //Subject 允许创建一个 account 的情况下
    ...
}

这基本上与下面的基于对象的逻辑效果相同

public void createAccount(Account account) {
    Subject currentUser = SecurityUtils.getSubject();
    if (!subject.isPermitted("account:create")) {
        throw new AuthorizationException(...);
    }

    //在这里 Subject 确保是允许
    ...
}
RequiresRoles 注解

RequiresRoles 注解表示要求当前Subject在执行被注解的方法时具备所有的角色,否则将抛出 AuthorizationException 异常。

例如:

@RequiresRoles("administrator")
public void deleteUser(User user) {
    //这个方法只会被 administrator 调用 
    ...
}

这基本上与下面的基于对象的逻辑效果相同

public void deleteUser(User user) {
    Subject currentUser = SecurityUtils.getSubject();
    if (!subject.hasRole("administrator")) {
        throw new AuthorizationException(...);
    }
//Subject 确保是一个 'administrator'
...

}

RequiresUser 注解

RequiresUser 注解表示要求在访问或调用被注解的类/实例/方法时,当前 Subject 是一个程序用户,“程序用户”是一个已知身份的 Subject,或者在当前 Session 中被验证过或者在以前的 Session 中被记住过。

例如:

@RequiresUser
public void updateAccount(Account account) {
    //这个方法只会被 'user' 调用 
    //i.e. Subject 是一个已知的身份with a known identity
    ...
}

这基本上与下面的基于对象的逻辑效果相同

public void updateAccount(Account account) {
    Subject currentUser = SecurityUtils.getSubject();
    PrincipalCollection principals = currentUser.getPrincipals();
    if (principals == null || principals.isEmpty()) {
        //无身份 - 他们是匿名的,不被允许
        throw new AuthorizationException(...);
    }

    //Subject 确保是一个已知的身份
    ...
}

授权序列

当一个授权命令调用时 Shiro 内部发生了什么事情?
第1步:程序或框架代码调用一个 Subject 的hasRole*、checkRole*、 isPermitted或者 checkPermission方法,传递所需的权限或角色。

第2步:Subject实例,通常是一个 DelegatingSubject(或子类),通过调用securityManager 与各 hasRole*、checkRole*、 isPermitted* 或 checkPermission* 基本一致的方法将权限或角色传递给程序的 SecurityManager(实现了 org.apache.shiro.authz.Authorizer 接口)

第3步:SecurityManager 作为一个基本的“保护伞”组件,接替/代表其内部 org.apache.shiro.authz.Authorizer 实例通过调用 authorizer 的各自的 hasRole*, checkRole* , isPermitted* ,或 checkPermission* 方法。 authorizer 默认情况下是一个实例 ModularRealmAuthorizer 支持协调一个或多个实例 Realm 在任何授权操作实例。

第4步:,检查每一个被配置的 Realm 是否实现相同的 Authorizer接口,如果是,Realm 自己的各 hasRole*、checkRole*、 isPermitted* 或 checkPermission* 方法被调用。

ModularRealmAuthorizer

前面提到过,Shiro SecurityManager 默认使用 ModularRealmAuthorizer 实例,ModularRealmAuthorizer 实例同等支持用一个 Realm 的程序和用多个 Realm 的程序。

对于任何授权操作,ModularRealmAuthorizer 将在其内部的 Realm 集中迭代(iterator),按迭代(iteration)顺序同每一个 Realm 交互,与每一个 Realm 交互的方法如下:

1.如果Realm实现了 Authorizer 接口,调用它各自的授权方法(hasRole*、 checkRole*、isPermitted或 checkPermission)。

1.1.如果 Realm 函数的结果是一个 exception,该 exception 衍生自一个 Subject 调用者的 AuthorizationException,就切断授权过程,剩余的授权 Realm 将不在执行。

1.2.如果 Realm 的方法是一个 hasRole* 或 isPermitted*,并且返回真,则真值立即被返回而且剩余的 Realm 被短路,这种做法作为一种性能增强,在一个 Realm 判断允许后,隐含认为这个 Subject 被允许。它支持最安全的安全策略:默认情况下所有都被禁止,明确指定允许的事情。

2.如果 Realm 没有实现 Authorizer 接口,将被忽略。

授权顺序

需要指出非常重要的一点,就如同验证(authentication)一样,ModularRealmAuthorizer 按迭代(iteration)顺序与 Realm 交互。

ModularRealmAuthorizer 拥有 SecurityManager 配置的 Realm 实例的入口,当执行一个授权操作时,它将在整个集合中进行迭代(iteration),对于每一个实现 Authorizer 接口的 Realm,调用Realm 各自的 Authorizer 方法(如 hasRole、 checkRole、 isPermitted或 checkPermission)。

配置全局的 PermissionResolver

当执行一个基于字符串的权限检查时,大部分 Shiro 默认的 Realm 将会在执行权限隐含逻辑之前首先把这个字符串转换成一个常用的权限实例。

这是因为权限被认为是基于隐含逻辑而不是相等检查(查看Permission章节了解更多隐含与相等的对比)。隐含逻辑用代码表示要比通过字符串对比好,因此,大部分 Realm需要转换一个提交的权限字符串为对应的权限实例。

为了这个转换目的,Shiro 支持 PermissionResolver,大部分 Shiro Realm 使用 PermissionResolver 来支持它们对Authorizer 接口中基于字符串权限方法的实现:当这些方法在Realm上被调用时,将使用PermissionResolver 将字符串转换为权限实例,并执行检查。

所有的 Shiro Realm 默认使用内部的 WildcardPermissionResolver,它使用 Shiro 的WildcardPermission字符串格式。

如果你想创建你自己的 PermissionResolver 实现,比如说你想创建你自己的权限字符串语法,希望所有配置的Realm实例都支持这个语法,你可以把自己的 PermissionResolver 设置成全局,供所有 realm 使用。

如,在shiro.ini中:

globalPermissionResolver = com.foo.bar.authz.MyPermissionResolver
...
securityManager.authorizer.permissionResolver = $globalPermissionResolver
...
PermissionResolverAware

如果你想配置一个全局的 PermissionResolver,每一个会读取这个PermissionResolver 配置的 Realm 必须实现PermissionResolverAware 接口,这确保被配置 PermissionResolver 的实例可以传递给支持这种配置的每一个 Realm。

如果你不想使用一个全局的 PermissionResolver 或者你不想被PermissionResolverAware 接口麻烦,你可以明确地为单个的 Realm 配置 PermissionResolver 接口(可看作是JavaBean的setPermissionResolver 方法):

permissionResolver = com.foo.bar.authz.MyPermissionResolver

realm = com.foo.bar.realm.MyCustomRealm
realm.permissionResolver = $permissionResolver
...
配置全局的RolePermissionResolver

与 PermissionResolver 类似,RolePermissionResolver 有能力表示执行权限检查的 Realm 所需的权限实例。

最主要的不同在于接收的字符串是一个角色名,而不是一个权限字符串。

RolePermissionResolver 被 Realm 在需要时用来转换一个角色名到一组明确的权限实例。

这是非常有用的,它支持那些遗留的或者不灵活的没有权限概念的数据源。

例如,许多 LDAP 目录存储角色名称(或组名)但不支持角色名和权限的联合,因为它没有权限的概念。一个使用 shiro 的程序可以使用存储于 LDAP 的角色名,但需要实现一个 RolePermissionResolver 来转换 LDAP 名到一组确切的权限中以执行明确的访问控制,权限的联合将被存储于其它的数据存储中,比如说本地数据库。

因为这种将角色名转换为权限的概念是特定的,Shiro 默认的 Realm 没有使用它们。

然而,如果你想创建你自己的 RolePermissionResolver 并且希望用它配置多个 Realm 实现,你可以将你的 RolePermissionResolver设置成全局。
shiro.ini

globalRolePermissionResolver = com.foo.bar.authz.MyPermissionResolver
...
securityManager.authorizer.rolePermissionResolver = $globalRolePermissionResolver
...
RolePermissionResolverAware

如果你想配置一个全局的 RolePermissionResolver, 每个 Realm 接收必须实现了 RolePermisionResolverAware 接口的配置了的 RolePermissionResolver 。这保证了配置全局 RolePermissionResolver 实例可以传递到各个支持这样配置的 Realm 。

如果你不想使用全局的 RolePermissionResolver 或者你不想麻烦实现 RolePermissionResolverAware 接口,你可以单独为一个 Realm 配置 RolePermissionResolver(可以看作 JavaBean 的 setRolePermissionResolver 方法)。

rolePermissionResolver = com.foo.bar.authz.MyRolePermissionResolver

realm = com.foo.bar.realm.MyCustomRealm
realm.rolePermissionResolver = $rolePermissionResolver
定制Authorizer

如果你的程序使用多于一个 Realm 来执行授权而 ModularRealmAuthorizer 默认的简单迭代(iteration)、短路授权的行为不能满足你的需求,你可以创建自己的 Authorizer 并配置给相应的 SecurityManager。

例如,在shiro.ini中:

[main]
...
authorizer = com.foo.bar.authz.CustomAuthorizer

securityManager.authorizer = $authorizer

3、Realms

Realm 是可以访问程序特定的安全数据如用户、角色、权限等的一个组件。Realm 会将这些程序特定的安全数据转换成一种 Shiro 可以理解的形式。

Realm通常和数据源是一对一的对应关系,如关系数据库,LDAP 目录,文件系统,或其他类似资源。因此,Realm 接口的实现使用数据源特定的API 来展示授权数据(角色,权限等),如JDBC,文件IO,Hibernate 或JPA,或其他数据访问API。

Realm实质上就是一个特定安全的DAO

因为这些数据源大多数通常存储身份验证数据(如密码的凭证)以及授权数据(如角色或权限),每个Shiro Realm能够执行身份验证和授权操作。

配置

如果使用 Shiro 的 ini 配置文件,你可以在[main]区域内像配置其它对象一样定义和引用Realms,但是 Realm 在 secrityManager上的配置有两种方式:明确方式和隐含方式。

显式配置

在定义一个或多个Realm后,再将它们在securityManager上进行统一配置。
在这里插入图片描述

隐式配置(不推荐)

这种方法可能引发意想不到的行为,如果你改变 realm 定义的顺序的话。建议你避免使用此方法,并使用显式分配,它拥有确定的行为。该功能很可能在未来的 Shiro 版本中被废弃或移除。

认证

了解在一个授权尝试中当 Authenticator 与 Realm 交互时到底发生了什么是很重要的。

Supporting AuthenticationTokens

正如在认证流程中提到的,在一个 Realm 执行一个验证尝试之前,它的supports)方法被调用。只有在返回值为 true 的时候它的getAuthenticationInfo(token) 方法才会执行。

通常情况下,一个 realm 将检查提交的令牌类型(接口或类)确定自己是否可以处理它,例如,一个处理生物特性数据的Realm 可能一点也不理解 UsernamePasswordTokens,在这种情况下它将从支持函数中返回 false。

Handling supported AuthenticationTokens

如果一个Realm支持提交的验证令牌,验证将调用 Realm 的getAuthenticationInfo(token)) 方法,这是Realm 使用后台数据进行验证的一次有效尝试,顺序执行以下动作:

1.检查主要 principal (身份)令牌(用户身份信息);

2.基于主要 principal (信息),在数据源中查找对应的用户数据;

3.确定令牌支持的 credentials (凭证数据)和存储的数据相符;

4.如果凭证相符,返回一个AuthenticationInfo实例,里面封装了 Shiro 可以理解的用户数据。

5.如果证据不符,抛出 AuthenticationException异常。

这是所有Realm getAuthenticationInfo 实现的最高级别工作流,Realm 在这个过程中可以自由做自己想做的事情,比如记录日志,修改数据,以及其他,只要对于存储的数据和验证尝试来讲是合理的就行。

仅有一件事情是必须的,如果 credentials (凭证)和给定的 principal (主要信息)匹配,需要返回一个非空的 AuthenticationInfo 实例,用来表示来自数据源的 Subject 账户信息。

节约时间

直接实现 Realm 接口也许需要时间并容易出错,大部分用户选择继承 AuthorizingRealm 虚拟类,这个类实现了常用的认证和授权工作流,这会节省你的时间而且不易出错。

凭证匹配

在上述 realm 认证工作流中,一个 Realm 必须较验 Subject 提交的凭证(如密码)是否与存储在数据中的凭证相匹配,如果匹配,验证成功,系统保留已认证的终端用户身份。

检查提交的凭证是否与后台存储数据相匹配是每一个 Realm 的责任而不是 Authenticator 的责任,每一个 Realm 都具备与凭证形式及存储密切相关的技能,可以执行详细的凭证比对,而 Authenticator 只是一个普通的工作流组件。

凭证匹配的过程在所有程序中基本上是一样的,通常只是对比数据方式不同。要确保这个过程在必要时是可插拔和可定制的,AuthenticatingRealm 以及它的子类支持用 CredentialsMatcher 来执行一个凭证对比。

在找到用户数据之后,它和提交的 AuthenticationToken 一起传递给一个 CredentialsMatcher ,后者用来检查提交的数据和存储的数据是否相匹配。

Shiro某些 CredentialsMatcher 实现可以使你开箱即用,比如 SimpleCredentialsMatcher 和 HashedCredentialsMatcher 实现,但如果你想配置一个自定义的实现来完成特定的对比逻辑,你可以这样做:

Realm myRealm = new com.company.shiro.realm.MyRealm();
CredentialsMatcher customMatcher = new com.company.shiro.realm.CustomCredentialsMatcher();
myRealm.setCredentialsMatcher(customMatcher);

或者,使用 Shiro 的 INI配置文件

[main]
...
customMatcher = com.company.shiro.realm.CustomCredentialsMatcher
myRealm = com.company.shiro.realm.MyRealm
myRealm.credentialsMatcher = $customMatcher
...

4、Session Management

Apache Shiro 提供安全框架界独一无二的东西:一个完整的企业级Session 解决方案,从最简单的命令行及智能手机应用到最大的集群企业Web 应用程序。

使用Sessions

几乎与所有其他在Shiro 中的东西一样,你通过与当前执行的Subject 交互来获取Session:

Subject currentUser = SecurityUtils.getSubject();

Session session = currentUser.getSession();
session.setAttribute( "someKey", someValue);

subject.getSession() 方法是调用 currentUser.getSubject(true)的快捷方式。

对于那些熟悉 HttpServletRequest API 的,Subject.getSession(boolean create) 方法与 HttpServletRequest.getSession(boolean create) 方法有着异曲同工之效。

  • 如果该Subject 已经拥有一个Session,则boolean 参数被忽略且Session 被立即返回。
  • 如果该Subject 还没有一个Session 且create 参数为true,则创建一个新的会话并返回该会话。
  • 如果该Subject 还没有一个Session 且create 参数为false,则不会创建新的会话且返回null。

getSession 要求能够在任何应用程序工作,甚至是非 Web 应用程序。

当开发框架代码来确保一个 Session 没有被创建是没有必要的时候,subject.getSession(false) 可以起到很好的作用。 当你获取了一个 Subject 的 Session 后,你可以用它来做许多事情,像设置或取得 attribute,设置其超时时间,以及 更多。

SessionManager

SessionManager,名如其意,在应用程序中为所有的 subject 管理Session —— 创建,删除,inactivity(失效)及验证,等等。如同其他在Shiro 中的核心结构组件一样,SessionManager 也是一个由 SecurityManager 维护的顶级组件。
默认的 SecurityManger 实现是默认使用开箱即用的DefaultSessionManager。

像其他被 SecurityManager 管理的组件一样,SessionManager 可以通过 JavaBean 风格的 getter/setter 方法在所有Shiro 默认 SecurityManager 实现(getSessionManager()/setSessionManager())上获取或设置值。或者例如,如果在使用 shiro.ini 配置:

[main]
...
sessionManager = com.foo.my.SessionManagerImplementation
securityManager.sessionManager = $sessionManager

但从头开始创建一个 SessionManager 是一个复杂的任务且是大多数人不想亲自做的事情。Shiro 的开箱即用的SessionManager 实现是高度可定制的和可配置的,并满足大多数的需要。本文档的其余部分假定你将使用 Shiro 的默认 SessionManager 实现,当覆盖配置选项时。但请注意,你基本上可以创建或插入任何你想要的东西。

Session超时

Shiro 的 SessionManager 实现默认是 30 分钟会话超时。也就是说,如果任何 Session 创建后闲置(未被使用,它的lastAccessedTime)未被更新)的时间超过了 30 分钟,那么该 Session 就被认为是过期的,且不允许再被使用。

你可以设置 SessionManager 默认实现的 globalSessionTimeout 属性来为所有的会话定义默认的超时时间。例如,如果你想超时时间是一个小时而不是 30 分钟:

[main]
...
# 3,600,000 milliseconds = 1 hour
securityManager.sessionManager.globalSessionTimeout = 3600000

Session监听器

Shiro 支持 SessionListener 概念来允许你对发生的重要会话作出反应。你可以实现 SessionListener 接口(或扩展易用的SessionListenerAdapter )并与相应的会话操作作出反应。 由于默认的 SessionManager sessionListeners 属性是一个集合,你可以对 SessionManager 配置一个或多个 listener 实 现,就像其他在 shiro.ini 中的集合一样:

[main]
...
aSessionListener = com.foo.my.SessionListener
anotherSessionListener = com.foo.my.OtherSessionListener

securityManager.sessionManager.sessionListeners = $aSessionListener, $anotherSessionListener, etc.

当任何会话发生事件时,SessionListeners 都会被通知——不仅仅是对一个特定的会话

Session存储

每当一个会话被创建或更新时,它的数据需要持久化到一个存储位置以便它能够被稍后的应用程序访问。同样地,当一个会话失效且不再被使用时,它需要从存储中删除以便会话数据存储空间不会被耗尽。SessionManager 实现委托这些 Create/Read/Update/Delete(CRUD) 操作为内部组件,同时,SessionDAO,反映了数据访问对象(DAO)设计模式。

SessionDAO 的权力是你能够实现该接口来与你想要的任何数据存储进行通信。这意味着你的会话数据可以驻留在内存中,文件系统,关系数据库或NoSQL 的数据存储,或其他任何你需要的位置。你得控制持久性行为。

你可以将任何 SessionDAO 实现作为一个属性配置在默认的SessionManager 实例上。例如,在shiro.ini 中:

[main]
...
sessionDAO = com.foo.my.SessionDAO
securityManager.sessionManager.sessionDAO = $sessionDAO

注意:上述的 securityManager.sessionManager.sessionDAO = $sessionDAO 作业仅在使用一个本地的 Shiro 会话管理器时才 工作。Web 应用程序默认不会使用本地的会话管理器,而是保持不支持SessionDAO 的 Servlet Container 的默认会话 管理器。如果你想基于 Web 应用程序启用 SessionDAO 来自定义会话存储或会话群集,你将不得不首先配置一个本 地的Web 会话管理器。例如:

[main]
...
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
securityManager.sessionManager = $sessionManager

# Configure a SessionDAO and then set it:
securityManager.sessionManager.sessionDAO = $sessionDAO

Shiro 的默认配置本地 SessionManagers 使用仅内存 Session 存储。这是不适合大多数应用程序的。大多数生产应用程序想要配置提供的 EHCache(见下文)支持或提供自己的SessionDAO 实现。

EHCache SessionDAO

EHCache 默认是没有启用的,但如果你不打算实现你自己的 SessionDAO,那么强烈地建议你为 Shiro 的 SessionManagerment 启用 EHCache 支持。EHCache SessionDAO 将会在内存中保存会话,并支持溢出到磁盘,若内存成为制约。这对生产程序确保你在运行时不会随机地“丢失”会话是非常好的。

如果你急需独立的容器会话集群,EHCache 会是一个不错的选择。你可以显式地在 EHCache 之后插入TerraCotta,并拥有一个独立于容器集群的会话缓存。不必再担心 Tomcat,JBoss,Jetty,WebSphere 或WebLogic 特定的会话集群!

为会话启用 EHCache 是非常容易的。首先,确保在你的 classpath 中有shiro-ehcache-.jar 文件;

当在 classpath 中后,这第一个 shiro.ini 实例向你演示怎样为所有Shiro 的缓存需要(不只是会话支持)使用 EHCache:

[main]

sessionDAO = org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
securityManager.sessionManager.sessionDAO = $sessionDAO

cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager
securityManager.cacheManager = $cacheManager

最后一行,securityManager.cacheManager = $cacheManager,为所有 Shiro 的需要配置了一个 CacheManager。该CacheManager 实例会自动地直接传送到 SessionDAO(通过 EnterpriseCacheSessionDAO 实现 CacheManagerAware 接口的性质)。

然后,当 SessionManager 要求 EnterpriseCacheSessionDAO 去持久化一个 Session 时,它使用一个 EHCache 支持的 Cache 实现去存储Session 数据。

注意:Web 应用程序默认使用基于容器的 SessionManager,它不支持 SessionDAO。如果你想在 Web 应用程序中使用基于 EHCache 的会话存储,配置一个 如上所述的 Web SessionManager。

EHCache Session Cache Configuration

默认地,EhCacheManager 使用一个 Shiro 特定的 ehcache.xml 文件来建立 Session 缓存区以及确保 Sessions 正常存取的必要设置。

然而,如果你想改变缓存设置,或想配置你自己的 ehcache.xml 或EHCache net.sf.ehcache.CacheManager 实例,你需要配置缓存区来确保Sessions 被正确地处理。

如果你查看默认的 ehcache.xml 文件,你会看到接下来的 shiro-activeSessionCache 缓存配置:

<cache name="shiro-activeSessionCache"
       maxElementsInMemory="10000"
       overflowToDisk="true"
       eternal="true"
       timeToLiveSeconds="0"
       timeToIdleSeconds="0"
       diskPersistent="true"
       diskExpiryThreadIntervalSeconds="600"/>

如果你希望使用你自己的 ehcache.xml 文件,那么请确保你已经为 Shiro 所需的定义了一个类似的缓存项。
很有可能 你会改变 maxElementsInMemory 的属性值来吻合你的需要。然而,至少下面两个存在于你自己配置中的属性是非常重要的:

  • overflowToDisk=“true” - 这确保当你溢出进程内存时,会话不丢失且能够被序列化到磁盘上。
  • eternal=“true” - 确保缓存项( Session 实例)永不过期或被缓存自动清除。这是很有必要的,因为 Shiro 基于计划过程完成自己的验证。如果我们关掉这项,缓存将会在 Shiro 不知道的情况下清扫这些 Sessions,这可能引起麻烦
EHCache Session Cache Name

默认地,EnterpriseCacheSessionDAO 向 CacheManager 寻求一个名为"shiro-activeSessionCache"的 Cache。该缓存的 name/region 将在 ehcache.xml 中配置,如上所述。

如果你想使用一个不同的名字而不是默认的,你可以在EnterpriseCacheSessionDAO 上配置名字,例如:

[main]
...
sessionDAO = org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
sessionDAO.activeSessionsCacheName = myname
...

只要确保在 ehcahe.xml 中有一项与名字匹配且你已经配置好了如上所述的 overflowToDisk=“true” 和 eternal=“true”。

会话集群

Apache Shiro 会话能力一个非常令人兴奋的事情是,你可以原生的集群 Subject 会话,不需要再担心你的容器环境。也就是说,如果您使用 Shiro 的原生会话并配置一个会话集群,可以,说,部署到 Jetty 和 Tomcat 开发环境,JBoss 或 Geronimo 的生产环境,或任何其他环境,不用担心容器/特定于环境的集群安装或配置。 Shiro 会话集群配置一次,无论您的部署环境如何,都能正常运行

因为 Shiro 的基于 pojo 的 n 层体系结构,使会话集群的集群机制非常简单,使会话持久性的水平。 也就是说,如果您配置集群 SessionDAO ,DAO 可以与集群交互机制, Shiro 的 SessionManager 不需要知道集群的问题。

Sessions和Subject状态

有状态

默认地,Shiro 的SecurityManager 实现使用一个Subject 的Session 作为一种策略来为接下来的引用存储Subject 的身份 ID(PrincipalCollection)和验证状态(subject.isAuthenticated())。这通常发生在一个Subject 登录后或当一个 Subject 的身份 ID 通过Remember 服务被发现后。

这个默认的方法有几个好处:

  • 任何服务于请求,调用或消息的应用程序可以用请求/调用/消息的有效载荷关联会话ID,且这是Shiro 用入站
    请求关联用户所有所必须的。例如,如果使用Subject.Builder,这是需要获取相关的Subject 所需的一切:
  Serializable sessionId = //get from the inbound request or remotemethod invocation payload Subject 
  requestSubject = new Subject.Builder().sessionId(sessionId).buildSubject();

这给大多数Web 应用程序及任何编写远程处理或消息框架的人带来了令人难以置信的方便(这事实上是Shiro 的Web 支持在自己的框架代码内关联Subject 和ServletRequest)。

  • 任何"RememberMe"身份基于一个能够在第一次访问就能持久化到会话的初始请求。这确保了Subject 被记住的身份可以跨请求保存而不需要反序列化及将它解释到每个请求。例如,在一个 Web 应用程序中,没有必要去读取每一个请求的加密RememberMe Cookie,如果该身份在会话中是已知的。这可是一个很好的性能提升。

无状态

虽然上述的默认策略对于大多数应用程序而言是很好的(通常是可取的),但这对于尝试尽可能无状态的应用程序来说是不合适的。许多无状态的架构规定在请求中不能存在持久状态,这种情况下的 Sessions 不会被允许(一个会话其本质代表了持久状态)。

但这一要求带来一个便利的代价—— Subject 状态不能跨请求保留。这意味着有这一要求的应用程序必须确保 Subject 状态可以在每一个请求中以其他的方式代表。

这几乎总是通过验证每个由应用程序处理的请求/调用/消息来完成的。例如,大多数无状态 Web 应用程序通常支持这一点通过执行 HTTP 基本验证,允许浏览器验证每一个代表最终用户的请求。

一个混合的方法

如果你想使用混合的方法呢?如果某些对象应该有会话而某些没有?这种混合法方法能够给许多应用程序带来好处。例如:

  • 也许 human Subject(如 Web 浏览器用户)由于上面提供的好处能够使用Session。
  • 也许non-human Subject(如 API 客户端或第三方应用程序)不应该创建session 由于它们与软件的交互可能会间歇或不稳定。
  • 也许所有某种确定类型的 Subject 或从某一确定位置访问系统的应该将状态保持在会话中,但所有其他的不应该。如果你需要这个混合方法,你可以实现一个 SessionStorageEvaluator。
SessionStorageEvaluator

在你想究竟控制哪个 Subject 能够在它们的 Session 中保存它们的状态的情况下,你可以实现org.apache.shiro.mgt.SessionStorageEvaluator 接口,并告诉Shiro 哪个 Subject 支持会话存储。

该接口只有一个方法:

public interface SessionStorageEvaluator {

    public boolean isSessionStorageEnabled(Subject subject);

}

关于更详细的API 说明,请参见 SessionStorageEvaluator 的JavaDoc。 你可以实现这一接口,并检查 Subject,为了你可能做出这一决定的任何信息

Subject Inspection

但实现 isSessionStorageEnabled(subject)接口方法时,你可以一直查看 Subject 并访问任何你需要用来作出决定的东西。

当然所有期望的 Subject 方法都是可用的(gePrincipals()等),但特定环境的 Subject 实例也是有价值的。

例如,在 Web 应用程序中,如果该决定必须基于当前 ServletRequest 中的数据,你可以获取该 request 或该 response,因为运行时的Subjce 实例实际上就是一个 WebSubject 实例:

...
public boolean isSessionStorageEnabled(Subject subject) {
    boolean enabled = false;
    if (WebUtils.isWeb(Subject)) {
        HttpServletRequest request = WebUtils.getHttpRequest(subject);
        //set 'enabled' based on the current request.
    } else {
        //not a web request - maybe a RMI or daemon invocation?
        //set 'enabled' another way...
    }

    return enabled;
}

N.B.框架开发人员应该考虑到这种类型的访问,并确保任何请求/调用/消息上下文对象可用是同过特定环境下的 Subject 实现的。联系 Shiro 用户邮件列表,如果你想帮助设置它,为了你的框架/环境。

配置

在你实现了 SessionStorageEvaluator 接口后,你可以在 shiro.ini 中配置它:

[main]
...
sessionStorageEvaluator = com.mycompany.shiro.subject.mgt.MySessionStorageEvaluator
securityManager.subjectDAO.sessionStorageEvaluator = $sessionStorageEvaluator
...
Web Applications

通常 Web 应用程序希望在每一个请求的基础上容易地启用或禁用会话的创建,不管是哪个 Subject 正在执行请求。这经常在支持 REST 及Messaging/RMI 构架上使用来产生很好的效果。例如,也许正常的终端用户(使用浏览器的人)被允许创建和使用会话,但远程的 API 客户端使用REST 或 SOAP,不该拥有会话(因为它们在每一个请求上验证, 常见于 REST/SOAP 体系结构)。

为了支持这种 hybrid/per-request (混合/每次请求)的能力,noSessionCreation 过滤器被添加到 Shiro 的默认“池”g过滤器中,为 Web 应用程序启用的。该过滤器将会阻止在请求期间创建新的会话来保证无状态的体验。在shiro.ini 的[urls]项中,你通常定义该过滤器在所有其它过滤器之前来确保会话永远不会被使用。

举例:

[urls]
...
/rest/** = noSessionCreation, authcBasic, ...

这个过滤器允许现有会话的任何会话操作,但不允许在过滤的请求创建新的会话。也就是说,在请求或没有会话存在的Subject 调用下面四个方法中的任何一个时,将会自动地触发一个 DisabledSessionException 异常:

  • httpServletRequest.getSession()
  • httpServletRequest.getSession(true)
  • subject.getSession()
  • subject.getSession(true)

如果一个 Subject 在访问 noSessionCreation-protected-URL(无会话创建保护的 URL) 之前已经有一个会话,则上述的四种调用仍然会如预期工作。

最后,在所有情况下,下面的调用将始终被允许:

  • httpServletRequest.getSession(false)
  • subject.getSession(false)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值