基于Kerberos认证的TCP通信

1 前言

kerberos是一种安全协议,它涉及到三个部分:KDC、 Server端 与 Client端的代码。对于Kerberos 的整个认证过程,有很多介绍。但是对于如何在一个已有TCP程序中进行kerberos认证,一直不知道如何使用。前段时间在oracle的文件中,找到一个server与client的样例,结合这个用例,将它进行改写,便于理解。
前置说明:

  1. KDC需要保证能正常运行
  2. 有对应的的princal, 如在我们的例子中,就需要保证admin与lch这两个princal必须存在

2 代码介绍

2.1 服务端代码说明

package com.test.gssapi.sample1;

import org.ietf.jgss.*;

import java.io.*;
import java.net.Socket;
import java.net.ServerSocket;


public class TestServer {
    
    

    private int localPort;
    private ServerSocket ss;
    private boolean iskerberos;
    private Socket socket = null;
    private GSSContext context = null;


    public TestServer(int port) {
        this.localPort = port;
        this.iskerberos = false;
    }
    public TestServer(int port, boolean iskerberos) {
        this.localPort = port;
        this.iskerberos = iskerberos;
    }

    public void receive() throws IOException, GSSException {
        this.ss =  new ServerSocket(localPort);
        while(true) {
            socket = ss.accept();
            DataInputStream inStream =  new DataInputStream(socket.getInputStream());
            DataOutputStream outStream =  new DataOutputStream(socket.getOutputStream());
            this.initKerberos(inStream, outStream);

            int length = inStream.readInt();
            byte[] token = new byte[length];

            token = new byte[length];
            System.out.println("Will read token of size " + token.length);
            inStream.readFully(token);
            String s = new String(token);
            System.out.println(s);
            //1. 发送回去的消息
            byte[] token1 = "Receive Client Message".getBytes();
            outStream.writeInt(token1.length);
            outStream.write(token1);
            outStream.flush();      

            this.destroy();
        }
    }


    private void initKerberos( DataInputStream inStream, DataOutputStream outStream) throws GSSException, IOException {
        if(!this.iskerberos) {
            return;
        }       
        GSSManager manager = GSSManager.getInstance();
        context = manager.createContext((GSSCredential) null);
        byte[] token = null;

        while (!context.isEstablished()) {

            token = new byte[inStream.readInt()];
            System.out.println(
                    "Will read input token of size " + token.length + " for processing by acceptSecContext");
            inStream.readFully(token);


            token = context.acceptSecContext(token, 0, token.length);

            // Send a token to the peer if one was generated by
            // acceptSecContext
            if (token != null) {
                System.out.println("Will send token of size " + token.length + " from acceptSecContext.");
                outStream.writeInt(token.length);
                outStream.write(token);
                outStream.flush();
            }
        }

        System.out.print("Context Established! ");
        System.out.println("Client is " + context.getSrcName());
        System.out.println("Server is " + context.getTargName());
    }

