了解 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 *方法执行的CallableRunnable将在Callable/Runnable执行之前和之后自动将主题绑定和取消绑定到线程。

  • 手动关联 -您将Subject实例手动绑定和取消绑定到当前正在执行的线程。这通常对框架开发人员很有用。

  • 不同的线程 -通过调用Subject.associateWith *方法将CallableRunnableSubject关联,然后由另一个线程执行返回的Callable/Runnable。如果您需要在另一个线程上作为Subject执行工作,则这是首选方法。

有关线程关联的重要信息是必须始终发生两件事:

  • 主题是绑定到线程的*,因此在线程执行的所有点上都可用。 Shiro 通过其ThreadState机制来实现此目的,该机制是ThreadLocal之上的抽象。

  • 即使线程执行导致错误,此主题在以后的某个 Moment 也是* unbound *。这样可以确保线程在清理/可重用的线程环境中保持干净并清除以前的任何Subject状态。

这些原则可以保证在上面列出的 3 种机制中发生。接下来详细说明它们的用法。

Automatic Association

如果只需要将Subject与当前线程临时关联,并且希望自动进行线程绑定和清除,则可以直接使用Subject直接执行CallableRunnableSubject.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 *方法的作用-它们只是在CallableRunnable执行之前和之后自动执行此逻辑。这也是 Shiro 的ShiroFilter针对 Web 应用程序执行的几乎相同的逻辑(ShiroFilter使用本节范围之外的特定于 Web 的ThreadState实现)。

Web Use

不要在正在处理 Web 请求的线程中使用上面的ThreadState代码示例。在 Web 请求期间会使用特定于 Web 的 ThreadState 实现。相反,请确保ShiroFilter拦截 Web 请求,以确保正确完成主题的构建/绑定/清除。

不同的线程

如果您有一个CallableRunnable实例应作为Subject执行,并且您将自己执行CallableRunnable(或将其交给线程池或ExecutorExecutorService),则应使用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用户邮件列表