Kafka安全机制解析(二)

PLAIN模式源码分析

Posted by Cadmean on July 28, 2018

简介

上一篇Kafka安全机制解析及重构(一)中介绍了Kafka的安全认证的流程。其实Kafka官方也是推荐用户自己写一个安全认证模块的。官方在介绍SASL/PLAIN模式的时候是这样说的

  • SASL/PLAIN should be used only with SSL as transport layer to ensure that clear passwords are not transmitted on the wire without encryption. SASL/PLAIN模式应该搭配SSL协议使用,这样可以避免密码明文传输
  • The default implementation of SASL/PLAIN in Kafka specifies usernames and passwords in the JAAS configuration file as shown here. To avoid storing passwords on disk, you can plug in your own implementation of javax.security.auth.spi.LoginModule that provides usernames and passwords from an external source. The login module implementation should provide username as the public credential and password as the private credential of the Subject. The default implementation org.apache.kafka.common.security.plain.PlainLoginModule can be used as an example. 由于SASL/PLAIN模式中密码是明文保存在JAAS配置文件中的,为了避免用户密码的明文保存,用户可以自己实现LoginModule接口来从别的位置获取用户密码。用户自己实现的login module需要将用户名和密码存放在Subject对象中,Kafka的PlainLoginModule可以作为一个示例供参考
  • In production systems, external authentication servers may implement password authentication. Kafka brokers can be integrated with these servers by adding your own implementation of javax.security.sasl.SaslServer. The default implementation included in Kafka in the packageorg.apache.kafka.common.security.plain can be used as an example to get started. 在生产环境中,Kafka可以从外部的源获取用户密码信息,用户可以通过实现SaslServer接口来实现这个能力,plain模式的源码同样可以作为参考。

通过阅读PLAIN模式的源码,的确让我理清了JAAS的认证模式,自己实现的KAFKA的认证模块需要实现以下几个接口

  1. LoginModule,接口类是javax.security.auth.spi.LoginModule,这个类会在启动时被自动实例化。
  2. Provider,接口类是java.security.Provider,这个类一般在LoginModule实例化的时候被调起,将SaslClient和SaslServer的构造方法告知Sasl模块。一般在客户端写一个SaslClientProvider,把SaslClientFactory传入即可,而在Server端则需要写SaslClientProvider和SaslServerProvider,传入SaslClientFactory和SaslServerFactory。
  3. SaslClientFactory,接口类是javax.security.sasl.SaslClientFactory,在这个工厂类中构造出SaslClient。
  4. SaslServerFactory,接口类是javax.security.sasl.SaslServerFactory,在这个工厂类中构造出SaslServer。
  5. SaslClient,由工厂类SaslClientFactory生成,用于处理服务端请求,构造鉴权报文并发送。
  6. SaslServer,由工厂类SaslServerFactory生成,用于处理客户端请求,并进行用户密码认证。

这里我们同样把上一章中的流程图放出来,并增加一些更细节的说明,下面的源码分析可以就着这个图看。

鉴权机制流程

PLAIN源码分析

LoginModule

我们先看看PLAIN模式的代码来热热身,源码我稍微缩减了一下,实际自己实现的时候最好别照搬,先实现出接口,然后看看有哪些方法是必须实现的。

public class PlainLoginModule implements LoginModule {

    private static final String USERNAME_CONFIG = "username";
    private static final String PASSWORD_CONFIG = "password";

    static {
        /**
        / 这里初始化PlainServerProvider,PLAIN模式没有ClientProvider,
        / 因为不需要进行客户端信息加密的工作,直接传明文。
        **/
        PlainSaslServerProvider.initialize();
    }

    @Override
    public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
        // 从options里获取用户密码,option是从JAAS配置文件中读取的信息
        String username = (String) options.get(USERNAME_CONFIG);
        if (username != null)
            subject.getPublicCredentials().add(username);
        String password = (String) options.get(PASSWORD_CONFIG);
        if (password != null)
            subject.getPrivateCredentials().add(password);
    }
}