    private void destroy() {
        if(this.socket != null) {
            try {
                this.socket.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            this.socket = null;
        }
        if(this.context != null) {
            try {
                this.context.dispose();
            } catch (GSSException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            this.context = null;

        }
    }


    public static void main(String[] args) throws IOException, GSSException {

        int localPort = Integer.parseInt(args[0]);
        boolean iskerberos = false;
        if(args.length >= 2) {
            iskerberos  = Boolean.parseBoolean(args[1]);
        }
        TestServer server = new TestServer(localPort, iskerberos);
        server.receive();
    }


}

2.2 客户端代码

package com.test.gssapi.sample1;


import org.ietf.jgss.*;

import java.net.Socket;
import java.net.UnknownHostException;
import java.io.IOException;
import java.io.DataInputStream;
import java.io.DataOutputStream;


public class TestClient {
    
    

    private String srvPrincal;
    private String srvIP;
    private int srvPort;
    private boolean isKerberos;
    private Socket socket;
    private DataInputStream inStream;
    private DataOutputStream outStream;
    private GSSContext context;

    public TestClient(String srvPrincal, String srvIp, int srvPort, boolean iskerberos) {
        this.srvPrincal = srvPrincal;
        this.srvIP = srvIp;
        this.srvPort = srvPort;
        this.context = null;
        this.isKerberos = iskerberos;
        try {
            this.initSocket();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public TestClient(String srvPrincal, String srvIp, int srvPort) {
        this.srvPrincal = srvPrincal;
        this.srvIP = srvIp;
        this.srvPort = srvPort;
        this.context = null;
        this.isKerberos = false;
        try {
            this.initSocket();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void destroy() throws IOException, GSSException {
        if (this.inStream != null) {
            this.inStream.close();
        }
        if (this.outStream != null) {
            this.outStream.close();
        }
        if (this.socket != null) {
            this.socket.close();
        }
        if (this.context != null) {
            this.context.dispose();
        }
    }

    private void initSocket() throws UnknownHostException, IOException {
        this.socket = new Socket(srvIP, srvPort);
        this.inStream = new DataInputStream(socket.getInputStream());
        this.outStream = new DataOutputStream(socket.getOutputStream());
        System.out.println("Connected to server: " + this.socket.getInetAddress());
    }

    private void initKerberos() throws GSSException, IOException {
        if (!isKerberos) {
            return;
        }

        /**
         * Oid用来表示GSS-API这种机制。 这个值是协议中定义的(RFC 1964),这一个部分是固定的
         */
        Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");

        GSSManager manager = GSSManager.getInstance();

        /*
         * Create a GSSName out of the server's name. The null indicates that this
         * application does not wish to make any claims about the syntax of this name
         * and that the underlying mechanism should try to parse it as per whatever
         * default syntax it chooses.
         */
        /**
         * 创建一个GSSName,需要注意的是第一个参数srvPrincal,服务器收到消息后,会与自己的princal进行比较,
         * 如果不正确,就会抛出异常.
         */
        GSSName serverName = manager.createName(srvPrincal, null);

        /*
         * Create a GSSContext for mutual authentication with the server. - serverName
         * is the GSSName that represents the server. - krb5Oid is the Oid that
         * represents the mechanism to use. The client chooses the mechanism to use. -
         * null is passed in for client credentials - DEFAULT_LIFETIME lets the
         * mechanism decide how long the context can remain valid. Note: Passing in null
         * for the credentials asks GSS-API to use the default credentials. This means
         * that the mechanism will look among the credentials stored in the current
         * Subject to find the right kind of credentials that it needs.
         */
        System.out.println("init kerberos .. 1");
        GSSContext context = manager.createContext(serverName, krb5Oid, null, GSSContext.DEFAULT_LIFETIME);

        // Set the desired optional features on the context. The client
        // chooses these options.

        context.requestMutualAuth(true); // Mutual authentication
        context.requestConf(true); // Will use confidentiality later
        context.requestInteg(true); // Will use integrity later
        System.out.println("init kerberos .. 2");

        // Do the context eastablishment loop

        byte[] token = new byte[0];

        while (!context.isEstablished()) {
            // token is ignored on the first call
            // 这里进行kerberos认证,即调用JaaS的代码进行认证。认证完成后,返回
            // 这里我们可以把它GSS-api将JaaS的代码集成了。kerberos信息的确认是在此函数中进行的
            token = context.initSecContext(token, 0, token.length);

            // Send a token to the server if one was generated by
            // initSecContext
            if (token != null) {

                System.out.println("Will send token of size " + token.length + " from initSecContext.");
                outStream.writeInt(token.length);
                outStream.write(token);
                outStream.flush();
            }

            // If the client is done with context establishment
            // then there will be no more tokens to read in this loop
            if (!context.isEstablished()) {
                token = new byte[inStream.readInt()];
                System.out
                        .println("Will read input token of size " + token.length + " for processing by initSecContext");
                inStream.readFully(token);
            }
        }

        System.out.println("Context Established! ");
        System.out.println("Client is " + context.getSrcName());
        System.out.println("Server is " + context.getTargName());

    }

    public void sendMessage() throws UnknownHostException, IOException, GSSException {
        // Obtain the command-line arguments and parse the port number
        System.setProperty("java.security.krb5.conf", "/etc/krb5.conf");

        //判断是不是需要初始化kerberos
        this.initKerberos();

        //发送消息
        String msg = "Hello Server ";
        byte[] messageBytes = msg.getBytes();
        outStream.writeInt(messageBytes.length);
        outStream.write(messageBytes);
        outStream.flush();


        //收到服务端传回来的消息
        byte[] token = new byte[0];
        token = new byte[inStream.readInt()];
        System.out.println("Will read token of size " + token.length);
        inStream.readFully(token);

        String s = new String(token);
        System.out.println(s);

        System.out.println("Exiting... ");

        this.destroy();
    }

    public static void main(String[] args) throws IOException, GSSException {
        String princal = args[0];
        String ip = args[1];
        int port = Integer.parseInt(args[2]);
        boolean iskerberos =  false;
        if(args.length >= 4) {
             iskerberos = Boolean.parseBoolean(args[3]);
        }
        TestClient client =  new TestClient(princal, ip, port, iskerberos);
        client.sendMessage();
    }
}

2.3 配置文件说明

还管是服务端还是客户端都需要使用到jaas的配置文件,这个配置文件,在下面结合程序的执行来进行说明

2.4 程序的执行

需要特别指出的,对于客户端/服务端,需要保证KDC正常可用,而且其中包含了正常的princal信息。下面介绍它的执行过程

2.4.1 不带Kerberos认证的通信,即我们正常的连接方式

服务端执行命令:

java -classpath kerberosTest.jar    com.test.gssapi.sample1.TestServer 9111 

客户端执行命令:

java  -classpath kerberosTest.jar   com.test.gssapi.sample1.TestClient admin 192.168.1.56 9111 

此时就是一个普通的TCP通信协议

2.4.2 加入Kerberos认证

服务端的命令:

[root@freeipa56 testKerberos]# java -classpath kerberosTest.jar -Djava.security.krb5.realm=EXAMPLE.COM  -Djava.security.krb5.kdc=freeipa56.example.com -Djavax.security.auth.useSubjectCredsOnly=false -Djava.security.auth.login.config=server.conf   com.test.gssapi.sample1.TestServer 9111 true

参数说明: 服务端一共有两个参数, 9111表示端口号, true表示启用kerberos,如果不输入,则表明不启用kerberos

客户端命令:

java  -classpath kerberosTest.jar -Djava.security.krb5.realm=EXAMPLE.COM  -Djava.security.krb5.kdc=freeipa56.example.com    -Djavax.security.auth.useSubjectCredsOnly=false -Djava.security.auth.login.config=client.conf  com.test.gssapi.sample1.TestClient admin 192.168.1.56 9111 true

参数说明:
客户端参数一共四个参数,
admin: 表示服务端对应的princal为admin, 服务端认证的princal必须为这个
192.168.1.56: 表明服务端对应的IP
9111: 为服务端监听的端口
true: 表明启用kerberos

Kerberos提供了多种认证方式,这些认证方式都是通过java.security.auth.login.config对应的配置文件来设置的。下面对于分几种情况说明.

2.4.2.1 通过输入用户名与密码的方式

服务端配置文件(即server.conf与client.conf):

[root@freeipa56 testKerberos]# cat server.conf 
com.sun.security.jgss.accept  {
  com.sun.security.auth.module.Krb5LoginModule required storeKey=true; 
};

客户端的配置文件client.conf

[root@freeipa56 testKerberos]# cat client.conf 
com.sun.security.jgss.initiate  {
  com.sun.security.auth.module.Krb5LoginModule required;
};

此时客户端的打屏信息:

[lch@freeipa56 testKerberos]$  java  -classpath kerberosTest.jar -Djava.security.krb5.realm=EXAMPLE.COM  -Djava.security.krb5.kdc=freeipa56.example.com    -Djavax.security.auth.useSubjectCredsOnly=false -Djava.security.auth.login.config=client.conf  com.test.gssapi.sample1.TestClient admin 192.168.1.56 9111 true
Connected to server: /192.168.1.56
Kerberos username [lch]: lch   <------ 提示输入princal
Kerberos password for lch:     <-----  提示输入相应的密码
Will send token of size 558 from initSecContext.
Will read input token of size 108 for processing by initSecContext
Context Established! 
Client is lch@EXAMPLE.COM
Server is admin
Will read token of size 22
Receive Client Message
Exiting... 

而服务端:

[root@freeipa56 testKerberos]# java -classpath kerberosTest.jar -Djava.security.krb5.realm=EXAMPLE.COM  -Djava.security.krb5.kdc=freeipa56.example.com -Djavax.security.auth.useSubjectCredsOnly=false -Djava.security.auth.login.config=bcsLogin.conf   com.test.gssapi.sample1.TestServer 9111 true
Will read input token of size 579 for processing by acceptSecContext
Kerberos username [root]: admin  <---- 提示输入服务端的princal名称 
Kerberos password for admin:     <--- 提示输入相应的密码
Will send token of size 108 from acceptSecContext.
Context Established! Client is [email protected]
Server is [email protected]
Will read token of size 13
Hello Server 

2.4.2.2 通过TGT的方式

服务端的配置与2.4.2.1相同,client通过获取ticket的方式来进行认证
服务端的配置文件:

[root@freeipa56 testKerberos]# cat server.conf 
com.sun.security.jgss.accept  {
  com.sun.security.auth.module.Krb5LoginModule required storeKey=true; 
};

客户端配置文件:

[lch@freeipa56 testKerberos]$ cat client.conf 
com.sun.security.jgss.initiate  {
  com.sun.security.auth.module.Krb5LoginModule required
  useKeyTab=false
  useTicketCache=true;

};

查看当前环境的kerberos信息,

[lch@freeipa56 testKerberos]$ klist
Ticket cache: FILE:/tmp/krb5cc_500
Default principal: lch@EXAMPLE.COM

Valid starting     Expires            Service principal
11/15/17 11:08:47  11/16/17 11:08:46  krbtgt/EXAMPLE.COM@EXAMPLE.COM

执行客户端命令:

[lch@freeipa56 testKerberos]$  java  -classpath kerberosTest.jar -Djava.security.krb5.realm=EXAMPLE.COM  -Djava.security.krb5.kdc=freeipa56.example.com    -Djavax.security.auth.useSubjectCredsOnly=false -Djava.security.auth.login.config=client.conf  com.test.gssapi.sample1.TestClient admin 192.168.1.56 9111 true
Connected to server: /192.168.1.56
Will send token of size 579 from initSecContext.
Will read input token of size 108 for processing by initSecContext
Context Established! 
Client is lch@EXAMPLE.COM
Server is admin
Will read token of size 22
Receive Client Message
Exiting... 

客户端程序不需要输入用户名密码(服务端与)

2.4.2.3 服务端通过keytab文件

服务端的配置文件

[root@freeipa56 testKerberos]# cat server.conf 
com.sun.security.jgss.accept  {
  com.sun.security.auth.module.Krb5LoginModule required 
  useKeyTab=true
  storeKey=true
  useTicketCache=false
  keyTab="/root/admin.headless.keytab"
  principal="[email protected]";

};

客户端的配置文件:

[lch@freeipa56 testKerberos]$ cat client.conf 
com.sun.security.jgss.initiate  {
  com.sun.security.auth.module.Krb5LoginModule required
  useKeyTab=false
  useTicketCache=true;

};

执行服务端程序与客户端程序就会发现服务端不再需要输入认证信息了。

3. 总结

Kerberos提供多种认证方式,而我们可以通过java.security.auth.login.config对应的配置参数进行认证

猜你喜欢

转载自blog.csdn.net/eyoulc123/article/details/78542761