MyCat源码分析系列之——前后端验证

更多MyCat源码分析,请戳MyCat源码分析系列


 MyCat前端验证

MyCat的前端验证指的是应用连接MyCat时进行的用户验证过程,如使用MySQL客户端时,$ mysql -uroot -proot -P8066 db_test触发的一系列行为。

验证的过程分为几个步骤:

1)应用与MyCat建立TCP连接;

2)MyCat发送握手包,其中带有为密码加密的种子(seed);

3)应用接收握手包,使用种子进行密码加密,随后将包含用户名、加密密码、字符集和需连接的数据库(可选)等的验证包发送给MyCat;

4)MyCat依次验证信息,并将验证结果回发给应用

MyCat中,前端应用与MyCat之间建立的连接称为FrontendConnection(其子类为ServerConnection和ManagerConnection,分别由ServerConnectionFactory和ManagerConnectionFactory负责创建),在构造函数的时候绑定了一个NIOHandler类型的FrontendAuthenticator用于后续的验证

public FrontendConnection(NetworkChannel channel) throws IOException {
        super(channel);
        InetSocketAddress localAddr = (InetSocketAddress) channel.getLocalAddress();
        InetSocketAddress remoteAddr = null;
        if (channel instanceof SocketChannel) {
            remoteAddr = (InetSocketAddress) ((SocketChannel) channel).getRemoteAddress();    
            
        } else if (channel instanceof AsynchronousSocketChannel) {
            remoteAddr = (InetSocketAddress) ((AsynchronousSocketChannel) channel).getRemoteAddress();
        }
        
        this.host = remoteAddr.getHostString();
        this.port = localAddr.getPort();
        this.localPort = remoteAddr.getPort();
        this.handler = new FrontendAuthenticator(this);
}

首先,应用与MyCat的TCP连接建立过程由NIOAcceptor负责完成,当之前注册的OP_ACCEPT事件得到响应时调用accept()方法:

private void accept() {
        SocketChannel channel = null;
        try {
            channel = serverChannel.accept();
            channel.configureBlocking(false);
            FrontendConnection c = factory.make(channel);
            c.setAccepted(true);
            c.setId(ID_GENERATOR.getId());
            NIOProcessor processor = (NIOProcessor) MycatServer.getInstance()
                    .nextProcessor();
            c.setProcessor(processor);
            
            NIOReactor reactor = reactorPool.getNextReactor();
            reactor.postRegister(c);

        } catch (Exception e) {
            LOGGER.warn(getName(), e);
            closeChannel(channel);
        }
}

该TCP连接建立完成后创建一个FrontendConnection实例,并交由NIOReactor负责后续的读写工作(reactor.postRegister(c);),而NIOReactor中的内部类RW会调用其register()方法:

private void register(Selector selector) {
            AbstractConnection c = null;
            if (registerQueue.isEmpty()) {
                return;
            }
            while ((c = registerQueue.poll()) != null) {
                try {
                    ((NIOSocketWR) c.getSocketWR()).register(selector);
                    c.register();
                } catch (Exception e) {
                    c.close("register err" + e.toString());
                }
            }
}

其中,NIOSocketWR会调用其register()方法注册OP_READ事件,随后FrontendConnection调用其register()方法发送握手包HandshakePacket,等待应用回发验证包:

public void register() throws IOException {
        if (!isClosed.get()) {

            // 生成认证数据
            byte[] rand1 = RandomUtil.randomBytes(8);
            byte[] rand2 = RandomUtil.randomBytes(12);

            // 保存认证数据
            byte[] seed = new byte[rand1.length + rand2.length];
            System.arraycopy(rand1, 0, seed, 0, rand1.length);
            System.arraycopy(rand2, 0, seed, rand1.length, rand2.length);
            this.seed = seed;

            // 发送握手数据包
            HandshakePacket hs = new HandshakePacket();
            hs.packetId = 0;
            hs.protocolVersion = Versions.PROTOCOL_VERSION;
            hs.serverVersion = Versions.SERVER_VERSION;
            hs.threadId = id;
            hs.seed = rand1;
            hs.serverCapabilities = getServerCapabilities();
            hs.serverCharsetIndex = (byte) (charsetIndex & 0xff);
            hs.serverStatus = 2;
            hs.restOfScrambleBuff = rand2;
            hs.write(this);

            // asynread response
            this.asynRead();
        }
}

