JAAS简介
JAAS全称为 Java Authentication Authorization Service,中文含义即Java认证和授权服务。使用可插入方式将认证和授权逻辑和应用程序分离开。
Authentication
术语解释
JAAS配置文件
独立于用户程序存在,用户程序可以指定使用哪个JAAS配置文件。JAAS配置通过无侵入用户代码的方式指定认证逻辑入口。
LoginModule
即登录模块。不同的LoginModule
对应了不同的认证方式。例如Krb5LoginModule
使用Kerberos作为认证方式,FileLoginModule
使用外部保存的密码文件,通过用户名密码方式登录等。
Subject
认证的主体,可以代表一个人,一个角色一个进程等。认证成功之后可以从LoginContext
获取Subject
,代表一种身份,用户可以通过这个身份执行一些需要权限才能运行的逻辑。
Principal
可认为是一种权限。一个Subject
可以包含多个Principal
。用户认证成功之后,把授予的Principal
加入到和该用户关联的Subject
中,该用户便具有了这个Principal
的权限。
JAAS文件
JAAS文件的格式:
{
;
;
};
上面是一个JAAS entry的配置格式。一个entry包含开头的entry name和大括号内部的entry body。
entry中包含三个要素:LoginModule
,flag和options。接下来分别讲解。
下面举一个例子。常见的Kerberos认证的JAAS文件如下:
Client {
com.sun.security.auth.module.Krb5LoginModule required
useKeyTab=true
storeKey=true
serviceName="xxx"
keyTab="/etc/security/keytabs/xxx.keytab"
principal="xxx/[email protected]"
userTicketCache=true;
};
JAAS文件官方链接:JAAS Login Configuration File。
LoginModule
LoginModule
需要配置为全限定名。LoginModule
的类型直接决定了该entry需要通过何种方式认证。
一个entry中可以配置多个LoginModule
。如果配置了多个LoginModule
,认证流程会按照顺序依次执行各个LoginModule
。但是有些flag的值会中断认证流程,后面分析。
flag
flag是枚举类型(LoginModuleControlFlag
),它的值和含义如下列表所示:
- Required:要求
LoginModule
成功。无论成功与否,认证流程都会继续走后面的LoginModule
。 - Requisite:要求
LoginModule
成功。如果成功,认证流程会继续走后面的LoginModule
,如果失败,认证流程终止。 - Sufficient:不要求
LoginModule
成功。如果成功,认证流程终止返回应用,如果失败认证流程会继续走后面的LoginModule
。 - Optional:不要求
LoginModule
成功。无论成功与否,认证流程都会继续走后面的LoginModule
。
整个认证流程是否成功的判定标准:
- 仅当所有的Required和Requisite
LoginModule
成功,认证流程才成功。 - 如果配置了Sufficient
LoginModule
,并且它认证成功,要求在它之前所有的Required和RequisiteLoginModule
都成功,认证流程才算成功。 - 如果没有配置任何Required和Requisite
LoginModule
,Sufficient或OptionalLoginModule
至少成功任意一个,认证流程才算成功。
options
options为LoginModule
接收的认证选项。格式为0或多个key=value
,常用于传递认证附属参数。在LoginModule
中使用一个Map来接收并处理这些参数。
指定应用使用的JAAS配置文件
Java通过配置VM参数的方式,指定应用使用哪个JAAS配置文件。VM参数配置方法如下:
-Djava.security.auth.login.config==/path/to/jaas.conf
注意:这里使用"=="来覆盖JVM默认的jaas配置文件
在应用中获取JAAS配置信息
可以在应用中通过Java代码方式获取JAAS配置文件内容。代码如下:
Configuration.getConfiguration().getAppConfigurationEntry("entryName");
注意:如果启用了
SecurityManager
,此步骤默认无权限执行,需要配置policy。本篇后面授权章节有讲解。
返回的JAAS配置文件中的每一个entry都被包装为AppConfigurationEntry
对象。它包含如下3个成员变量,正好对应JAAS entry中的3部分。
private String loginModuleName;
private LoginModuleControlFlag controlFlag;
private Map options;
JAAS配置文件的官方讲解参见:Class Configuration。
自定义LoginModule
这一节我们编写一个简单的认证模块,如果用户提供特定的用户名和密码就可以通过认证。
首先编写一个Principal
,该principal仅包含用户名信息。
public class UsernamePrincipal implements Principal {
private String name;
public UsernamePrincipal(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UsernamePrincipal that = (UsernamePrincipal) o;
return Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
注意:
Principal
必须重写equals
和hashCode
方法。否则后面根据principal授权的时候,判断是否是同一个principal会出问题,导致无法授权成功。
接下来是认证模块,解析在代码注释中。如下:
public class UsernameLoginModule implements LoginModule {
private Subject subject;
private CallbackHandler callbackHandler;
private String username;
private String password;
private UsernamePrincipal principal;
private boolean isLoggedIn = false;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
// 认证主体
this.subject = subject;
// callbackHandler负责和用户应用交互,传递认证参数
// 这里我们不使用这种方式
this.callbackHandler = callbackHandler;
// options为JAAS options的各个参数,封装为map形式
// 获取参数中的username和password
this.username = (String) options.get("username");
this.password = (String) options.get("password");
}
// 自定义认证逻辑,很简单不再描述
private boolean canLogin(String username, String password) {
return "paul".equals(username) && "123456".equals(password);
}
// 登录调用逻辑的入口
// 返回值含义为是否成功
@Override
public boolean login() throws LoginException {
if (canLogin(this.username, this.password)) {
// 如果登陆成功,创建出UsernamePrincipal
this.principal = new UsernamePrincipal(username);
// 设置状态为已登录
isLoggedIn = true;
// 返回登陆成功
return true;
}
return false;
}
// 这里是登录成功之后的逻辑
@Override
public boolean commit() throws LoginException {
if (subject == null) {
return false;
}
if (principal == null) {
return false;
}
if (!isLoggedIn) {
return false;
}
// 将新创建的principal加入到subject中
// 这样subject就拥有了principal权限
subject.getPrincipals().add(principal);
return true;
}
// 登录终止逻辑
@Override
public boolean abort() throws LoginException {
// 复用登出方法
return logout();
}
// 登出逻辑
@Override
public boolean logout() throws LoginException {
if (!isLoggedIn) {
return false;
}
// 从subject中移除principal
if (subject != null && principal != null) {
subject.getPrincipals().remove(principal);
}
principal = null;
subject = null;
isLoggedIn = false;
return true;
}
}
接下来是JAAS配置文件:
UsernameLogin {
com.paultech.UsernameLoginModule required username=paul password="123456";
};
我们将用户名和密码通过配置文件传入LoginModule
。
最后是用户认证主程序:
public static void main(String[] args) {
LoginContext usernameLoginContext = null;
try {
// 使用UsernameLogin这个entry name进行认证
usernameLoginContext = new LoginContext("UsernameLogin");
// 执行登录
usernameLoginContext.login();
} catch (LoginException e) {
// 如果登录失败会执行这里
e.printStackTrace();
System.exit(-1);
}
// 获取认证主体subject
// 已包含新创建出的principal
Subject subject = usernameLoginContext.getSubject();
Subject.doAsPrivileged(subject, (PrivilegedAction
运行的时候千万不要忘记添加VM参数:
-Djava.security.auth.login.config==/path/to/jaas.conf
Authorization
只有认证没有授权是无法对权限进行细粒度管理的。此章节是授权部分的讲解。
SecurityManager
字面翻译为Java安全管理器。默认情况Java没有开启SecurityManager
,程序拥有所有执行权限。
开启SecurityManager
的方法同样是添加VM参数:
-Djava.security.manager
打开这个参数之后我们会发现访问系统变量,访问文件等操作都会被阻止。这是因为默认的安全管理器不允许这些操作。我们需要配置policy文件,放开这些权限。policy文件用法在下节介绍。
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个片段:
- SignedBy:针对某个签名者赋予权限。可使用
jarsigner xxx.jar signer_name
为jar文件签名。要求有一个密钥库keystore,签名的时候需要(使用keytool命令创建)。 - codebase:用于为某个目录下的用户代码授权。
- Principal:用于为特定类型,特定name(
Principal
的getName
方法决定)的Principal授权。
接下来说下Permission
。Permission部分为赋予的具体权限。例如:
permission javax.security.auth.AuthPermission "createLoginContext";
赋予创建LoginContext
的权限。
permission java.util.PropertyPermission "os.name", "read";
赋予读取系统变量"os.name"的权限(System.getProperty("os.name")
)。
permission java.io.FilePermission "c:/-", "read";
赋予读取"c:/"下所有各级目录的权限。
大家可能会问如何知道Java支持的权限类型,以及他们怎么配置。实际上所有的权限都对应一个Java类。它们都具有一个共同的父类java.security.Permission
。我们查看他的子类,就可以知道有多少种权限。其中常用的几个为:
- AuthPermission:认证操作权限
- FilePermission:文件访问权限
- PropertyPermission:属性访问权限
- AllPermission:所有权限
- SocketPermission:网络通信权限
具体某种权限的target和action如何配置,可以参考对应子类的Java doc。此处不再赘述。
图形界面编辑policy文件
JDK的bin
目录有policytool
,运行它可以打开一个图形化policy文件编辑工具。相比文本方式编辑方便了许多。
指定进程使用的policy文件
同样使用VM参数方式指定。
-Djava.security.policy==/path/to/xxx.policy
和JAAS配置文件一样,这里使用"=="来覆盖Java默认的配置。
结合认证与授权
我们需要清楚如何使用特定Subject
来执行特权代码。Java使用Subject.doAsPrivileged()
方法执行特权代码。一个例子如下:
Subject.doAsPrivileged(subject, (PrivilegedAction
为了保证这段代码能够运行,我们编写一个policy,如下所示。为了描述清楚,直接以注释的形式在配置文件中说明权限的含义,实际上不确定policy配置文件是否支持注释,真正使用的时候先将它们删除。
// 为任何客体授予权限
grant {
// 授予创建LoginContext的权限
permission javax.security.auth.AuthPermission "createLoginContext";
// 由于login的时候需要将新创建出的principal加入subject,需要有这个权限
permission javax.security.auth.AuthPermission "modifyPrincipals";
// 赋予执行特权代码的权限
permission javax.security.auth.AuthPermission "doAsPrivileged";
// 赋予在代码中获取JAAS配置的权限
permission javax.security.auth.AuthPermission "getLoginConfiguration";
// 样例
// 读取环境变量的权限
permission java.util.PropertyPermission "os.name", "read";
permission java.util.PropertyPermission "os.arch", "read";
// 读写环境变量的权限
permission java.util.PropertyPermission "jboss.modules.system.pkgs", "read,write";
// 读取c盘和d盘各级目录所有文件的权限
permission java.io.FilePermission "c:/-", "read";
permission java.io.FilePermission "d:/-", "read";
};
// 为name为paul的类型为com.paultech.UsernamePrincipal授予权限
// 只有subject中的principal包含上面所述的principal时,doAsPrivileged方法才具有这里的权限
// 为subject添加principal的逻辑在LoginModule的commit方法中完成
grant principal com.paultech.UsernamePrincipal "paul" {
// 赋予系统变量demo的读写权限
permission java.util.PropertyPermission "demo", "read,write";
};
配置了这个policy文件之后,我们使用上一章认证的代码认证"paul"用户后,subject就拥有了"paul"这个com.paultech.UsernamePrincipal
配置的权限。
本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。