毫无疑问,Apache Shiro中最重要的概念就是Subject
。“主题”只是一个安全术语,表示应用程序用户的特定于安全性的“视图”。Shiro Subject
实例表示单个应用程序用户的安全状态和操作。
这些操作包括:
- 身份验证(登录)
- 授权(访问控制)
- 会话访问
- 登出
我们原本想把它称为“用户”,因为“只是有意义”,但我们决定反对它:太多的应用程序已经拥有自己的用户类/框架的现有API,我们不想与这些相冲突。此外,在安全领域,术语“主体”实际上是公认的命名法。
Shiro的API鼓励Subject
应用程序的中心编程范例。编写应用程序逻辑时,大多数应用程序开发人员想知道当前正在执行的用户是谁。虽然应用程序通常可以通过自己的机制(UserService等)查找任何用户,但在安全性方面,最重要的问题是**“谁是当前用户?”**
虽然可以通过使用来获取任何主题SecurityManager
,但仅基于当前用户的应用程序代码/ Subject
更加自然和直观。
在几乎所有环境中,您都可以Subject
使用org.apache.shiro.SecurityUtils
以下命令获取当前执行的:
Subject currentUser = SecurityUtils.getSubject();
getSubject()
独立应用程序中的调用可以Subject
基于特定于应用程序的位置中的用户数据返回,并且在服务器环境(例如,web应用程序)中,它基于与当前线程或传入请求相关联的用户数据来获取主题。
获得电流后Subject
,你能用它做什么?
如果您希望在当前与应用程序的会话期间向用户提供内容,则可以获取其会话:
Session session = currentUser.getSession();
session.setAttribute( "someKey", "aValue" );
这Session
是一个特定于Shiro的实例,它提供了常规HttpSession所用的大部分内容,但有一些额外的好东西和一个很大的区别:它不需要HTTP环境!
如果在Web应用程序内部部署,则默认情况下Session
将HttpSession
基于。但是,在非Web环境中,如此简单的快速入门,Shiro默认会自动使用其企业会话管理。这意味着无论部署环境如何,您都可以在任何层中的应用程序中使用相同的API。这打开了一个全新的应用程序世界,因为任何需要会话的应用程序都不需要被强制使用HttpSession
或EJB有状态会话Bean。而且,任何客户端技术现在都可以共享会话数据。
所以现在你可以获得一个Subject
和他们的Session
。那些真正有用的东西呢,比如检查是否允许他们做某事,比如检查角色和权限?
好吧,我们只能为已知用户进行检查。我们Subject
上面的实例代表当前用户,但实际上谁是当前用户?好吧,他们是匿名的 - 也就是说,直到他们至少登录一次。所以,让我们这样做:
if ( !currentUser.isAuthenticated() ) {
//collect user principals and credentials in a gui specific manner
//such as username/password html form, X509 certificate, OpenID, etc.
//We'll use the username/password example here since it is the most common.
//(do you know what movie this is from? ;)
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
//this is all you have to do to support 'remember me' (no config - built in!):
token.setRememberMe(true);
currentUser.login(token);
}
而已!这可不容易。
但如果他们的登录尝试失败怎么办?您可以捕获各种特定的异常,告诉您到底发生了什么:
try {
currentUser.login( token );
//if no exception, that's it, we're done!
} catch ( UnknownAccountException uae ) {
//username wasn't in the system, show them an error message?
} catch ( IncorrectCredentialsException ice ) {
//password didn't match, try again?
} catch ( LockedAccountException lae ) {
//account for that username is locked - can't login. Show them a message?
}
... more types exceptions to check if you want ...
} catch ( AuthenticationException ae ) {
//unexpected condition - error?
}
您,因为应用程序/ GUI开发人员可以选择根据异常显示最终用户消息(例如,"There is no account in the system with that username."
)。您可以检查许多不同类型的例外情况,或者根据Shiro可能无法解释的自定义条件投放您自己的例外情况。有关更多信息,请参阅AuthenticationException JavaDoc。
好的,所以到现在为止,我们已经登录了用户。我们还能做什么?
让我们说一下他们是谁:
//print their identifying principal (in this case, a username):
log.info( "User [" + currentUser.getPrincipal() + "] logged in successfully." );
我们还可以测试他们是否具有特定的角色:
if ( currentUser.hasRole( "schwartz" ) ) {
log.info("May the Schwartz be with you!" );
} else {
log.info( "Hello, mere mortal." );
}
我们还可以看看他们是否有权对某种类型的实体采取行动:
if ( currentUser.isPermitted( "lightsaber:weild" ) ) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
此外,我们可以执行极其强大的实例级 权限检查 - 能够查看用户是否能够访问类型的特定实例:
if ( currentUser.isPermitted( "winnebago:drive:eagle5" ) ) {
log.info("You are permitted to 'drive' the 'winnebago' with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
一块蛋糕,对吧?
最后,当用户完成使用该应用程序后,他们可以注销:
currentUser.logout(); //removes all identifying information and invalidates their session too.
这个简单的API构成了Shiro最终用户在使用Shiro时必须处理的90%。
Shiro 1.0中添加的一项新功能是能够构建自定义/临时主题实例,以便在特殊情况下使用。
仅限特殊用途!
您应该几乎总是通过调用SecurityUtils.getSubject();
创建自定义Subject
实例来获取当前正在执行的主题只应在特殊情况下完成。
一些“特殊情况”,当这可能有用:
-
系统启动/引导 - 当没有用户与系统交互时,但代码应作为“系统”或守护程序用户执行。期望创建表示特定用户的主题实例,因此引导代码作为该用户(例如,作为
admin
用户)执行。鼓励这种做法,因为它确保实用程序/系统代码以与普通用户相同的方式执行,确保代码一致。这使得代码更易于维护,因为您不必担心仅针对系统/守护程序方案的自定义代码块。
-
守护程序/后台进程工作 - 执行守护程序或后台进程时,可能需要以特定用户身份执行。
小费
如果您已经有权访问某个Subject
实例并希望它可供其他线程使用,则应使用Subject.associateWith
*方法而不是创建新的Subject实例。
好的,假设你仍然需要创建自定义主题实例,让我们看看如何做到这一点:
本Subject.Builder
类提供给建立Subject
轻松实例,而无需知道施工细节。
Builder最简单的用法是构造一个匿名的无会话Subject
实例:
Subject subject = new Subject.Builder().buildSubject()
上面显示的默认的no-arg Subject.Builder()
构造函数将使用当前可SecurityManager
通过该SecurityUtils.getSecurityManager()
方法访问的应用程序。SecurityManager
如果需要,您还可以指定其他构造函数使用的实例:
SecurityManager securityManager = //acquired from somewhere
Subject subject = new Subject.Builder(securityManager).buildSubject();
Subject.Builder
可以在buildSubject()
方法之前调用所有其他方法,以提供有关如何构造Subject
实例的上下文。例如,如果您有会话ID并且想要获取该Subject
“会话”(假设会话存在且未过期):
Serializable sessionId = //acquired from somewhere
Subject subject = new Subject.Builder().sessionId(sessionId).buildSubject();
同样,如果要创建Subject
反映特定标识的实例:
Object userIdentity = //a long ID or String username, or whatever the "myRealm" requires
String realmName = "myRealm";
PrincipalCollection principals = new SimplePrincipalCollection(userIdentity, realmName);
Subject subject = new Subject.Builder().principals(principals).buildSubject();
然后,您可以使用构建的Subject
实例并按预期对其进行调用。但请注意:
构建的Subject
实例不会自动绑定到应用程序(线程)以供进一步使用。如果希望它对任何调用的代码可用SecurityUtils.getSubject()
,则必须确保Thread与构造的代码相关联Subject
。
如上所述,仅构建Subject
实例不会将其与线程相关联 - 如果SecurityUtils.getSubject()
线程执行期间的任何调用都能正常工作,则通常需要这样做。有三种方法可以确保线程与a相关联Subject
:
- 自动关联 -一个
Callable
或Runnable
通过执行Subject.execute
*方法将自动绑定和前,后取消绑定除线程Callable
/Runnable
执行。 - 手动关联 - 您手动将
Subject
实例绑定和取消绑定到当前正在执行的线程。这通常对框架开发人员有用。 - 不同的线程 - 通过调用*方法A
Callable
或Runnable
与之关联,然后由另一个线程执行返回的/ 。如果您需要在另一个线程上执行工作,这是首选方法。Subject``Subject.associateWith``Callable``Runnable``Subject
关于线程关联的重要事项是必须始终发生两件事:
- Subject被绑定到线程,因此它可以在线程执行的所有点上使用。Shiro通过它的
ThreadState
机制来做到这一点,这是一个抽象的顶部ThreadLocal
。 - 即使线程执行导致错误,Subject 在某个时间点仍未绑定。这确保了线程保持清洁并且清除
Subject
了池化/可重用线程环境中的任何先前状态。
这些原则保证在上面列出的3种机制中发生。接下来详细阐述了它们的用法。
如果你只需要Subject
暂时与当前线程关联,并且你想要自动进行线程绑定和清理,那么Subject
直接执行a Callable
或者Runnable
是要走的路。在之后Subject.execute
调用返回,当前线程是保证在相同的状态,因为它是在执行前。这种机制是三者中使用最广泛的机制。
例如,假设您在系统启动时有一些逻辑要执行。您希望作为特定用户执行一大块代码,但是一旦逻辑完成,您需要确保线程/环境自动恢复正常。你可以通过调用Subject.execute
*方法来做到这一点:
Subject subject = //build or acquire subject
subject.execute( new Runnable() {
public void run() {
//subject is 'bound' to the current thread now
//any SecurityUtils.getSubject() calls in any
//code called from here will work
}
});
//At this point, the Subject is no longer associated
//with the current thread and everything is as it was before
当然Callable
也支持实例,因此您可以拥有返回值并捕获异常:
Subject subject = //build or acquire subject
MyResult result = subject.execute( new Callable<MyResult>() {
public MyResult call() throws Exception {
//subject is 'bound' to the current thread now
//any SecurityUtils.getSubject() calls in any
//code called from here will work
...
//finish logic as this Subject
...
return myResult;
}
});
//At this point, the Subject is no longer associated
//with the current thread and everything is as it was before
这种方法在框架开发中也很有用。例如,Shiro对安全Spring远程处理的支持确保远程调用作为特定主题执行:
Subject.Builder builder = new Subject.Builder();
//populate the builder's attributes based on the incoming RemoteInvocation ...
Subject subject = builder.buildSubject();
return subject.execute(new Callable() {
public Object call() throws Exception {
return invoke(invocation, targetObject);
}
});
虽然Subject.execute
*方法在返回后会自动清理线程状态,但在某些情况下您可能需要自行管理ThreadState
。当与wiro集成时,这几乎总是在框架级开发中完成,甚至在bootstrap / daemon场景中也很少使用(Subject.execute(callable)
上面的例子更常见)。
保证清理
关于此机制最重要的一点是,必须始终保证在执行逻辑后清除当前线程,以确保在可重用或池化线程环境中没有线程状态损坏。
保证清理最好在一个try/finally
块中完成:
Subject subject = new Subject.Builder()...
ThreadState threadState = new SubjectThreadState(subject);
threadState.bind();
try {
//execute work as the built Subject
} finally {
//ensure any state is cleaned so the thread won't be
//corrupt in a reusable or pooled thread environment
threadState.clear();
}
有趣的是,这正是Subject.execute
*方法做-他们只是之前和之后自动执行此逻辑Callable
或Runnable
执行。它也是Shiro ShiroFilter
为Web应用程序执行的几乎相同的逻辑(ShiroFilter
使用ThreadState
本节范围之外的特定于Web的实现)。
网络使用
不要ThreadState
在处理Web请求的线程中使用上面的代码示例。而是在Web请求期间使用特定于Web的ThreadState实现。相反,请确保ShiroFilter
拦截Web请求以确保正确完成主题构建/绑定/清理。
如果你有一个Callable
或Runnable
应该执行的情况下Subject
,你将执行Callable
或Runnable
自己(或它交给一个线程池或Executor
或ExecutorService
为例),你应该使用Subject.associateWith
*的方法。这些方法确保在最终执行的线程上保留和访问Subject。
可调用的例子:
Subject subject = new Subject.Builder()...
Callable work = //build/acquire a Callable instance.
//associate the work with the built subject so SecurityUtils.getSubject() calls works properly:
work = subject.associateWith(work);
ExecutorService executorService = new java.util.concurrent.Executors.newCachedThreadPool();
//execute the work on a different thread as the built Subject:
executor.execute(work);
可运行的例子:
Subject subject = new Subject.Builder()...
Runnable work = //build/acquire a Runnable instance.
//associate the work with the built subject so SecurityUtils.getSubject() calls works properly:
work = subject.associateWith(work);
Executor executor = new java.util.concurrent.Executors.newCachedThreadPool();
//execute the work on a different thread as the built Subject: executor.execute(work);
自动清理
在associateWith
*方法自动执行必要的线程清理,以确保线程池环境中保持清洁。