当收到应用的验证包AuthPacket时,之前绑定在FrontendConnection上的FrontendAuthenticator会调用其handle()方法:

public void handle(byte[] data) {
        // check quit packet
        if (data.length == QuitPacket.QUIT.length && data[4] == MySQLPacket.COM_QUIT) {
            source.close("quit packet");
            return;
        }

        AuthPacket auth = new AuthPacket();
        auth.read(data);

        // check user
        if (!checkUser(auth.user, source.getHost())) {
            failure(ErrorCode.ER_ACCESS_DENIED_ERROR, "Access denied for user '" + auth.user + "' with host '" + source.getHost()+ "'");
            return;
        }

        // check password
        if (!checkPassword(auth.password, auth.user)) {
            failure(ErrorCode.ER_ACCESS_DENIED_ERROR, "Access denied for user '" + auth.user + "', because password is error ");
            return;
        }
        
        // check degrade
        if ( isDegrade( auth.user ) ) {
             failure(ErrorCode.ER_ACCESS_DENIED_ERROR, "Access denied for user '" + auth.user + "', because service be degraded ");
             return;
        }
        
        // check schema
        switch (checkSchema(auth.database, auth.user)) {
        case ErrorCode.ER_BAD_DB_ERROR:
            failure(ErrorCode.ER_BAD_DB_ERROR, "Unknown database '" + auth.database + "'");
            break;
        case ErrorCode.ER_DBACCESS_DENIED_ERROR:
            String s = "Access denied for user '" + auth.user + "' to database '" + auth.database + "'";
            failure(ErrorCode.ER_DBACCESS_DENIED_ERROR, s);
            break;
        default:
            success(auth);
        }
}

依次验证用户名、密码、用户负载、数据库权限,验证成功后调用success()方法向应用发送OK包:

protected void success(AuthPacket auth) {
        source.setAuthenticated(true);
        source.setUser(auth.user);
        source.setSchema(auth.database);
        source.setCharsetIndex(auth.charsetIndex);
        source.setHandler(new FrontendCommandHandler(source));

        if (LOGGER.isInfoEnabled()) {
            StringBuilder s = new StringBuilder();
            s.append(source).append('\'').append(auth.user).append("' login success");
            byte[] extra = auth.extra;
            if (extra != null && extra.length > 0) {
                s.append(",extra:").append(new String(extra));
            }
            LOGGER.info(s.toString());
        }

        ByteBuffer buffer = source.allocate();
        source.write(source.writeToBuffer(AUTH_OK, buffer));
        boolean clientCompress = Capabilities.CLIENT_COMPRESS==(Capabilities.CLIENT_COMPRESS & auth.clientFlags);
        boolean usingCompress= MycatServer.getInstance().getConfig().getSystem().getUseCompression()==1 ;
        if(clientCompress&&usingCompress)
        {
            source.setSupportCompress(true);
        }
}

这里最重要的就是将FrontendConnection的handler重新绑定为FrontendCommandHandler对象,用于后续各类命令的接收和处理分发。

 


后端验证

后端验证过程类似于前端验证过程,区别就在于此时MyCat是作为客户端,向后端MySQL数据库发起连接与验证请求。

 MyCat中,MyCat与后端MySQL的连接称为MySQLConnection,由MySQLConnectionFactory创建,并绑定了一个NIOHandler类型的MySQLConnectionAuthenticator,用于完成与MySQL的验证过程:

public MySQLConnection make(MySQLDataSource pool, ResponseHandler handler,
            String schema) throws IOException {

        DBHostConfig dsc = pool.getConfig();
        NetworkChannel channel = openSocketChannel(MycatServer.getInstance()
                .isAIO());

        MySQLConnection c = new MySQLConnection(channel, pool.isReadNode());
        MycatServer.getInstance().getConfig().setSocketParams(c, false);
        c.setHost(dsc.getIp());
        c.setPort(dsc.getPort());
        c.setUser(dsc.getUser());
        c.setPassword(dsc.getPassword());
        c.setSchema(schema);
        c.setHandler(new MySQLConnectionAuthenticator(c, handler));
        c.setPool(pool);
        c.setIdleTimeout(pool.getConfig().getIdleTimeout());
        if (channel instanceof AsynchronousSocketChannel) {
            ((AsynchronousSocketChannel) channel).connect(
                    new InetSocketAddress(dsc.getIp(), dsc.getPort()), c,
                    (CompletionHandler) MycatServer.getInstance()
                            .getConnector());
        } else {
            ((NIOConnector) MycatServer.getInstance().getConnector())
                    .postConnect(c);
        }
        return c;
}

