JAAS简述


发布于 2024-04-06 / 33 阅读 / 0 评论 /
JAAS全称为Java Authentication and Authorization Service,即Java认证和授权服务。JAAS包含两方面的内容:Authentication和Authorization。

Java Security包含很多特性,JAAS只是众多特性中的一个,JAAS从JDK1.4正式进入JDK生态。

1.JAAS基本概念

学习JAAS一定要理解一下基本概念。

1.1.Subject

类全路径为javax.security.auth.Subject。

要授权访问资源,应用程序首先需要对请求的源进行身份验证。JAAS框架定义了表示请求源的术语subject。

Subject表示单个实体(例如人或服务)的相关信息的分组。 Subject就是在某个你想去认证和分配访问权限的系统里的一个标识。一个主体(subject)可能是一个用户、一个进程或者是一台机器。一个Subject可能涉及多个授权(一个网上银行密码和另一个电子邮件系统)。

一旦对subject进行了身份验证,则使用javax.security.auth.Subject由关联的标识或Principals填充。一个subject可能有多个principal。例如,一个人可能有一个name Principal(“John Doe”)和一个SSN Principal(“123-45-6789”)。

subject还可以拥有与安全性相关的属性,这些属性称为credentials凭据,敏感的credentials比如私钥使用Set privCredentials表示,共享非敏感credentials比如公钥使用Set pubCredentials表示。访问和修改不同的凭据集需要不同的权限(如下所述):

subject是使用这些构造函数创建的:

public Subject();

public Subject(boolean readOnly, Set principals, Set pubCredentials, Set privCredentials);

第一个构造函数创建了一个principal和credential为空(非空)的subject。第二个构造函数使用指定的principals和credentials创建subject。它还有一个布尔参数,可用于使subject为只读。在只读subject中,principals和credentials是不可变的。

subject的实例化一般在javax.security.auth.login.LoginContext类的login方法中。

1.2.Principal

这是个接口,全路径为java.security.Principal。

Principal就被用作在那些关联里的标识。也就是说,该Principal接口是一个能够被用作代表某个实体、公司或者登陆ID的抽象概念。一个Subject可能包含多个Principles。

当一个Subject认证成功,Principal将会关联到这个Subject。Principal表示Subject的身份表示,必须要实现 java.security.Principal 和 java.io.Serializable 接口。Subject section 描述了更新Principal关联到subject的方法。

1.3.Credential

并不是主要的Jaas代码,任何类可以表示为Credential,并需要实现Credential的两个接口Refreshable和Destroyable。

2.Authentication

Authentication就是身份认证,是对系统保护的第一道防线。其主要功能是鉴定请求的来源,只有满足特定身份的主体(用户或其他服务)才能访问。

2.1.Authentication框架

JAAS的authentication framework的设计虽然比较简单,但其很好的阐述了OCP(Open-Closed Principle)和DIP(Dependency Inversion Principle)的设计原则,可以说是教科书式的优秀设计。

身份认证的方式很多,JAAS实现了一个非常灵活的框架,如下图所示:

图中涉及的类有:

(1)javax.security.auth.login.LoginContext

(2)javax.security.auth.login.Configuration:告诉LoginContext具体哪些身份验证模块。

(3)javax.security.auth.spi.LoginModule:接口,定义认证登陆的方法。如果要实现我们自己的身份验证模块,则需要实现LoginModule接口。

(4)com.sun.security.auth.module.Krb5LoginModule:LoginModule的实现类,用于kerberos认证。

(5)org.apache.kafka.common.security.plain.PlainLoginModule:LoginModule的实现类,是kafka模块实现的,用于SASL_PLAINTEXT认证的登陆。

(6)org.apache.hadoop.security.HadoopLoginModule:LoginModule的实现类,hadoop的登陆模块。

2.2.LoginContext

通常,我们的应用程序增加身份验证功能的时候,只需要与LoginContext打交道。例如在我们的应用程序中只需要如下代码即可,剩下的事情JAAS会搞定。

LoginContext lc = null;
try {
    lc = new LoginContext("Login2");
    lc.login();
} catch(LoginException e) {
    ......
} 

定义了login的上下文信息。

2.3.Login Configuration

Login Configuration是提供给LoginContext的配置文件。可以通过以下两种方式来指定login configuration的位置:在java.security中指定、通过命令行指定。

2.3.1.java.security中指定login configuration

java.security文件位于JRE的lib/security目录下。通过在java.security中配置下面的属性,来指定login configuration:

login.configuration.provider={provider}
login.config.url.{n}={config url}

login.configuration.provider指定其它的provider class。默认是com.sun.security.auth.login.ConfigFile,从文件中读取配置信息。

login.config.url.n指定配置文件的具体位置,可以指定多个配置文件,通过整数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.3.2.命令行中指定login configuration

命令行参数为“java.security.auth.login.config”。例如:

java -Djava.security.auth.login.config=/tmp/sample_jaas.config sample/SampleAcn

配置文件内容必须满足给定的格式

2.3.3.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”对应LoginContext的名称,分别属于两个不同的配置项。

