HDFSRPC安全认证Kerberos篇

本文主要阐述HDFSRPC安全认证相关的实现。主要介绍Kerberos相关的实现。

写在前面

相关blog可以先看一下

https://segmentfault.com/a/1190000039085046?sort=newest

https://blog.csdn.net/qq_35995514/article/details/106348765

https://blog.csdn.net/S1124654/article/details/128791481

Rpc安全认证

Rpc安全认证使用的是sasl框架,sasl框架本身无认证相关的实现,认证实现使用的Kerberos。SASL: 在jdk中定义的一种通用的基于客户端和服务端的认证框架,GSSAPI是其实现之一。

GSSAPI: 在jdk中,作为对kerberos认证实现的一部分。

Kerberos: 一种基于中心认证服务器的中心化认证协议和框架。应用程序访问服务前需使用此框架进行登录认证,以在应用程序之间形成动态可控的受信。中心化登录服务器称为KDC。

Krb5LoginModule: 在jdk中,负责从KDC获取登录凭证,是kerberos认证实现的一部分。

SASL

简单认证与安全层(Simple Authentication And Security Layer)是一个在网络协议中用来认证和数据加密的框架。它把认证机制从程序中分离开,理论上使用SASL的程序协议都可以使用SASL所支持的全部认证机制(token认证就是其中的一种认证机制)。

认证机制可支持代理认证,这让一个用户可以承担另一个用户的认证。

SASL同样提供数据安全层,这提供了数据完整验证和数据加密,例如DIGEST_MD5提供了数据加密层。

SASL是一种challenge-response的协议,由服务端发送challenge到客户端,客户端基于challenge发送response,这种交互直到服务端被满足并且不再发布新的challenge。

challenge和对应的response都是任意长度的二进制数据。其大概流程如下所示:

1857_1.png

要注意一点sasl是一种框架,不涉及具体实现,通信时可以自己定义相关的包。

Hdfs中sasl相关实现

Negotiate:

client会发送一个saslMessage给server,其中saslstate为Negotiate,无其他信息。Server接收到Negotiate请求后,会返回一个Negotiate的saslMessage,其中包含sasl使用需要使用哪种协议,例如:

auths {
  method: "KERBEROS"
  mechanism: "GSSAPI"
  protocol: "root"
  serverId: "node17"
}

Initiate:

client接收到Server的Negotiate的saslMessage后,会根据相关的信息,初始化saslClient,并产生一个Token,发送一个saslstate为Initiate的saslMessage给server。Server接收到saslMessage以后,同样会初始化saslServer,然后evaluate token,生成新的token,返回一个saslstate为challenge的saslMessage给client。

Response-challenge:

client接收到Server的challenge的saslMessage后,会evaluate challenge token,产生一个新的token,然后发送一个saslstate为Response的saslMessage给server。。Server接收到saslMessage以后,然后evaluate token,生成新的token,如果server已经完成初始化,返回一个saslstate为success的saslMessage给client,反之则返回一个saslstate为challenge的saslMessage给client。Client接收如果为challenge的saslMessage则重复上述流程,反之如果接收到success则完成client的初始化。一般这个过程会经过两轮。

GSSAPI

在jdk中,作为对kerberos认证实现的一部分。

Kerberos

Hadoop 使用 Kerberos 作为用户和服务的强身份验证和身份传播的基础。Kerberos 是一种计算机网络认证协议,它允许某实体在非安全网络环境下通信,向另一个实体以一种安全的方式证明自己的身份。 Kerberos 是第三方认证机制,其中用户和服务依赖于第三方(Kerberos 服务器)来对彼此进行身份验证。Kerberos服务器本身称为密钥分发中心或 KDC。在较高的层面上,它有三个部分:

1)它知道的用户和服务(称为主体)及其各自的 Kerberos 密码的数据库

2)一个认证服务器(Authentication Server,简称 AS):验证Client端的身份(确定你是身份证上的本人),验证通过就会给一张票证授予票证(Ticket Granting Ticket,简称 TGT)给 Client。

3)一个票据授权服务器(Ticket Granting Server,简称 TGS):通过 TGT(AS 发送给 Client 的票)获取访问 Server 端的票(Server Ticket,简称 ST)。ST(Service Ticket)也有资料称为 TGS Ticket。

以平时坐火车举例:

1857_2.png

一个用户主要来自AS请求认证。AS 返回 使用用户主体 的 Kerberos密码加密 的 TGT ,该密码仅为用户主体和 AS 所知。用户主体使用其 Kerberos 密码在本地解密TGT,从那时起,直到ticket 到期,用户主体可以使用 TGT 从 TGS 获取服务票据。服务票证允许委托人访问服务。

Hdfs中的Kerberos

在sasl中似乎没有提到Kerberos,实际上saslclient、saslserver的初始化后需要通过Kerberos验证以后才能实现。具体流程如下:

Client:

Client使用的当前登录用户,需要使用kinit登录用户,然后代码中使用通用库登录,获取Subject。Subject有doas方法,可以使用此方法来初始化saslclient,已经使用saslclient.evaluateChallenge方法。

Server:

Server为服务类型,通常使用免密登录。需要单独的 Kerberos 账号及其 keytab 文件。有了这个以后,可以使用通用库登录,获取Subject。Subject有doas方法,可以使用此方法来初始化saslserver,已经使用saslserver.evaluateResponse方法。

为了更好的理解整个过程,附上Java代码简单实现

Java代码简单实现

SaslClient.java

public class SaslClientTest {
  private static Socket socket;

  public static void main(String[] args) throws UnknownHostException, IOException, LoginException, PrivilegedActionException {
    //kerberos login
    Configuration config = new Configuration() {
      @Override
      public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
        ArrayList<AppConfigurationEntry> entries = new ArrayList<>();
        String loginModuleName = "com.sun.security.auth.module.Krb5LoginModule";
        final Map<String,String> options = new HashMap<>();
        LoginModuleControlFlag controlFlag = LoginModuleControlFlag.OPTIONAL;
        options.put("useTicketCache", "true");
        options.put("renewTGT", "true");
        options.put("doNotPrompt", "true");
        options.put("refreshKrb5Config", "true");
        AppConfigurationEntry kerberosEntry = new AppConfigurationEntry(loginModuleName, controlFlag, options);
        entries.add(kerberosEntry);
        return entries.toArray(new AppConfigurationEntry[0]);
      }
    };
    LoginContext login = new LoginContext("hadoop-kerberos", null, null, config);
    login.login();
    Subject subject = login.getSubject();
    System.out.println(subject.getPrincipals());
    
    Subject.doAs(subject, new PrivilegedExceptionAction<String>() {
      @Override
      public String run() throws Exception {
        String hostname = "localhost";
        int port = 19876;
        socket = new Socket(hostname, port);
        DataInputStream socketIn = new DataInputStream(socket.getInputStream());
        DataOutputStream socketOut = new DataOutputStream(socket.getOutputStream());
        System.out.println("Connected to server " + socket.getInetAddress());
        
        boolean done = false;
        SaslClient saslClient = null;
        byte[] responseToken;
        Packet requestPacket;
        Packet negotiatePacket = new Packet(SaslState.NEGOTIATE);
        negotiatePacket.write(socketOut);
        do {
          Packet responsePacket = Packet.read(socketIn);
          switch (responsePacket.getState()) {
            case NEGOTIATE:
              //create saslclient
              //auths { method: "KERBEROS" mechanism: "GSSAPI" protocol: "root" serverId: "node17" }
              String mechanism = "GSSAPI";
              String saslProtocol = "root";
              String saslServerName = "node17";
              String saslUser = null;
              CallbackHandler saslCallback = null;
              Map<String, String> saslProperties = new HashMap<String, String>();
              saslProperties.put("javax.security.sasl.qop", "auth");
              saslProperties.put("javax.security.sasl.server.authentication", "true");
              saslClient = Sasl.createSaslClient(new String[]{mechanism}, saslUser, saslProtocol, saslServerName, saslProperties, saslCallback);
              
              byte[] challengeToken = null;
              if (saslClient.hasInitialResponse()) {
                challengeToken = new byte[0];
              }
              responseToken = (challengeToken != null) ? saslClient.evaluateChallenge(challengeToken) : new byte[0];
              requestPacket = new Packet(SaslState.INITIATE, responseToken);
              requestPacket.write(socketOut);
              break;
            case CHALLENGE:
              if (saslClient == null) {
                // should probably instantiate a client to allow a server to
                // demand a specific negotiation
                throw new SaslException("Server sent unsolicited challenge");
              }
              responseToken = saslEvaluateToken(responsePacket, false, saslClient);
              requestPacket = new Packet(SaslState.RESPONSE, responseToken);
              requestPacket.write(socketOut);
              break;
            case SUCCESS:
              if (saslClient == null) {
                throw new SaslException("Server sent unsolicited success");
              } else {
                saslEvaluateToken(responsePacket, true, saslClient);
              }
              done = true;
              break;
            default:
              throw new SaslException("error state");
          }
        }while (!done);
        socket.close();
        return null;
      }
    });
  }
  
  private static byte[] saslEvaluateToken(Packet response, boolean serverIsDone, SaslClient saslClient) throws SaslException {
    byte[] saslToken = null;
    if (!serverIsDone && response.hasToken()) {
      saslToken = response.getToken();
      saslToken = saslClient.evaluateChallenge(saslToken);
    } else if (!serverIsDone) {
      // the server may only omit a token when it's done
      throw new SaslException("Server challenge contains no token");
    }
    if (serverIsDone) {
      // server tried to report success before our client completed
      if (!saslClient.isComplete()) {
        throw new SaslException("Client is out of sync with server");
      }
      // a client cannot generate a response to a success message
      if (saslToken != null) {
        throw new SaslException("Client generated spurious response");        
      }
    }
    return saslToken;
  }
}

