参考资料

  该文中的内容来源于 Oracle 的官方文档。Oracle 在 Java 方面的文档是非常完善的。对 Java 8 感兴趣的朋友,可以从这个总入口 Java SE 8 Documentation 开始寻找感兴趣的内容。本博客不定期从 Oracle 官网搬砖。这一篇主要讲 JAAS,官方文档在这里 Java Authentication and Authorization Service (JAAS) Reference Guide

前言

  说实话,我用 Java 这么多年基本上就没有用到过 JAAS,所以它在我心中永远都是一个梗。和我一样一直没有跨过这个梗的 Java 程序员多吗?今天,就让我们一起揭开它的面纱,看看 JAAS 究竟是个什么鬼。

  从定义上看,JAAS 是一个 Java 程序专用的认证和授权框架。认证和授权我们并不陌生,比如我们登录系统的时候需要输入用户名和密码,远程连接 SSH 的时候需要提供密钥,网络支付的时候需要提供手机号和验证码等等,这些都是对用户进行认证的方式。我们陌生的是,在我们实现用户的认证和授权这样的功能时,我们是否需要用到 JAAS,以及 JAAS 是否如商家宣传的那样好用,再以及 JAAS 这个框架是否解决了用户认证和授权过程中那些千奇百怪的用户交互方式的统一抽象。

  JAAS 的主要优点是什么?官方文档说它是一个可插拔的用户认证和授权框架,当我们需要更改用户的认证方式和授权的时候,只需要修改少量的配置文件即可,无需更改程序代码。和前面讲过的 使用 SecurityManager 和 Policy File 管理 Java 程序的权限 相比,JAAS 的区别又是什么?可以这样理解,SecurityManager 和 Policy File 是针对程序本身的授权,比如程序从哪个路径运行,程序由谁签名等,而 JAAS 则是从用户的角度进行授权,它关注的是:哪一个用户在运行这个程序?哪一个用户在访问需要授权的资源?

  下面,我们看看实际的 JAAS 程序长什么样。

JAAS 示例程序的运行效果

  对于陌生的领域,如果能直接看到运行效果,应该可以加快我们对它的理解速度。下图显示了一个 JAAS 示例程序的运行:
JAAS 是个什么梗-LMLPHP

  可以看到,要使用 JAAS,除了程序之外,还需要准备额外的配置文件。在本例中,有两个配置文件,一个 youxia_jaas.policy 文件,这是一个 Policy File,在 使用 SecurityManager 和 Policy File 管理 Java 程序的权限 中讲过,另一个文件是 youxia_jaas.config 文件,它就是 JAAS 必须用到的配置文件。另外,要使用 JAAS,还必须以 -Djava.security.auth.login.config=... 这个参数运行 java 命令。这个在上图中都有展示。

  示例程序运行的时候,会跳出一个对话框让我们输入 keystore 中的条目的别名以及 keystore 的密码。这就是一种和用户的交互方式。JAAS 需要解决的一个问题就是如何将和用户交互的千奇百怪的方式进行统一,对话框是一种常见的方式,从标准输入读取用户输入也是一种常见的方式,还有其它一些我们想得到和想不到的方式等等,这个后面会讲到。在示例程序中,我们输入的别名会被用来认证我们的身份,然后决定我们是否有权访问相应的资源。比如,当我们输入的是 youxia,则可以顺利读取 System Property,可以顺利统计 src/com/xkland/sample/JaasDemo.java 中的字符数。当我们输入的是 stranger 时,则显示该用户没有相应的权限。如下图:
JAAS 是个什么梗-LMLPHP

  官方对 JAAS 的宣传说它可以不用更改代码、只需要更改配置文件就能更改认证方式,是这样的吗?在本示例中,我用的认证方式是通过访问 keystore 中的条目来的。下面,我把它改成通过当前 Linux 的用户进行认证。如下图:
JAAS 是个什么梗-LMLPHP

  可以看到,我只修改了 youxia_jaas.config 文件,将 LoginModel 从 KeyStoreLoginModel 改成了 UnixLoginModel,没有修改程序代码。更改 LoginModel 后,程序输出的 Principal 的格式和内容都变了,但是功能仍然正常执行。而且可以看到,可以有很多个不同格式的 Principal 代表一个用户,比如本例中就有的 Principal 代表用户名,有的 Principal 代表用户 ID,还有的 Principal 代表 Group ID。

  LoginModel 控制的只是认证,认证成功后,我们的程序中就有了代表用户的 Principal,哦不,是 Principal(s)。而授权靠的依然是 Policy File,恰好,policytool 中有专用的基于 Principal 进行授权的按钮和文本框,我们终于知道 Principal 是什么了(中文版是“主用户”,英文版是“Principal”),如下图:
JAAS 是个什么梗-LMLPHP

解析 JAAS 的架构

  看完示例程序的运行,我们对 JAAS 已经有了一个比较直观的了解。下面,我们在 Eclipse 的辅助下来解析一下 JAAS 的架构。随着探索的领域越来越深,是时候上 IDE 了,关于几种 IDE 的体验,请看这里 Java 开发主流 IDE 环境体验

  首先,JAAS 认证的核心是 LoginModel,它代表了认证的方式。认证的方式有很多种,但 JDK 自带的几种实现不一定能满足我们的需求。所以,如果我们真的要在产品中使用 JAAS 的话,一定要学会实现自己的 LoginModel。先来看看 JDK 中自带了哪几种 LoginModel 的实现,通过 Eclipse 的 Type Hierarchy 功能可以查看,如下图:
JAAS 是个什么梗-LMLPHP

  可以看到,JDK 自带了 JndiLoginModel、UnixLoginModel、KeyStoreLoginModel、Krb5LoginModel、LdapLoginModel 等实现,看名字就能猜到它们使用的认证方式。很显然,在实际的产品种,我们必须实现自己的 LoginModel,比如说,在 Web 开发中流行的都是把用户名和密码储存在关系型数据库中,那么要在 Web 开发中使用 JAAS,就要实现一个 RdbmsLoginModel。在本例中,我就不实现自己的 LoginModel 了,就用 JDK 自带的 UnixLoginModel 和 KeyStoreLoginModel 吧,做示范已经足够了。

  而且从上图中可以看出, UnixLoginModel 使用了好几个不同类型的 Principal,这和示例程序的输出是一致的。KeyStoreLoginModel 就只使用了一种 Principal,那就是 X500Principal,如下图:
JAAS 是个什么梗-LMLPHP

  JDK 自带的 Principal 有多少种呢?看下图:
JAAS 是个什么梗-LMLPHP

  每一个 Principal 都有不同的格式和存储数据的方式,但它们都有一个 getName() 方法,返回代表该 Principal 的字符串。在实际应用中,我们可以实现自己的 Principal。

  其次,JAAS 和用户交互的核心是 CallbackHandler,它代表了和用户交互的方式。JDK 自带了哪几种 CallbackHandler 呢?看下图:
JAAS 是个什么梗-LMLPHP

  从名字可以看出,DialogCallbackHandler 就是使用对话框和用户交互,而 TextCallbackHandler 就是使用标准输入输出和用户交互,其它几种 CallbackHandler 我也不熟。但是很显然,在实际的产品中,我们必须实现自己的 CallbackHandler,比如在 Web 开发中, DialogCallbackHandler 和 TextCallbackHandler 都是用不到的,可能需要实现自己的 HttpCallbackHandler。在本例中,我也不实现自己的 CallbackHandler 了,就用 JDK 自带的 DialogCallbackHandler 和 TextCallbackHandler 吧。不知道为什么 DialogCallbackHandler 还是 Deprecated 的,也就是说在下个版本中可能就没有了。为什么了,难道是 Swing 已死?要是果真如此,NetBeans 死期也不远了。

  最后,LoginModel 和 CallbackHandler 之间沟通的桥梁是 Callback。Callback 就那么几种,如下图:
JAAS 是个什么梗-LMLPHP

  当 LoginModel 需要从用户获取 username 时,就向 CallbackHandler 发一个 NameCallback, CallbackHandler 和用户交互,获得 username 后再发回去。当 LoginModel 需要密码的时候就向 CallbackHandler 发一个 PasswordCallback,CallbackHandler 和用户交互,获得 password 后再发回去。当 LoginModel 需要给用户发送一些消息时,就向 CallbackHandler 发一个 TextOutputCallback,让 CallbackHandler 将消息显示给用户。以此类推,总之,和用户进行交互的永远是 CallbackHandler。