MySQLConnection创建完成后,交由NIOConnector完成TCP连接,它会调用其connect()方法注册OP_CONNECT事件,并发送连接请求至MySQL:

private void connect(Selector selector) {
        AbstractConnection c = null;
        while ((c = connectQueue.poll()) != null) {
            try {
                SocketChannel channel = (SocketChannel) c.getChannel();
                channel.register(selector, SelectionKey.OP_CONNECT, c);
                channel.connect(new InetSocketAddress(c.host, c.port));
            } catch (Exception e) {
                c.close(e.toString());
            }
        }
}

TCP连接建立后同样交由NIOReactor负责后续的读写工作,最终之前绑定在MySQLConnection上的MySQLConnectionAuthenticator会调用其handle()方法:

public void handle(byte[] data) {
        try {
            switch (data[4]) {
            case OkPacket.FIELD_COUNT:
                HandshakePacket packet = source.getHandshake();
                if (packet == null) {
                    processHandShakePacket(data);
                    // 发送认证数据包
                    source.authenticate();
                    break;
                }
                // 处理认证结果
                source.setHandler(new MySQLConnectionHandler(source));
                source.setAuthenticated(true);
                boolean clientCompress = Capabilities.CLIENT_COMPRESS==(Capabilities.CLIENT_COMPRESS & packet.serverCapabilities);
                boolean usingCompress= MycatServer.getInstance().getConfig().getSystem().getUseCompression()==1 ;
                if(clientCompress&&usingCompress)
                {
                    source.setSupportCompress(true);
                }
                if (listener != null) {
                    listener.connectionAcquired(source);
                }
                break;
            case ErrorPacket.FIELD_COUNT:
                ErrorPacket err = new ErrorPacket();
                err.read(data);
                String errMsg = new String(err.message);
                LOGGER.warn("can't connect to mysql server ,errmsg:"+errMsg+" "+source);
                //source.close(errMsg);
                throw new ConnectionException(err.errno, errMsg);

            case EOFPacket.FIELD_COUNT:
                auth323(data[3]);
                break;
            default:
                packet = source.getHandshake();
                if (packet == null) {
                    processHandShakePacket(data);
                    // 发送认证数据包
                    source.authenticate();
                    break;
                } else {
                    throw new RuntimeException("Unknown Packet!");
                }

            }

        } catch (RuntimeException e) {
            if (listener != null) {
                listener.connectionError(e, source);
                return;
            }
            throw e;
        }
}

与前端验证一样,MySQL数据库作为服务端会在TCP连接建立完成后向应用发送一个握手包HandshakePacket,在此MySQLConnectionAuthenticator负责读取此包,并通过source.authenticate();调用MySQLConnection的authenticate()方法向MySQL发送验证包AuthPacket:

public void authenticate() {
        AuthPacket packet = new AuthPacket();
        packet.packetId = 1;
        packet.clientFlags = clientFlags;
        packet.maxPacketSize = maxPacketSize;
        packet.charsetIndex = this.charsetIndex;
        packet.user = user;
        try {
            packet.password = passwd(password, handshake);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e.getMessage());
        }
        packet.database = schema;
        packet.write(this);
}

验证通过后,将MySQLConnection的handler重新绑定为MySQLConnectionHandler对象,用于后续接收MySQL数据库发送过来的各类数据和处理分发


 

参考资料

[1] http://sonymoon.iteye.com/blog/2245141

 


为尊重原创成果,如需转载烦请注明本文出处:http://www.cnblogs.com/fernandolee24/p/5196332.html,特此感谢

posted @ 2016-02-18 09:33  Aπoλλων  阅读(4157)  评论(2编辑  收藏  举报