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