Kerberos (Secure Network Authentication System,网络安全认证系统),是一种网络认证协议,其设计目标是通过密钥系统为 Client/Server 提供强大的认证服务。该认证过程的实现不依赖于主机操作系统的认证,无需基于的信任,不要求网络上所有主机的物理安全,并假定网络上传送的数据包可以被任意地读取、修改和插入数据。
SASL (Simple Authentication and Security Layer,简单授权和安全层),是一个互联网标准,它制定了一个授权协议,在 Client/Server 之间建立连接。SASL 定义了授权数据如何交换,但是并没有制定数据的内容。它是一个授权机制框架。
GSSAPI (Generic Security Services Application Program Interface,通用安全服务应用程序接口),也称 GSS-API,是程序访问安全服务的应用程序编程接口。GSSAPI 是 IETF 标准,用于解决当今使用的许多类似但不兼容的安全服务的问题。
GSSAPI 本身是一个独立的认证框架外,它同时也适配了 SASL,也就是说 GSSAPI 同时也是 SASL 规范下的一种认证机制,这就使得 SASL 可以通过 GSSAPI 间接支持 Kerberos,本文我们将使用 SASL/GSSAPI 指代两者。
Kerberos 的基本介绍和安装配置,可以参考 “Linux基础知识(16)- Kerberos (一) | Kerberos 安装配置”。
本文创建两个 Springboot 程序 Client 和 Server,演示通过 SASL/GSSAPI 实现 Kerberos 认证。
1. 系统环境
操作系统:Ubuntu 20.04
Java 版本:openjdk 11.0.18
本文 Kerberos 的客户端和服务端都安装在同一台主机上,主机名为 hadoop-master-vm,Springboot 程序也运行在 hadoop-master-vm 上。
2. 创建 Springboot 项目
Windows版本:Windows 10 Home (20H2)
IntelliJ IDEA:Community Edition for Windows 2020.1.4
Apache Maven:3.8.1
注:Spring 开发环境的搭建,可以参考 “ Spring基础知识(1)- Spring简介、Spring体系结构和开发环境配置 ”。
1) 运行 IDEA 创建一个空项目
点击菜单 New 创建 Project:
New Project -> Empty Project -> Next
Project Name: SpringbootExample24
Project location: 指定一个目录,比如 D:\Workshop\idea\SpringbootExample24
-> Finish
2) 添加 Server 模块 (Module)
点击菜单 File -> New 创建 Module:
New Module -> Maven -> Project Type: Maven -> Project SDK: 1.8 -> Check "Create from archtype" -> select "org.apache.maven.archtypes:maven-archtype-quickstart" -> Next
Name: Server
Location: D:\Workshop\idea\SpringbootExample24\Server
GroupId: com.example
ArtifactId: Server
Version: 1.0-SNAPSHOT
-> Next
Maven home directory: D:/Apps/Java/apache-maven-3.8.1 (本文的配置路径,下同)
User settings file: D:\Apps\Java\apache-maven-3.8.1\conf\settings.xml
Local repository: D:\Apps\Java\maven-repository
-> Finish
3) 添加 Client 模块 (Module)
点击菜单 File -> New 创建 Module:
New Module -> Maven -> Project Type: Maven -> Project SDK: 1.8 -> Check "Create from archtype" -> select "org.apache.maven.archtypes:maven-archtype-quickstart" -> Next
Name: Client
Location: D:\Workshop\idea\SpringbootExample24\Client
GroupId: com.example
ArtifactId: Client
Version: 1.0-SNAPSHOT
-> Next
Maven home directory: D:/Apps/Java/apache-maven-3.8.1
User settings file: D:\Apps\Java\apache-maven-3.8.1\conf\settings.xml
Local repository: D:\Apps\Java\maven-repository
-> Finish
3. Server 模块 (Module)
1) 修改 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>Server</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Server</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>Server</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.example.ServerApp</mainClass>
<layout>JAR</layout>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
...
</plugins>
</pluginManagement>
</build>
</project>
在IDE中项目列表 -> Server -> 点击鼠标右键 -> Maven -> Reload Project
本文选择了 spring-boot-starter-parent 2.6.6 相关依赖包,spring-boot-starter 和 spring-boot-starter-test 的版本由 spring-boot-starter-parent 控制。
2) 配置文件
添加 src/main/resources/application.properties 文件,内容如下:
spring.main.banner-mode=off
添加 src/main/resources/krb5_testsrc.keytab 文件,这里使用 “Linux基础知识(17)- Kerberos (二) | krb5 API 的 C 程序示例” 里创建的 krb5_testsrc.keytab 文件。
3) 添加 src/main/java/com/example/ServerApp.java 文件
package com.example;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.security.Principal;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.io.*;
import org.ietf.jgss.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ServerApp {
private static String strRealm = "hadoop.com";
private static String strPrincipalServer = "testsrv/hadoop-master-vm";
private static String strKeytab = "krb5_testsrv.keytab";
private static String strKrb5MechOid = "1.2.840.113554.1.2.2";
private static String strSpnegoOid = "1.3.6.1.5.5.2";
private static String strKdcServer = "hadoop-master-vm";
private static String strServerHost = "hadoop-master-vm";
private static int iServerPort = 9988;
public static void main(String[] args) {
SpringApplication.run(ServerApp.class, args);
System.setProperty("java.security.krb5.realm", strRealm);
System.setProperty("java.security.krb5.kdc", strKdcServer);
javax.security.auth.login.Configuration config = new javax.security.auth.login.Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
HashMap<String, Object> options = new HashMap<String, Object>() {
{
put("useKeyTab", "true");
put("keyTab", strKeytab);
put("principal", strPrincipalServer);
put("doNotPrompt", "true");
put("storeKey", "true");
put("isInitiator", "true");
put("debug", "true");
}
};
return new AppConfigurationEntry[]{
new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options)
};
}
};
try {
System.out.println("Server Krb5Login ... ");
final Set<Principal> principalSet = new HashSet<Principal>(1);
principalSet.add(new KerberosPrincipal(strPrincipalServer));
Subject subject = new Subject(false, principalSet, new HashSet<Object>(), new HashSet<Object>());
LoginContext loginContext = new LoginContext(strPrincipalServer, subject, null, config);
loginContext.login();
Subject serviceSubject = loginContext.getSubject();
System.out.println("Subject.doAs() ... ");
GSSCredential gssCredential = Subject.doAs(serviceSubject, new PrivilegedExceptionAction<GSSCredential>() {
final Oid krb5MechOid = new Oid(strKrb5MechOid);
final Oid spnegoOid = new Oid(strSpnegoOid);
@Override
public GSSCredential run() throws Exception {
GSSManager manager = GSSManager.getInstance();
GSSName gssServerName = manager.createName(strPrincipalServer, GSSName.NT_USER_NAME);
GSSCredential serverGssCreds = manager.createCredential(gssServerName,
GSSCredential.INDEFINITE_LIFETIME,
krb5MechOid, // spnegoOid or krb5MechOid, consistent with the GSSContext settings on the client
GSSCredential.ACCEPT_ONLY);
return serverGssCreds;
}
});
if (gssCredential == null) {
System.out.println("gssCredential == null");
return;
}
//
ServerSocket serverSocket = new ServerSocket(iServerPort);
OUTER: while (true) {
System.out.println("serverSocket.accept() ...");
Socket connSocket = serverSocket.accept();
DataInputStream inStream = new DataInputStream(connSocket.getInputStream());
DataOutputStream outStream = new DataOutputStream(connSocket.getOutputStream());
System.out.println("client:" + connSocket.getInetAddress());
GSSManager manager = GSSManager.getInstance();
GSSName gssServerName = manager.createName(strPrincipalServer, GSSName.NT_USER_NAME);
GSSContext gssContext = manager.createContext(gssServerName,
null,
gssCredential,
GSSContext.DEFAULT_LIFETIME);
// Do the context establish loop
byte[] token = null;
while (!gssContext.isEstablished()) {
token = new byte[inStream.readInt()];
inStream.readFully(token);
byte[] decodedToken = Base64.getDecoder().decode(token);
System.out.println("gssContext.acceptSecContext(): decodedToken.length == " + decodedToken.length);
token = gssContext.acceptSecContext(decodedToken, 0, decodedToken.length);
// Send a token to the peer if one was generated by
// acceptSecContext
if (token != null) {
System.out.println("outStream.writeInt(): token.length == " + token.length);
outStream.writeInt(token.length);
outStream.write(token);
outStream.flush();
}
}
System.out.println("gssContext.isEstablished() == " + gssContext.isEstablished());
System.out.println("client: " + gssContext.getSrcName());
System.out.println("server: " + gssContext.getTargName());
if (gssContext.getMutualAuthState())
System.out.println("Mutual authentication is enable!");
// Normal message loop
int done = 0;
int count = 0;
byte[] data = new byte[256];
do {
try {
count = inStream.readInt();
inStream.read(data);
} catch (EOFException e) {
System.out.println("EOFException(): client exit or network broken");
break;
}
if (count <= 0) {
if (count < 0) {
System.out.println("in.read(): error -> count == " + count);
break;
}
done = 1;
System.out.println("in.read(): done == " + done);
}
// Shutdown from client
String str = new String(data);
if ("shutdown".equals(str.substring(0, 8))) {
System.out.println(str);
connSocket.close();
gssContext.dispose();
break OUTER;
}
System.out.println("in.read(): from client -> " + str);
Thread.sleep(2000);
if (done <= 0) {
outStream.writeInt(str.length());
outStream.write(data);
outStream.flush();
System.out.println("outStream.write(): to client -> " + str);
}
} while (done <= 0);
/*
// Security message channel
MessageProp prop = new MessageProp(0, false);
token = new byte[inStream.readInt()];
System.out.println("Will read token of size " + token.length);
inStream.readFully(token);
byte[] bytes = gssContext.unwrap(token, 0, token.length, prop);
String str = new String(bytes);
System.out.println("Received data \"" + str + "\" of length " + str.length());
System.out.println("Confidentiality applied: " + prop.getPrivacy());
prop.setQOP(0);
token = gssContext.getMIC(bytes, 0, bytes.length, prop);
System.out.println("Will send MIC token of size " + token.length);
outStream.writeInt(token.length);
outStream.write(token);
outStream.flush();
*/
System.out.println("connSocket.close()");
connSocket.close();
gssContext.dispose();
}
serverSocket.close();
} catch (LoginException e) {
e.printStackTrace();
} catch (PrivilegedActionException e) {
e.printStackTrace();
} catch (GSSException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
4. Client 模块 (Module)
1) 修改 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>Client</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Client</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>Client</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.example.ClientApp</mainClass>
<layout>JAR</layout>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
...
</plugins>
</pluginManagement>
</build>
</project>
在IDE中项目列表 -> Server -> 点击鼠标右键 -> Maven -> Reload Project
本文选择了 spring-boot-starter-parent 2.6.6 相关依赖包,spring-boot-starter 和 spring-boot-starter-test 的版本由 spring-boot-starter-parent 控制。
2) 配置文件
添加 src/main/resources/application.properties 文件,内容如下:
spring.main.banner-mode=off
添加 src/main/resources/krb5_testcli.keytab 文件,这里使用 “Linux基础知识(17)- Kerberos (二) | krb5 API 的 C 程序示例” 里创建的 krb5_testcli.keytab 文件。
3) 添加 src/main/java/com/example/ClientApp.java 文件
package com.example;
import java.net.Socket;
import java.util.Scanner;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.util.Base64;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.security.Principal;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.io.*;
import org.ietf.jgss.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ClientApp {
private static String strRealm = "hadoop.com";
private static String strPrincipalClient = "testcli";
private static String strPrincipalServer = "testsrv/hadoop-master-vm";
private static String strKeytab = "krb5_testcli.keytab";
private static String strSpnegoOid = "1.3.6.1.5.5.2";
private static String strKrb5MechOid = "1.2.840.113554.1.2.2";
private static String strKdcServer = "hadoop-master-vm";
private static String strServerHost = "hadoop-master-vm";
private static int iServerPort = 9988;
public static void main(String[] args) {
SpringApplication.run(ClientApp.class, args);
// Config
System.setProperty("java.security.krb5.realm", strRealm);
System.setProperty("java.security.krb5.kdc", strKdcServer);
javax.security.auth.login.Configuration config = new javax.security.auth.login.Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
HashMap<String, Object> options = new HashMap<String, Object>() {
{
put("useKeyTab", "true");
put("keyTab", strKeytab);
put("principal", strPrincipalClient);
put("doNotPrompt", "true");
put("storeKey", "true");
put("isInitiator", "true");
put("debug", "true");
}
};
return new AppConfigurationEntry[]{
new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options)
};
}
};
try {
System.out.println("Client Krb5Login ... ");
final Set<Principal> principalSet = new HashSet<Principal>(1);
principalSet.add(new KerberosPrincipal(strPrincipalClient));
Subject subject = new Subject(false, principalSet, new HashSet<Object>(), new HashSet<Object>());
LoginContext loginContext = new LoginContext(strPrincipalClient, subject, null, config);
loginContext.login();
Subject serviceSubject = loginContext.getSubject();
System.out.println("Subject.doAs() ... ");
GSSContext gssContext = Subject.doAs(serviceSubject, new PrivilegedExceptionAction<GSSContext>() {
final Oid krb5MechOid = new Oid(strKrb5MechOid); // Kerberos authentication
final Oid spnegoOid = new Oid(strSpnegoOid); // SPNEGO authentication
@Override
public GSSContext run() throws Exception {
GSSManager manager = GSSManager.getInstance();
//
GSSName gssClientName = manager.createName(strPrincipalClient, GSSName.NT_USER_NAME);
GSSCredential clientGssCreds = manager.createCredential(gssClientName,
GSSCredential.INDEFINITE_LIFETIME,
krb5MechOid,
GSSCredential.INITIATE_ONLY);
// GSS ticket or token
GSSName gssServerName = manager.createName(strPrincipalServer, GSSName.NT_USER_NAME);
GSSContext context = manager.createContext(gssServerName,
null, // spnegoOid, krb5MechOid or null (null equals Kerberos authentication)
clientGssCreds,
GSSContext.DEFAULT_LIFETIME);
return context;
}
});
if (gssContext == null) {
System.out.println("gssContext == null");
return;
}
gssContext.requestCredDeleg(true);
gssContext.requestMutualAuth(true); // Mutual authentication
gssContext.requestConf(true); // Will use confidentiality later
gssContext.requestInteg(true); // Will use integrity later
// Connect to server
Socket clientSocket = new Socket(strServerHost, iServerPort);
DataInputStream inStream = new DataInputStream(clientSocket.getInputStream());
DataOutputStream outStream = new DataOutputStream(clientSocket.getOutputStream());
System.out.println("Connected to server: " + clientSocket.getInetAddress());
// Do the context loop
byte[] token = new byte[0];
while (!gssContext.isEstablished()) {
token = gssContext.initSecContext(token, 0, token.length);
// Send a token to the server if one was generated by
// initSecContext
if (token != null) {
byte[] encodedToken = Base64.getEncoder().encode(token);
System.out.println("outStream.writeInt(): encodedToken.length == " + encodedToken.length);
outStream.writeInt(encodedToken.length);
outStream.write(encodedToken);
outStream.flush();
}
// If the client is done with context establishment
// then there will be no more tokens to read in this loop
if (!gssContext.isEstablished()) {
token = new byte[inStream.readInt()];
System.out.println("inStream.writeInt(): token.length == " + token.length);
inStream.readFully(token);
}
}
System.out.println("gssContext.isEstablished() == " + gssContext.isEstablished());
System.out.println("client: " + gssContext.getSrcName());
System.out.println("server: " + gssContext.getTargName());
if (gssContext.getMutualAuthState())
System.out.println("Mutual authentication is enable!");
/*
// Security message channel
byte[] messageBytes = "Hello There!\0".getBytes();
MessageProp prop = new MessageProp(0, true);
token = gssContext.wrap(messageBytes, 0, messageBytes.length, prop);
System.out.println("Will send wrap token of size " + token.length);
outStream.writeInt(token.length);
outStream.write(token);
outStream.flush();
token = new byte[inStream.readInt()];
System.out.println("Will read token of size " + token.length);
inStream.readFully(token);
gssContext.verifyMIC(token, 0, token.length, messageBytes,
0, messageBytes.length, prop);
System.out.println("Verified received MIC for message.");
*/
// Normal message loop
Scanner sc = new Scanner(System.in);
System.out.print("Input> ");
String str = sc.next();
byte[] data = new byte[256];
int count = 0;
while (!str.equals("exit") && !str.equals("quit")) {
outStream.writeInt(str.length());
outStream.write(str.getBytes());
if (str.equals("shutdown")) break;
//
InputStream in = clientSocket.getInputStream();
count = inStream.readInt();
inStream.read(data);
if (count > 0) {
System.out.println("in.read(): from server -> " + new String(data));
} else {
System.out.println("in.read(): count == " + count);
break;
}
System.out.print("Input> ");
str = sc.next();
}
System.out.println("Exiting ...");
sc.close();
clientSocket.close();
gssContext.dispose();
} catch (LoginException e) {
e.printStackTrace();
} catch (PrivilegedActionException e) {
e.printStackTrace();
} catch (GSSException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
5. 运行
1) 打包 Jar
菜单 View -> Tool Windows -> Maven -> Client -> Lifecycle -> Clean & Package
Client 模块 (Module) 的 Jar 包生成在 Client 模块下的目录 target/ 里
Client.jar
Client.jar.original
注:Client.jar 包含依赖包,可以直接运行。 Client.jar.original 里不包含依赖的包(要手动配置依赖环境),运行前要把文件名上的 “.original” 去掉。
菜单 View -> Tool Windows -> Maven -> Server -> Lifecycle -> Clean & Package
Server 模块 (Module) 的 Jar 包生成在 Server 模块下的目录 target/ 里
Server.jar
Server.jar.original
2) 运行 Jar
把 Server.jar 和 Client.jar 复制到主机 hadoop-master-vm 上,在两个控制台分别运行这两个 Jar 包,运行命令如下。
控制台1:
$ java -jar Server.jar
...
Server Krb5Login ...
Debug is true storeKey true useTicketCache false useKeyTab true doNotPrompt true ticketCache is null isInitiator true KeyTab is krb5_testsrv.keytab refreshKrb5Config is false principal is testsrv/hadoop-master-vm tryFirstPass is false useFirstPass is false storePass is false clearPass is false
principal is testsrv/hadoop-master-vm@hadoop.com
Will use keytab
Commit Succeeded
Subject.doAs() ...
serverSocket.accept() ...
client: /192.168.1.5
gssContext.acceptSecContext(): token.length == 1600
outStream.writeInt(): token.length == 108
gssContext.isEstablished() == true
client: testcli@hadoop.com
server: testsrv/hadoop-master-vm
Mutual authentication is enable!
控制台2:
$ java -jar Client.jar
Client Krb5Login ...
Debug is true storeKey true useTicketCache false useKeyTab true doNotPrompt true ticketCache is null isInitiator true KeyTab is krb5_testcli.keytab refreshKrb5Config is false principal is testcli tryFirstPass is false useFirstPass is false storePass is false clearPass is false
principal is testcli@hadoop.com
Will use keytab
Commit Succeeded
Subject.doAs() ...
Connected to server: hadoop-master-vm/192.168.1.5
outStream.writeInt(): encodedToken.length == 1600
inStream.writeInt(): token.length == 108
gssContext.isEstablished() == true
client: testcli
server: testsrv/hadoop-master-vm
Mutual authentication is enable!
Input> test
注: 输入文本 “test”,按回车键,Server 收到 “test” 后会发回 Client。输入 “exit” 或 “quit”,可以退出 Client 程序,Server 程序继续处于 accept 状态。输入 “shutdown”,Server 和 Client 都退出。
参考来源:https://docs.oracle.com/javase/8/docs/technotes/guides/security/jgss/tutorials/BasicClientServer.html