On this page
了解 Apache Shiro 中的主题
毫无疑问,Apache Shiro 中最重要的概念是Subject
。 “主题”只是一个安全性术语,表示应用程序用户的特定于安全性的“视图”。 Shiro Subject
实例代表*“单个”应用程序用户的安全性状态和操作。
这些操作包括:
authentication (login)
授权(访问控制)
session access
logout
我们最初想要将其称为“用户”,因为那“很有意义”,但是我们决定反对:太多的应用程序具有已经具有自己的用户类/框架的现有 API,并且我们不想与之冲突。同样,在安全领域中,“主题”一词实际上是公认的术语。
Shiro 的 API 鼓励以Subject
为中心的编程范例。在编写应用程序逻辑代码时,大多数应用程序开发人员都想知道“当前正在执行”的用户是谁。尽管应用程序通常可以通过自己的机制(UserService 等)查找任何用户,但是在安全性方面,最重要的问题是 “谁是当前用户?”
尽管可以使用SecurityManager
来获取任何主题,但仅基于当前用户/ Subject
的应用程序代码更加自然和直观。
当前正在执行的主题
在几乎所有环境中,您都可以使用org.apache.shiro.SecurityUtils
获得当前正在执行的Subject
:
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 将默认自动使用其企业会话 Management。这意味着无论部署环境如何,您都可以在任何层的应用程序中使用相同的 API。这为应用程序打开了一个全新的世界,因为不需要强制要求会话的任何应用程序使用HttpSession
或 EJB 状态会话 Bean。而且,任何 Client 端技术现在都可以共享会话数据。
因此,现在您可以获得Subject
和他们的Session
。 “true”有用的东西(例如检查是否被允许做事,例如检查角色和权限)呢?
好吧,我们只能对已知用户进行检查。上面的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." );
}
我们还可以查看他们是否具有permission来对某种类型的实体执行操作:
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.");
}
此外,我们可以执行极其强大的* instance-level * permission检查-能够查看用户是否具有访问类型的特定实例的能力:
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 中新增的功能是可以构造自定义/临时主题实例以在特殊情况下使用的功能。
Special Use Only!
您几乎应该总是通过调用SecurityUtils.getSubject();
来获取当前正在执行的 Subject,只有在特殊情况下才应该创建自定义Subject
实例。
可能有用的一些“特殊情况”:
- 系统启动/引导-没有用户与系统交互,但代码应以“系统”或守护程序用户身份执行。希望创建代表特定用户的 Subject 实例,以便引导程序代码以该用户(例如
admin
用户)的身份执行。
鼓励这种做法,因为它可确保 Util/系统代码以与普通用户相同的方式执行,从而确保代码一致。这使代码更易于维护,因为您不必担心仅针对系统/守护程序方案的自定义代码块。
集成Testing-您可能需要创建
Subject
实例以用于集成测试。有关详情,请参见testing documentation。守护程序/后台进程的工作-守护程序或后台进程执行时,可能需要以特定用户的身份执行。
Tip
如果您已经可以访问Subject
实例并希望其他线程可以使用它,则应使用Subject.associateWith
*方法,而不要创建新的 Subject 实例。
好的,假设您仍然需要创建自定义主题实例,让我们看看如何做到这一点:
Subject.Builder
提供Subject.Builder
类可轻松构建Subject
实例,而无需了解构造细节。
Builder 的最简单用法是构造一个匿名的,无会话的Subject
实例:
Subject subject = new Subject.Builder().buildSubject()
上面显示的默认无参数Subject.Builder()
构造函数将通过SecurityUtils.getSecurityManager()
方法使用应用程序当前可访问的SecurityManager
。如果需要,您还可以指定SecurityManager
实例供其他构造函数使用:
SecurityManager securityManager = //acquired from somewhere
Subject subject = new Subject.Builder(securityManager).buildSubject();
可以在buildSubject()
方法之前调用所有其他Subject.Builder
方法,以提供有关如何构造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
关联。
Thread Association
如上所述,仅构建Subject
实例不会将其与线程相关联-这是通常的要求,如果在线程执行过程中对SecurityUtils.getSubject()
的任何调用都能正常工作。确保线程与Subject
关联的方法有以下三种:
自动关联 -通过
Subject.execute
*方法执行的Callable
或Runnable
将在Callable
/Runnable
执行之前和之后自动将主题绑定和取消绑定到线程。手动关联 -您将
Subject
实例手动绑定和取消绑定到当前正在执行的线程。这通常对框架开发人员很有用。不同的线程 -通过调用
Subject.associateWith
*方法将Callable
或Runnable
与Subject
关联,然后由另一个线程执行返回的Callable
/Runnable
。如果您需要在另一个线程上作为Subject
执行工作,则这是首选方法。
有关线程关联的重要信息是必须始终发生两件事:
主题是绑定到线程的*,因此在线程执行的所有点上都可用。 Shiro 通过其
ThreadState
机制来实现此目的,该机制是ThreadLocal
之上的抽象。即使线程执行导致错误,此主题在以后的某个 Moment 也是* unbound *。这样可以确保线程在清理/可重用的线程环境中保持干净并清除以前的任何
Subject
状态。
这些原则可以保证在上面列出的 3 种机制中发生。接下来详细说明它们的用法。
Automatic Association
如果只需要将Subject
与当前线程临时关联,并且希望自动进行线程绑定和清除,则可以直接使用Subject
直接执行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);
}
});
Manual Association
尽管Subject.execute
*方法在返回后会自动清除线程状态,但在某些情况下您可能需要自己 ManagementThreadState
。集成 w/Shiro 时,这几乎总是在框架级开发中完成的,即使在 bootstrap/daemon 场景(上面的Subject.execute(callable)
示例更常见)中也很少使用。
Guarantee Cleanup
关于此机制的最重要的事情是,您必须始终确保在执行逻辑后清除当前线程,以确保在可重用或池化的线程环境中没有线程状态损坏。
确保清理最好在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
使用本节范围之外的特定于 Web 的ThreadState
实现)。
Web Use
不要在正在处理 Web 请求的线程中使用上面的ThreadState
代码示例。在 Web 请求期间会使用特定于 Web 的 ThreadState 实现。相反,请确保ShiroFilter
拦截 Web 请求,以确保正确完成主题的构建/绑定/清除。
不同的线程
如果您有一个Callable
或Runnable
实例应作为Subject
执行,并且您将自己执行Callable
或Runnable
(或将其交给线程池或Executor
或ExecutorService
),则应使用Subject.associateWith
*方法。这些方法确保可以保留主题,并且可以在最终执行的线程上对其进行访问。
Callable example:
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 executor = java.util.concurrent.Executors.newCachedThreadPool();
//execute the work on a different thread as the built Subject:
executor.execute(work);
Runnable example:
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);
ExecutorService executor = java.util.concurrent.Executors.newCachedThreadPool();
//execute the work on a different thread as the built Subject:
executor.execute(work);
Automatic Cleanup
associateWith
*方法自动执行必要的线程清理,以确保线程在池化环境中保持干净。
协助处理文档
尽管我们希望该文档对您使用 Apache Shiro 所做的工作有所帮助,但社区一直在不断改进和扩展文档。如果您想为 Shiro 项目提供帮助,请考虑在需要的地方更正,扩展或添加文档。您提供的每一点帮助都会扩大社区,进而改善 Shiro。
提交文档的最简单方法是通过单击下面的Edit
链接提交请求请求,然后将其发送到User Forum或用户邮件列表。