Java认证与授权 - JAAS

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

整个认证流程是否成功的判定标准:

  1. 仅当所有的Required和Requisite LoginModule成功,认证流程才成功。
  2. 如果配置了Sufficient LoginModule,并且它认证成功,要求在它之前所有的Required和Requisite LoginModule都成功,认证流程才算成功。
  3. 如果没有配置任何Required和Requisite LoginModule,Sufficient或Optional LoginModule至少成功任意一个,认证流程才算成功。

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必须重写equalshashCode方法。否则后面根据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) () -> {
        // ...
        // 需要权限才能执行的逻辑放在此处
        return null;
    }, null);

    try {
        // 登出用户
        // 在此之后subject不再具有principal的权限
        usernameLoginContext.logout();
    } catch (LoginException e) {
        e.printStackTrace();
        System.exit(-1);
    }

}
 
 

运行的时候千万不要忘记添加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(PrincipalgetName方法决定)的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) () -> {
    System.setProperty("demo", "demo value");
    System.out.println(System.getProperty("demo"));
    System.out.println("Success");
    return null;
}, null);
 
 

为了保证这段代码能够运行,我们编写一个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配置的权限。

本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。

你可能感兴趣的:(Java认证与授权 - JAAS)