编写使用 JAAS 的程序

  通过前面的解析,我们已经了解了 JAAS 中的几个核心接口。但是在实际程序中如何使用呢?那就要看这里的示例程序了。下图是 JaasDemo.java 的代码的前半截:
JAAS 是个什么梗-LMLPHP

  可以看到,用户认证需要用到 LoginContext 类。在上图的代码中,先是使用 lc = new LoginContext("JaasDemo", new DialogCallbackHandler()); 来创建一个 LoginContext 类的对象 lc。就是这个语句将 LoginModel 和 CallbackHandler 联系到了一起。LoginModel 在哪?在配置文件中呢!这里的字符串 "JaasDemo" 代表的就是配置文件中的一个条目,而配置文件是在运行程序的时候通过 -Djava.security.auth.login.config= 参数指定的。然后,调用 lc.login() 就完成了用户的认证。就是这么简单。

  用户认证失败程序就退出了。用户认证成功了,怎么才能让该用户去执行需要授权的功能呢?请看示例程序 JaasDemo.java 的后半截,如下图:
JAAS 是个什么梗-LMLPHP

  这里用到了一个新类 Subject,它代表的才是通过认证的用户。所以代码 Subject mySubject = lc.getSubject(); 就是获得当前通过认证的用户。前面提到过,一个用户可以有很多个 Principal,所以通过 mySubject.getPrincipals(); 获得所有的 Principal,然后把它们打印出来。怎么样才能让用户执行需要授权的任务呢?我们必须把任务代码写到另外一个类中,这个类必须从 PrivilegedAction<T> 或 PrivilegedExceptionAction<T> 继承,如下图,我的 SampleAction 类的代码:
JAAS 是个什么梗-LMLPHP

  PrivilegedExceptionAction<T> 是一个泛型类,其中的模板参数 T 代表的是 run() 方法返回的值的类型。在本例中我不关心它返回什么,所以用 Object 作为模板参数就行了。然后在 JaasDemo.java 中使用 Subject.doAsPrivileged(mySubect, new SampleAction(), null); 调用我的任务即可。这里的示例任务很简单,就是获取几个 System Property,再统计一个文件的字符数,这个示例任务在上上篇博客中出现过。

为示例程序授权

  授权还是要使用 Policy File。关于 policytool 工具的使用我就不展示了,直接展示 youxia_jaas.policy 文件的内容,如下图:
JAAS 是个什么梗-LMLPHP

  可以看到,这个 Policy File 中有三个条目。第一个条目中不涉及到 Principal,而且它授权的两个 Permission 我们之前也没有碰到过。其实很简单,这个条目就是对我们代码中的 new LoginContext()Subject.doAsPrivileged() 这两个语句进行授权,如果没有这个条目,SecurityManager 是不会允许我们的程序运行的。而后面两个条目分别针对 X500Principal 和 UnixPrincipal 进行授权,让我们的程序能够访问相应的 System Property 和相应的文件。就是这么简单。

在 IDE 中运行示例程序

  示例程序的运行效果本文一开篇就已经给出了。其实除了从控制台执行程序外,我们还能从 IDE 中直接运行程序。从 IDE 运行程序需要修改程序运行的参数,如下图:
JAAS 是个什么梗-LMLPHP

  运行效果如下图:
JAAS 是个什么梗-LMLPHP

总结

  这就是我对 JAAS 的理解。虽然以后的工作中可能用不到,但是至少我知道了 JAAS 怎么用。这个示例程序是很简单的,仅仅使用了 JDK 自带的 KeyStoreLoginModel、UnixLoginModel 和 DialogCallbackHandler,而实际工作中需要实现自己的 LoginModel 和 CallbackHandler。只要理解了 JAAS 的本质,实现自己的 LoginModel 和 CallbackHandler 也不难。具体细节请大家自己查看官方文档吧。

04-14 08:56