Java Nio (5) Java Nio implementa solicitações HTTPS

Na verdade, o HTTPS adiciona uma camada de criptografia e descriptografia entre o TCP e a camada de aplicativo. Essa camada é SSL.

Introdução aos princípios SSL

  1. trocar chaves
  2. Usando chaves para criptografia e descriptografia simétrica
  3. Princípio detalhado https://blog.csdn.net/qq_38265137/article/details/90112705

Chaves de troca (handshake SSL de quatro vias)

  1. O cliente envia uma mensagem Client Hello para o servidor. Esta mensagem contém um número aleatório  Random1 gerado pelo cliente, conjuntos de cifras (Support Ciphers) e versão SSL suportada pelo cliente. Após recebê-la, o servidor envia Server para o cliente. Hello mensagem, esta mensagem determinará um conjunto de criptografia a partir das Cifras de Suporte passadas pelo Cliente Olá. Este conjunto determina quais algoritmos serão usados ​​na criptografia subseqüente e na geração de resumo, e também gerará um número aleatório Random2. Observe que agora tanto o cliente quanto o servidor possuem dois números aleatórios (Random1+ Random2)
  2. O servidor enviará 4 pacotes. 1. Certificado digital e toda a cadeia até a CA raiz (permitindo ao cliente autenticar o servidor com a chave pública do servidor no certificado do servidor) 2. Troca de chaves do servidor (opcional, dependendo do algoritmo de troca de chaves). 3. Solicitação de certificado: O servidor pode exigir que o cliente se verifique. 4. Conclusão do handshake do servidor: o final da segunda fase e o início da terceira fase.
  3. O cliente envia 3 pacotes. 1. Certificado (opcional. Para provar seu valor ao servidor, o cliente deve enviar informações do certificado. Isso é opcional. A autenticação forçada do certificado do cliente pode ser configurada no IIS). 2. Troca de chave do cliente (pré-master-secret), gera um número aleatório Random3 e usa a chave pública do servidor para criptografia (é claro, esse número depende do algoritmo de troca de chave, seja para enviar um número aleatório ou parâmetros de criptografia). 3. Verificação do certificado (opcional), assinar o número secreto e aleatório preparado para comprovar a posse da (a) chave pública do certificado. Neste ponto, ambas as partes têm Random1, Random2 e Random3. Ambas as partes calculam a mesma chave simétrica através destes três parâmetros e métodos de criptografia.
  4. O cliente envia uma mensagem Change Cipher Spec para informar ao servidor para usar o novo algoritmo de criptografia para mensagens futuras. Em seguida, o cliente envia uma mensagem Concluído com o novo algoritmo e parâmetros de chave.Esta mensagem pode verificar se a troca de chaves e o processo de autenticação foram bem-sucedidos. Inclui um valor de verificação para verificar a mensagem de todo o processo de handshake do cliente. O servidor também envia a mensagem Change Cipher Spec e a mensagem Finished. O processo de handshake é concluído e o cliente e o servidor podem trocar dados da camada de aplicação para comunicação.

Criptografia e descriptografia

  • Os dados gerados pela camada de aplicação são criptografados pela chave simétrica mencionada acima e pelo algoritmo de criptografia acordado e enviados ao TCP para transmissão.
  • Os dados recebidos pelo TCP são descriptografados pela chave simétrica mencionada acima e pelo algoritmo de criptografia acordado e passados ​​para a camada de aplicação.

 

Motor SSL

Para operações SSL, Java já possui uma implementação de SSLEngine, então não precisamos reinventar a roda. O método de uso é https://nowjava.com/docs/java-api-11/java.base/javax/net /ssl/SSLEngine.html

Aperto de mão

Você pode avaliar o status do handshake por meio de SSLEngineResult.HandshakeStatus, que nos orientará sobre o que fazer a seguir.

FINISHED: O aperto de mão está concluído.
NEED_TASK: É necessário aguardar a conclusão de algumas tarefas, caso contrário o handshake não poderá continuar. Quando isso acontecer, os métodos subsequentes de encapsulamento e desembrulhamento do mecanismo serão bloqueados até que a tarefa seja concluída.
NEED_UNWRAP: Novos dados precisam ser lidos do par, caso contrário, o handshake não poderá continuar.
NEED_UNWRAP_AGAIN: Semelhante a NEED_UNWRAP, mas significa que os dados lidos do peer já existem localmente, neste estado não há necessidade de passar pela rede novamente, apenas analisar os dados que foram recebidos. NOTA: Em java8_u151, não existe esse tipo de enumeração.
NEED_WRAP: Os dados precisam ser enviados ao par, caso contrário, o handshake não poderá continuar.
NOT_HANDSHAKING: Atualmente não está no estágio de handshake.

Autenticação bidirecional e certificados autoassinados

Para situações em que o cliente precisa ser autenticado, o cliente também deve ter um certificado CA. Claro, podemos gastar dinheiro para solicitar um certificado de uma instituição confiável ou podemos usar as próprias ferramentas Java para gerar um certificado.

