Java security包含很多特性,JAAS只是众多特性中的一个,JAAS正式集成入JDK1.4。JAAS是Java Authentication and Authorization Service的缩写。不难看出,JAAS包含两方面的内容:Authentication和Authorization。本文主要是介绍JAAS中的Authentication。
什么是Authentication
Authentication就是身份验证,是对系统保护的第一道防线。其主要功能是鉴定请求的来源,只有满足特定身份的主体(用户或其它服务)才能访问。
举一个通俗的例子,我们在登录邮箱的时候,需要输入正确的用户名和密码之后才能查看邮件,这个过程就是身份验证。这就是Authentication要干的事情。
注:零君以前写过一篇文章《应用程序安全(Application Security)》,也讲到了Authentication的概念。具体可查阅此公众号的历史文章。
Authencation framework in JAAS
由于身份验证的方式有很多,所以JAAS实现了一个非常灵活的框架。JAAS实现的authentication framework如下图所示:

上图中LoginContext是定义于JDK包javax.security.auth.login中的一个类;而LoginModule则是定义在JDK包javax.security.auth.spi中的一个接口。
通常在为我们的应用程序增加身份验证的功能的时候,只需要与LoginContext打交道。例如在我们的应用程序中只需要如下代码即可,剩下的事情JAAS会替你搞定。
LoginContext lc = null;
try {
lc = new LoginContext("Login2");
lc.login();
} catch(LoginException e) {
......
}
如果需要实现我们自己的身份验证模块,则需要实现LoginModule接口。Login Configuration是告诉LoginContext具体有哪些身份验证模块。下文会具体描述。
Login Modules
JAAS实现的authentication framework非常灵活,使得我们可以很容易扩展,实现我们自己的login module,作为plugin集成到framework中去。要实现定制的login module,需要实现LoginModule接口。主要是要实现以下5个抽象方法:
void initialize(Subject subject, CallbackHandler callbackHandler, Map<String,?> sharedState, Map<String,?> options)
boolean login()
boolean commit()
boolean abort()
boolean logout()
initialize方法是用来初始化login module的,是被LoginContext调用的。该方法的几个参数值得注意。第一个参数subject是由上层application,或者LoginContext创建。如果上层应用程序在创建LoginContext的时候,没有传入subject,那么LoginContext会自己创建一个Subject的实例。不管下面有几个LoginModule,subject实例只会有一个。第二个参数callbackhandler是由上层应用程序创建的,可以为null。其主要用于LoginModule与上层应用程序交互,比如login module在进行身份验证时,可能会需要用户输入用户名和密码,这时就是通过这个callbackhandler来交互。第三个参数sharedState是用于多个login modules之间共享一些状态。第四个参数options是该login module特有的一些参数,定义在login configuration中。
这里主要是要理解两阶段的身份验证过程。当上层应用调用LoginContext的login函数时,实际执行过程会经历两个阶段。第一个阶段是LoginContext调用login configuration中配置的每一个LoginModule的login方法。第二阶段则是调用每个LoginModule的commit方法或者abort方法。如果在第一阶段中验证成功,则在第二阶段调用commit方法,否则调用abort方法。
在第一阶段中,会调用每个LoginModule的login方法,可能有的成功,有的失败。只要最终结果是成功,那么在第二阶段中就会调用每个LoginModule的commit方法。对于每个LoginModule来说,如果之前login成功了,那么commit方法就会将根据之前login时的状态信息生成Principal实例,并将其加入到subject中。
关于LoginModule,可以参考Oracle官方的例子,如下:
https://docs.oracle.com/javase/8/docs/technotes/guides/security/jaas/tutorials/GeneralAcnOnly.html
关于Subject和Principal这两个概念,我在以前的一篇文章“应用程序安全(Application Security)”中已经讲过。因为这两个概念还是比较重要,所以这里再提一下。Subject是身份验证作用的主体,可以是一个人或一个服务。Principal则是具体的身份标示,每一个subject可以包含多个Principals。
Login configuration
login configuration是提供给LoginContext的配置文件。可以通过两种方式来指定login configuration的位置:
1. 在java.security中指定;
java.security文件位于JRE的lib/security目录下。通过在java.security中配置下面的属性,来指定login configuration:
login.configuration.provider
login.config.url.n
我们可以通过login.configuration.provider指定其它的provider class。默认是com.sun.security.auth.login.ConfigFile,从文件中读取配置信息。 配置文件的具体位置由login.config.url.n指定,可以指定多个配置文件,例如:
login.configuration.provider=sun.security.provider.ConfigFile
login.config.url.1=file:${user.home}/.java.login.config
login.config.url.2=file:/tmp/.java.login.config
当指定了多个配置文件时,多个文件会被自动合并。
2. 通过如下命令行参数指定:
-Djava.security.auth.login.config
例如:
java -Djava.security.auth.login.config=/tmp/sample_jaas.config sample/SampleAcn
不管采用哪种方式,配置文件的格式都是一样的。下面是Oracle给出的一个login configuration的例子:
Login1 {
sample.SampleLoginModule required debug=true;
};
Login2 {
sample.SampleLoginModule required;
com.sun.security.auth.module.NTLoginModule sufficient;
com.foo.SmartCard requisite debug=true;
com.foo.Kerberos optional debug=true;
};
上面例子中的"Login1"和"Login2"对应上层的application。在本文前面给出的一个例子中,传给LoginContext的第一个参数是"Login2",就与login configuration中的"Login2"对应,也就会用包含在Login2里面的LoginModule进行实际的身份验证。
lc = new LoginContext("Login2");
每个LoginModule的配置项包含3个部分。首先是LoginModule的类名,这些类都实现了接口LoginModule。其次是每个LoginModule的标示项(flag),有4种有效的值:Required,Requisite,Sufficient和Optional。最后是可选项,具体如何使用由每个LoginModule自己决定,比如上面的"debug=true"。
这里重点是要理解LoginModule的四种标示项的含义和区别,Oracle的官方文档解释如下:
Required:
The LoginModule is required to succeed. If it succeeds or fails, authentication still continues to proceed down the LoginModule list.
Requisite:
The LoginModule is required to succeed. If it succeeds, authentication continues down the LoginModule list. If it fails, control immediately returns to the application (authentication does not proceed down the LoginModule list).
Sufficient:
The LoginModule is not required to succeed. If it does succeed, control immediately returns to the application (authentication does not proceed down the LoginModule list). If it fails, authentication continues down the LoginModule list.
Optional:
The LoginModule is not required to succeed. If it succeeds or fails, authentication still continues to proceed down the LoginModule list.
当某个application配置有多个LoginModule时,那么这些LoginModule被调用的顺序和它们在配置文件中的定义的顺序保持一致。
Requisite和Sufficient很容易理解。如果标示为Requisite的LoginModule身份验证失败,那么最终身份验证就失败了,不会调用后续的LoginModule。如果成功了,则会继续调用后续的LoginModule。
如果标示为Sufficient的LoginModule身份验证成功,那么不会继续调用后续的LoginModule了。但是只有在之前所有标示为Required和Requisite的LoginModule都成功,最终才算成功。
如果没有标示为Required和Requisite的LoginModule,那么至少需要一个标示为Sufficient或Optional的LoginModule验证成功,最终才算验证成功。
相信很多人对Required比较困惑。Required和Requisite一样,必须验证成功,否则最终验证结果就算失败。有人可能会问,那为什么标示为Required的LoginModule验证失败了,还要继续调用后面的LoginModule呢?这是故意这么设计的。设想一个场景,假设我们想统计所有的login,那么我们可以实现一个Optional的LoginModule,里面实现一些特殊的逻辑。必须保证这个Optional的LoginModule每次login时必须被调用到。这时Required就派上用场了。
小结
上面讲的是Java SE中实现的authentication framework。而实际中,很多Java application都是基于Java EE开发的。其实一般Java EE平台的authentication framework也遵循Java SE相同的逻辑,比如WebLogic、JBoss。
JAAS的authentication framework的设计虽然比较简单,但其很好的阐述了OCP(Open-Closed Principle)和DIP(Dependency Inversion Principle)的设计原则,可以说是教科书式的优秀设计。这里就不啰嗦了,具体可以自行Google或查阅本公众号早期的一篇历史文章《面向对象设计的5条原则》。
--END--