配置项的定义由类javax.security.auth.login.AppConfigurationEntry决定。包含三个属性:

(1)loginModuleName:LoginModule的全类名,这些类都实现了LoginModule接口。

(2)controlFlag:每个LoginModule的控制项,有四种有效值——Required、Requisite、Sufficient、Optinal。

(3)options:可选项,具体由LoginModule实现类决定,比如上面的“debug=true”。

当某个application配置有多个LoginModule时,那么这些LoginModule被调用的顺序和它们在配置文件中的定义的顺序保持一致。

如果标示为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就派上用场了。

2.4.LoginModule

JAAS实现的Authentication framework非常灵活,使得我们可以很容易扩展,实现我们自己的login module,作为plugin集成到framework中去。要实现定制的login module,需要实现LoginModule接口,主要是实现以下五个抽象方法。

public interface LoginModule {

    void initialize(Subject subject, CallbackHandler callbackHandler,
                    Map<String,?> sharedState,
                    Map<String,?> options);
    boolean login() throws LoginException;
    boolean commit() throws LoginException;
    boolean abort() throws LoginException;
    boolean logout() throws LoginException;
}

其中,initialize方法是用来初始化login module的,是被LoginContext调用的。该方法的几个参数值得注意。

(1)第一个参数subject是由上层application,或者LoginContext创建。如果上层应用程序在创建LoginContext的时候,没有传入subject,那么LoginContext会自己创建一个Subject的实例。不管下面有几个LoginModule,subject实例只会有一个。

(2)第二个参数callbackhandler是由上层应用程序创建的,可以为null。其主要用于LoginModule与上层应用程序交互,比如login module在进行身份验证时,可能会需要用户输入用户名和密码,这时就是通过这个callbackhandler来交互。

(3)第三个参数sharedState是用于多个login modules之间共享一些状态。

(4)第四个参数options是该login module特有的一些参数,定义在login configuration中。

这里主要是要理解两阶段的身份验证过程。如下图所示:

当上层应用调用LoginContext的login函数时,实际执行过程会经历两个阶段。

(1)第一个阶段是LoginContext调用login configuration中配置的每一个LoginModule的login方法。

(2)第二阶段则是调用每个LoginModule的commit方法或者abort方法。

如果在第一阶段中验证成功,则在第二阶段调用commit方法,否则调用abort方法。

在第一阶段中,会调用每个LoginModule的login方法,可能有的成功,有的失败。只要最终结果是成功,那么在第二阶段中就会调用每个LoginModule的commit方法。对于每个LoginModule来说,如果之前login成功了,那么commit方法就会将根据之前login时的状态信息生成Principal实例,并将其加入到subject中。

3.Authorization

只有认证没有授权是无法对权限进行细粒度管理的。下面对授权部分进行讲解。

3.1.SecurityManager

字面翻译为Java安全管理器。默认情况Java没有开启SecurityManager,程序拥有所有执行权限。如果开启SecurityManager,需要添加JVM参数:

-Djava.security.manager

打开这个参数之后我们会发现访问系统变量,访问文件等操作都会被阻止。这是因为默认的安全管理器不允许这些操作。我们需要配置policy文件,放开这些权限。

我们一般通过VM参数指定进程使用的policy文件,例如:

-Djava.security.policy==/path/to/xxx.policy

这里使用“==”来覆盖Java默认的配置。

policy文件用法在下节介绍。

3.2.Policy文件

policy文件用于配置权限。policy文件的格式如下:

grant [SignedBy "signer_names"] [,codebase "file:..."] [,Principal principal_class_name "principal_name"] {
    permission permission_class_name "target_name", "action";
    permission permission_class_name "target_name", "action";
    ...
}

grant关键字后可以跟3个片段:

(1)SignedBy:针对某个签名者赋予权限。可使用jarsigner xxx.jar signer_name为jar文件签名。要求有一个密钥库keystore,签名的时候需要(使用keytool命令创建)。

(2)codebase:用于为某个目录下的用户代码授权。

(3)Principal:用于为特定类型,特定name(Principal的getName方法决定)的Principal授权。

可以看到一个Policy文件是由各种Permission组成的。

3.3.Permission

Permission部分为赋予的具体权限。例如:

#赋予创建LoginContext的权限。
permission javax.security.auth.AuthPermission "createLoginContext"; 

#赋予读取系统变量"os.name"的权限(System.getProperty("os.name"))。
permission java.util.PropertyPermission "os.name", "read"; 

#赋予读取”c:/"下所有各级目录的权限。
permission java.io.FilePermission "c:/-", "read";

大家可能会问如何知道Java支持的权限类型,以及他们怎么配置。实际上所有的权限都对应一个Java类。它们都具有一个共同的父类java.security.Permission。我们查看他的子类,就可以知道有多少种权限。其中常用的几个为:

(1)AuthPermission:认证操作权限

(2)FilePermission:文件访问权限

(3)PropertyPermission:属性访问权限

(4)AllPermission:所有权限

(5)SocketPermission:网络通信权限

具体某种权限的target和action如何配置,可以参考对应子类的Java doc。