Execute o seguinte comando

keytool -genkey -keyalg RSA -keysize 2048 -keystore /home/XXX.jks

Observe que você precisa inserir a senha deste keystore neste momento.
O comprimento da senha deve ter pelo menos 6 caracteres.

Enter keystore password:
Keystore password is too short - must be at least 6 characters
Enter keystore password:
Re-enter new password:

Após inserir a senha, o próximo passo é inserir algumas informações relacionadas a esta chave,
as partes que precisam de atenção são o nome e o sobrenome , pois indica o nome de domínio ao qual este certificado se aplica.

What is your first and last name?
  [Unknown]:  XXX.XX.com
What is the name of your organizational unit?
  [Unknown]:  XX
What is the name of your organization?
  [Unknown]:  XX
What is the name of your City or Locality?
  [Unknown]:  Dalian
What is the name of your State or Province?
  [Unknown]:  CN
What is the two-letter country code for this unit?
  [Unknown]:  CN
Is CN=XXX.com, OU=XX, O=XX, L=Dalian, ST=CN, C=CN correct?
  [no]:  y

O último passo é deixar a senha em branco e apenas pressionar Enter.

Enter key password for <mykey>
        (RETURN if same as keystore password):

Desta forma, o certificado jks foi gerado no diretório especificado.

Podemos então usar SSLContext para configurar nosso certificado (primeiro parâmetro) e lista de certificados confiáveis ​​(segundo parâmetro)

sslContext.init(keyManagers(), trustManagers(), null);

Se o certificado CA do servidor não for emitido por uma autoridade confiável, configure a lista de certificados confiáveis ​​do cliente com o certificado do servidor. O mesmo vale para servidores.

Criptografia e descriptografia

SSLEngineResult res = sslEngine.wrap(appWBuffer, packetWBuffer);
SSLEngineResult res = sslEngine.unwrap(packetBuffer, appBuffer)

Determine o status de criptografia e descriptografia por meio de SSLEngineResult

OK: agrupar (enviar dados) ou desembrulhar (receber dados) foi bem-sucedido e sem erros.
CLOSED: Para a operação NEED_WRAP de handshake, o final atual fecha ativamente a comunicação TLS; para NEED_UNWRAP, o peer chama ativamente a comunicação TLS e o final atual obtém a mensagem close_notify enviada pelo peer.
BUFFER_UNDERFLOW (buffer free): Teoricamente, esta situação não ocorrerá no estágio NEED_WRAP de handshake, para o estágio NEED_UNWRAP, (1) o espaço packetBuffer é insuficiente e precisa ser expandido (o tamanho inicial pode ser sslSession.getPacketBufferSize()); (2) Há um problema de meio pacote nos dados lidos pelo packetBuffer e você precisa continuar lendo no soquete (você pode executar packetBuffer.compact() e continuar lendo).
BUFFER_OVERFLOW (estouro de buffer): Para NEED_WRAP, o espaço myNetBuf é insuficiente e precisa ser expandido ou limpo; para NEED_UNWRAP, peerAppBuf é insuficiente e precisa ser expandido ou limpo.

Código de amostra

Como estamos imitando o navegador para enviar https, não precisamos carregar nosso próprio certificado ou adicionar um certificado confiável. Se você estiver acessando um site privado, precisará colocar seu certificado em um certificado confiável. Se a outra parte exigir autenticação bidirecional, você também precisará carregar seu próprio certificado e enviar seu próprio certificado para a outra parte para configurá-lo como confiável (se o certificado for confiável. Se for emitido por uma organização CA, não há necessidade de configurar um certificado confiável)

Então vamos alterar o código do artigo anterior, tomando Content-Length como exemplo (Transfer-Encoding é o mesmo que chunked, apenas criptografa e descriptografa os dados). O código aqui não é bloqueador, o código completo para multiplexação io está no github  https://github.com/cxsummer/net-nio