Saslserver.java

public class SaslServerTest {
  private static ServerSocket ss;

  public static void main(String[] args) throws IOException, LoginException, PrivilegedActionException {
    int port = 19876;
    ss = new ServerSocket(port);
    //login kerberos
    Configuration config = new Configuration() {
      @Override
      public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
        ArrayList<AppConfigurationEntry> entries = new ArrayList<>();
        String loginModuleName = "com.sun.security.auth.module.Krb5LoginModule";
        final Map<String,String> options = new HashMap<>();
        LoginModuleControlFlag controlFlag = LoginModuleControlFlag.REQUIRED;
        options.put("principal", "root/node17@LOONGOOP.COM");
        options.put("useKeyTab", "true");
        options.put("keyTab", "/var/kerberos/krb5kdc/nn.keytab");
        options.put("storeKey", "true");
        options.put("doNotPrompt", "true");
        options.put("refreshKrb5Config", "true");
        AppConfigurationEntry kerberosEntry = new AppConfigurationEntry(loginModuleName, controlFlag, options);
        entries.add(kerberosEntry);
        return entries.toArray(new AppConfigurationEntry[0]);
      }
    };
    LoginContext  login = new LoginContext("hadoop-kerberos", null, null, config);
    login.login();
    Subject subject = login.getSubject();
    System.out.println(subject.getPrincipals());
    while(true) {
      Subject.doAs(subject, new PrivilegedExceptionAction<String>() {
  
        @Override
        public String run() throws Exception {
          Socket socket = ss.accept();
          DataInputStream socketIn = new DataInputStream(socket.getInputStream());
          DataOutputStream socketOut = new DataOutputStream(socket.getOutputStream());
          System.out.println("Got connection from client " + socket.getInetAddress());
          boolean done = false;
          Packet responsePacket;
          SaslServer saslServer = null;
          do {
            Packet requestPacket = Packet.read(socketIn);
            switch(requestPacket.getState()) {
              case NEGOTIATE:
                responsePacket = new Packet(SaslState.NEGOTIATE);
                requestPacket.write(socketOut);
                break;
              case INITIATE:
                saslServer = Subject.doAs(subject, new PrivilegedExceptionAction<SaslServer>() {
                  @Override
                  public SaslServer run() throws Exception {
                    //auths { method: "KERBEROS" mechanism: "GSSAPI" protocol: "root" serverId: "node17" }
                    String mechanism = "GSSAPI";
                    String protocol = "root";
                    String serverId = "node17";
                    final CallbackHandler callback = new SaslGssCallbackHandler();
                    return FastSaslServerFactory.getInstance().createSaslServer(mechanism, protocol, serverId, null, callback);
                  }
                });
                
                responsePacket = processSaslToken(requestPacket, saslServer);
                responsePacket.write(socketOut);
                break;
              case RESPONSE:
                responsePacket = processSaslToken(requestPacket, saslServer);
                responsePacket.write(socketOut);
                if(saslServer.isComplete()) {
                  done = true;
                }
                break;
              default:
                throw new SaslException("error sasl state");
            }
          } while(!done);  
          return null;
        }
      });
    }
  }
  
  private static Packet processSaslToken(Packet requestPacket, SaslServer saslServer) throws SaslException {
    if (!requestPacket.hasToken()) {
      throw new SaslException("Client did not send a token");
    }
    if (saslServer == null) {
      throw new SaslException("sasl server is null");
    }
    byte[] saslToken = requestPacket.getToken();
    saslToken = saslServer.evaluateResponse(saslToken);
    return new Packet(saslServer.isComplete() ? SaslState.SUCCESS : SaslState.CHALLENGE, saslToken);
  }
  
  public static class SaslGssCallbackHandler implements CallbackHandler {
    @Override
    public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
      AuthorizeCallback ac = null;
      for (Callback callback : callbacks) {
        if (callback instanceof AuthorizeCallback) {
          ac = (AuthorizeCallback) callback;
        } else {
          throw new UnsupportedCallbackException(callback, "Unrecognized SASL GSSAPI Callback");
        }
      }
      if (ac != null) {
        String authid = ac.getAuthenticationID();
        String authzid = ac.getAuthorizationID();
        if (authid.equals(authzid)) {
          ac.setAuthorized(true);
        } else {
          ac.setAuthorized(false);
        }
        if (ac.isAuthorized()) {
          ac.setAuthorizedID(authzid);
        }
      }
    }
  }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zfpigpig

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值