Skip to content

Latest commit

 

History

History
337 lines (231 loc) · 15.9 KB

4.Custom Subjects.md

File metadata and controls

337 lines (231 loc) · 15.9 KB

毫无疑问,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应用程序内部部署,则默认情况下SessionHttpSession基于。但是,在非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实例并希望它可供其他线程使用,则应使用Subject.associateWith*方法而不是创建新的Subject实例。

好的,假设你仍然需要创建自定义主题实例,让我们看看如何做到这一点:

Subject.Builder

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

  • 自动关联 -一个CallableRunnable通过执行Subject.execute*方法将自动绑定和前,后取消绑定除线程Callable/ Runnable执行。
  • 手动关联 - 您手动将Subject实例绑定和取消绑定到当前正在执行的线程。这通常对框架开发人员有用。
  • 不同的线程 - 通过调用*方法A CallableRunnable与之关联,然后由另一个线程执行返回的/ 。如果您需要在另一个线程上执行工作,这是首选方法。Subject``Subject.associateWith``Callable``Runnable``Subject

关于线程关联的重要事项是必须始终发生两件事:

  1. Subject被绑定到线程,因此它可以在线程执行的所有点上使用。Shiro通过它的ThreadState机制来做到这一点,这是一个抽象的顶部ThreadLocal
  2. 即使线程执行导致错误,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*方法做-他们只是之前和之后自动执行此逻辑CallableRunnable执行。它也是Shiro ShiroFilter为Web应用程序执行的几乎相同的逻辑(ShiroFilter使用ThreadState本节范围之外的特定于Web的实现)。

网络使用


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

如果你有一个CallableRunnable应该执行的情况下Subject,你将执行CallableRunnable自己(或它交给一个线程池或ExecutorExecutorService为例),你应该使用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*方法自动执行必要的线程清理,以确保线程池环境中保持清洁。