public static void main(String[] args) throws Exception {
        int port = 443;
        String host = "www.ximalaya.com";
        String path = "/";
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(host, port));
        socketChannel.configureBlocking(false);

        SSLContext sslCtx = SSLContext.getInstance("TLS");
        sslCtx.init(null, null, null);
        SSLEngine sslEngine = sslCtx.createSSLEngine(host, port);
        sslEngine.setUseClientMode(true);
        sslEngine.beginHandshake();
        SSLSession sslSession = sslEngine.getSession();
        SSLEngineResult.HandshakeStatus handshakeStatus = sslEngine.getHandshakeStatus();
        ByteBuffer appBuffer = ByteBuffer.allocate(sslSession.getApplicationBufferSize());
        ByteBuffer packetBuffer = ByteBuffer.allocate(sslSession.getPacketBufferSize());

        ByteBuffer appWBuffer = ByteBuffer.allocate(sslSession.getApplicationBufferSize());
        ByteBuffer packetWBuffer = ByteBuffer.allocate(sslSession.getPacketBufferSize());

        while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED && handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
            switch (handshakeStatus) {
                case NEED_UNWRAP:
                    socketChannel.read(packetBuffer);
                    packetBuffer.flip();
                    SSLEngineResult res = sslEngine.unwrap(packetBuffer, appBuffer);
                    packetBuffer.compact();
                    handshakeStatus = res.getHandshakeStatus();
                    break;
                case NEED_WRAP:
                    packetWBuffer.clear();
                    res = sslEngine.wrap(appWBuffer, packetWBuffer);
                    handshakeStatus = res.getHandshakeStatus();
                    if (res.getStatus() == SSLEngineResult.Status.OK) {
                        packetWBuffer.flip();
                        while (packetWBuffer.hasRemaining()) {
                            socketChannel.write(packetWBuffer);
                        }
                    }
                    break;
                case NEED_TASK:
                    Runnable task;
                    while ((task = sslEngine.getDelegatedTask()) != null) {
                        new Thread(task).start();
                    }
                    handshakeStatus = sslEngine.getHandshakeStatus();
                    break;
            }
        }

        StringBuilder stringBuilder = new StringBuilder("GET " + path + " HTTP/1.1 \r\n");
        stringBuilder.append("Host: " + host + "\r\n");
        stringBuilder.append("Accept-Encoding: gzip, deflate\r\n");
        stringBuilder.append("\r\n");
        ByteBuffer byteBuffer = ByteBuffer.wrap(stringBuilder.toString().getBytes());

        packetBuffer.clear();
        SSLEngineResult res = sslEngine.wrap(byteBuffer, packetBuffer);
        if (res.getStatus() != SSLEngineResult.Status.OK) {
            throw new RuntimeException("SSL加密失败");
        }
        packetBuffer.flip();

        while (packetBuffer.hasRemaining()) {
            socketChannel.write(packetBuffer);
        }

        int num;
        byte[] body = null;
        int bodyIndex = 0;
        int headerIndex = 0;
        Integer contentLength = null;
        byte[] originHeader = new byte[1024];
        LinkedHashMap<String, List<String>> head = null;
        appBuffer.clear();
        packetBuffer.clear();
        while ((num = socketChannel.read(packetBuffer)) > -2) {
            packetBuffer.flip();
            do {
                res = sslEngine.unwrap(packetBuffer, appBuffer);
            } while (res.getStatus() == SSLEngineResult.Status.OK);
            packetBuffer.compact();
            for (int i = 0; i < appBuffer.position(); i++) {
                byte b = appBuffer.get(i);
                if (head == null) {
                    originHeader[headerIndex++] = b;
                    if (originHeader.length == headerIndex) {
                        originHeader = byteExpansion(originHeader, 1024);
                    }
                    if (originHeader[headerIndex - 1] == '\n' && originHeader[headerIndex - 2] == '\r' && originHeader[headerIndex - 3] == '\n' && originHeader[headerIndex - 4] == '\r') {
                        String headerStr = new String(originHeader);
                        String[] headerList = headerStr.split("\r\n");
                        head = Arrays.stream(headerList).skip(1).filter(h -> h.contains(":")).collect(Collectors.groupingBy(h -> h.split(":")[0].trim(), LinkedHashMap::new, Collectors.mapping(h -> h.split(":")[1].trim(), Collectors.toList())));
                        contentLength = Optional.ofNullable(head.get("Content-Length")).map(c -> Integer.parseInt(c.get(0))).orElse(-1);
                    }
                } else {
                    Integer finalContentLength = contentLength;
                    body = Optional.ofNullable(body).orElseGet(() -> new byte[finalContentLength]);
                    body[bodyIndex++] = b;
                    if (bodyIndex == contentLength) {
                        num = -2;
                        break;
                    }
                }
            }
            if (num < 0) {
                socketChannel.close();
                System.out.println(new String(originHeader));
                System.out.println(new String(uncompress(body)));
                return;
            }
            appBuffer.clear();
        }
    }

requer atenção

  • A razão pela qual sslEngine.unwrap(packetBuffer, appBuffer) é executado em um loop é que às vezes a decodificação SSL descriptografa apenas uma parte de cada vez. Se não executarmos em um loop, isso fará com que a operação de leitura continue a ser executada , o que desperdiça desempenho. Por exemplo, ao acessar o Bilibili, sempre aparecerá que os dados foram lidos, mas restarão 31 bytes na decodificação, então quando realizarmos a operação de leitura, o servidor irá esperar mais de 5 segundos (timeout) porque não há não há dados. Volte para desconectar e o cliente lerá -1. Isso significa que perdemos muito tempo até o tempo acabar. A solução é decodificá-lo novamente e os 31 bytes restantes serão decodificados.
  • Se ocorrer uma versão de registro não suportada Unknown-12 (qualquer número).49 (qualquer número) ou um erro de incompatibilidade de tag, você precisará verificar se os dados do pacote estão completos, ou seja, os dados no packetBuffer estão incompletos ou a posição e o limite estão incorretos. Verifique se os métodos relacionados de packetBuffer foram executados incorretamente.

 

Guess you like

Origin blog.csdn.net/cjc000/article/details/116998014