PlainLoginModule这个类的作用是:

  • 获取用户密码并将其放入Subject类中
  • 初始化Provider类。

    Provider

    再来看Server的Provider类,这个类比较简单,是把安全机制及工厂类传入Security对象,供后续如果有需要就通过反射机制由该工厂类生成对应的PlainServer。

    public class PlainSaslServerProvider extends Provider {
    
      private static final long serialVersionUID = 1L;
    
      protected PlainSaslServerProvider() {
          super("Simple SASL/PLAIN Server Provider", 1.0, "Simple SASL/PLAIN Server Provider for Kafka");
          super.put("SaslServerFactory." + PlainSaslServer.PLAIN_MECHANISM, PlainSaslServerFactory.class.getName());
      }
    
      public static void initialize() {
          Security.addProvider(new PlainSaslServerProvider());
      }
    }
    

    SaslServerFactory

    PlainSaslServerFactory类是用来构造PlainSaslServer的工厂类,也是比较简单的,PlainSaslServerFactory类在PlainSaslServer类中,实际如果自己要写的话最好分出一个新的类,不然构造的时候会出问题。SaslServerFactory接口必须实现两个方法:

  • createSaslServer,首要是构造SaslServer,不然怎么叫工厂类呢
  • getMechanismNames,上层需要知道这个工厂类对应的机制,比如PLAIN,SCRAM-SHA-256等等,如果自己写的话,像阿里的认证机制叫ONS,华为的叫DMS。
    public static class PlainSaslServerFactory implements SaslServerFactory {

        @Override
        public SaslServer createSaslServer(String mechanism, String protocol, String serverName, Map<String, ?> props, CallbackHandler cbh)
            throws SaslException {

            if (!PLAIN_MECHANISM.equals(mechanism)) {
                throw new SaslException(String.format("Mechanism \'%s\' is not supported. Only PLAIN is supported.", mechanism));
            }
            return new PlainSaslServer(cbh);
        }

        @Override
        public String[] getMechanismNames(Map<String, ?> props) {
            String noPlainText = (String) props.get(Sasl.POLICY_NOPLAINTEXT);
            if ("true".equals(noPlainText))
                return new String[]{};
            else
                return new String[]{PLAIN_MECHANISM};
        }
    }

PlainSaslServer

重头戏是PlainSaslServer类,主要的方法就是evaluateResponse,这个方法被用于处理客户端发送过来的报文,在PLAIN模式中,报文被一个空字节切分成authorizationID,username,password三个元素。然后通过JaasUtils.defaultServerJaasConfigOption(JAAS_USER_PREFIX + username, PlainLoginModule.class.getName())方法来获取到JAAS配置文件中用户名对应的密码,比对通过则返回一个空字节报文至客户端。

public class PlainSaslServer implements SaslServer {

    public static final String PLAIN_MECHANISM = "PLAIN";
    private static final String JAAS_USER_PREFIX = "user_";

    private boolean complete;
    private String authorizationID;

    public PlainSaslServer(CallbackHandler callbackHandler) {
    }

    @Override
    public byte[] evaluateResponse(byte[] response) throws SaslException {
        String[] tokens;
        try {
            tokens = new String(response, "UTF-8").split("\u0000");
        } catch (UnsupportedEncodingException e) {
            throw new SaslException("UTF-8 encoding not supported", e);
        }
        if (tokens.length != 3)
            throw new SaslException("Invalid SASL/PLAIN response: expected 3 tokens, got " + tokens.length);
        authorizationID = tokens[0];
        String username = tokens[1];
        String password = tokens[2];

        if (username.isEmpty()) {
            throw new SaslException("Authentication failed: username not specified");
        }
        if (password.isEmpty()) {
            throw new SaslException("Authentication failed: password not specified");
        }
        if (authorizationID.isEmpty())
            authorizationID = username;

        try {
            // 从配置文件中获取到expectedPassword,而后比对。
            String expectedPassword = JaasUtils.defaultServerJaasConfigOption(JAAS_USER_PREFIX + username, PlainLoginModule.class.getName());
            if (!password.equals(expectedPassword)) {
                throw new SaslException("Authentication failed: Invalid username or password");
            }
        } catch (IOException e) {
            throw new SaslException("Authentication failed: Invalid JAAS configuration", e);
        }
        complete = true;
        return new byte[0];
    }
}

今天这篇介绍了一下kafka认证的关键类,解析了一下PLAIN模式,根据这些知识我们就可以实现一个自己的安全机制了,下一章中就介绍重构时候需要注意的几个要点吧。


< script src = " / js / bootstrap.min.js " >