8.7 HTTPS / SSL
Resumo
HTTPS (SSL/TLS) — protocolo de transferência segura pela internet. Criptografa o tráfego entre cliente e servidor.
Proteção: Bloqueia ataque Man-in-the-Middle, escuta de tráfego, roubo de senha e cookie.
Importante: Let's Encrypt para certificado grátis, header HSTS para forçar HTTPS, middleware TrustProxies para funcionar certo atrás do load balancer.
Conteúdo
O que é
O que é: HTTPS — HTTP por cima de SSL/TLS. Criptografa o tráfego entre cliente e servidor. Protege contra escuta e ataque MITM.
Por que precisa:
- Criptografia dos dados
- Proteção contra Man-in-the-Middle
- Boost de SEO (Google)
- Confiança do usuário
Como funciona
Pegar o certificado SSL:
# Let's Encrypt (gratuito)
sudo certbot --nginx -d example.com -d www.example.com
# Renovação automática
sudo certbot renew --dry-runConfiguração no Laravel:
// .env
APP_URL=https://example.com
// config/app.php
'url' => env('APP_URL', 'https://example.com'),
// Middleware para forçar HTTPS
// app/Http/Middleware/ForceHttps.php
class ForceHttps
{
public function handle($request, Closure $next)
{
if (!$request->secure() && app()->environment('production')) {
return redirect()->secure($request->getRequestUri());
}
return $next($request);
}
}
// Registrar no Kernel.php
protected $middleware = [
\App\Http\Middleware\ForceHttps::class,
];Trust Proxies (para load balancers):
// app/Http/Middleware/TrustProxies.php
class TrustProxies extends Middleware
{
protected $proxies = '*'; // Confiar em todos os proxies
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}Quando usar
Use sempre HTTPS:
- ✅ Production (obrigatório)
- ✅ Staging (recomendado)
- ❌ Local dev (opcional, mas dá pra usar)
Exemplo prático
Configuração Nginx:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri; # Redirect HTTP → HTTPS
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Configuração SSL
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
}HSTS (HTTP Strict Transport Security):
// app/Http/Middleware/AddSecurityHeaders.php
class AddSecurityHeaders
{
public function handle($request, Closure $next)
{
$response = $next($request);
// Força HTTPS por 1 ano
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
return $response;
}
}Mixed Content (HTTP em página HTTPS):
{{-- ❌ RUIM: HTTP em página HTTPS --}}
<script src="http://example.com/script.js"></script>
{{-- ✅ BOM: HTTPS --}}
<script src="https://example.com/script.js"></script>
{{-- ✅ BOM: URL protocol-relative --}}
<script src="//example.com/script.js"></script>
{{-- ✅ BOM: asset() usa o protocolo certo sozinho --}}
<script src="{{ asset('js/app.js') }}"></script>SSL no ambiente local:
# Valet (macOS)
valet secure example.test
# Laravel Homestead (automático)
# https://homestead.test
# mkcert (qualquer SO)
brew install mkcert
mkcert -install
mkcert example.test "*.example.test"
# Usar no nginx
ssl_certificate example.test.pem;
ssl_certificate_key example.test-key.pem;Checar a configuração SSL:
# SSL Labs
https://www.ssllabs.com/ssltest/analyze.html?d=example.com
# OpenSSL
openssl s_client -connect example.com:443
# Curl
curl -I https://example.com
# Inspecionar o certificado
openssl x509 -in cert.pem -text -nooutTratar erros de SSL:
// app/Exceptions/Handler.php
public function render($request, Throwable $exception)
{
if ($exception instanceof \Illuminate\Http\Client\RequestException) {
if (str_contains($exception->getMessage(), 'SSL')) {
Log::error('Erro SSL', ['message' => $exception->getMessage()]);
return response()->json(['error' => 'Falha na conexão SSL'], 500);
}
}
return parent::render($request, $exception);
}CloudFlare (CDN com SSL):
# Pegar o IP real via CloudFlare
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
# ... outros IP ranges do CloudFlare
real_ip_header CF-Connecting-IP;// TrustProxies para CloudFlare
protected $proxies = [
'103.21.244.0/22',
'103.22.200.0/22',
// ...
];Cookies via HTTPS:
// config/session.php
'secure' => env('SESSION_SECURE_COOKIE', true), // Só HTTPS
// Na mão
return response('Content')->cookie(
'name',
'value',
$minutes = 60,
$path = '/',
$domain = null,
$secure = true, // Só HTTPS
$httpOnly = true
);Na entrevista
"HTTPS criptografa o tráfego com SSL/TLS. Let's Encrypt para certificado grátis. Force HTTPS no middleware com redirect()->secure(). Header HSTS força HTTPS (max-age). Trust Proxies para o protocolo sair certo atrás do load balancer. asset() gera a URL certa sozinho. Mixed Content é recurso HTTP em página HTTPS — o browser bloqueia. SESSION_SECURE_COOKIE=true manda cookie só por HTTPS. SSL Labs para checar a configuração."
Exercícios práticos
Exercício 1: Crie um middleware para forçar HTTPS
Enunciado: Implemente um middleware que redireciona toda request HTTP para HTTPS em production.
Solução
// app/Http/Middleware/ForceHttps.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ForceHttps
{
/**
* Redirect para HTTPS em production
*/
public function handle(Request $request, Closure $next)
{
// Checa só em production
if (!app()->environment('local')) {
// Checa se a request não veio por HTTPS
if (!$request->secure()) {
return redirect()->secure($request->getRequestUri(), 301);
}
}
return $next($request);
}
}
// Alternativa: checar pelos headers (para load balancers)
class ForceHttps
{
public function handle(Request $request, Closure $next)
{
if (!app()->environment('local')) {
// Checa o header X-Forwarded-Proto (do load balancer)
$proto = $request->header('X-Forwarded-Proto');
if ($proto !== 'https' && !$request->secure()) {
return redirect()->secure($request->getRequestUri(), 301);
}
}
return $next($request);
}
}
// Registro no app/Http/Kernel.php
protected $middleware = [
// ...
\App\Http\Middleware\ForceHttps::class,
];
// Alternativa 2: usar o método nativo
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
if (!app()->environment('local')) {
URL::forceScheme('https');
}
}
}
// Alternativa 3: redirect no Nginx
// nginx.conf
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
// Alternativa 4: via .htaccess (Apache)
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
</IfModule>Exercício 2: Configure os Security Headers
Enunciado: Crie um middleware que adiciona todos os security headers necessários, incluindo HSTS.
Solução
// app/Http/Middleware/SecurityHeaders.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class SecurityHeaders
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
// HSTS: força HTTPS por 1 ano
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
// Previne clickjacking
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
// Previne MIME type sniffing
$response->headers->set('X-Content-Type-Options', 'nosniff');
// XSS Protection (legado, mas para browsers antigos)
$response->headers->set('X-XSS-Protection', '1; mode=block');
// Content Security Policy
$csp = implode('; ', [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
]);
$response->headers->set('Content-Security-Policy', $csp);
// Referrer Policy
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
// Permissions Policy (substitui Feature-Policy)
$permissions = implode(', ', [
'geolocation=()',
'microphone=()',
'camera=()',
'payment=()',
'usb=()',
]);
$response->headers->set('Permissions-Policy', $permissions);
return $response;
}
}
// Registro
protected $middleware = [
// ...
\App\Http\Middleware\SecurityHeaders::class,
];
// Alternativa: usar um pacote
composer require bepsvpt/secure-headers
// config/secure-headers.php (depois do publish)
return [
'hsts' => [
'enable' => true,
'max-age' => 31536000,
'include-sub-domains' => true,
'preload' => true,
],
'x-frame-options' => 'SAMEORIGIN',
'x-content-type-options' => 'nosniff',
'csp' => [
'enable' => true,
'default-src' => [
'self',
],
'script-src' => [
'self',
'unsafe-inline',
],
],
];
// Teste dos headers
class SecurityHeadersTest extends TestCase
{
public function test_security_headers_are_present(): void
{
$response = $this->get('/');
$response->assertHeader('Strict-Transport-Security');
$response->assertHeader('X-Frame-Options', 'SAMEORIGIN');
$response->assertHeader('X-Content-Type-Options', 'nosniff');
$response->assertHeader('Content-Security-Policy');
}
}Exercício 3: Configure TrustProxies para AWS/CloudFlare
Enunciado: Configure o app para funcionar certo atrás de um load balancer ou CloudFlare.
Solução
// app/Http/Middleware/TrustProxies.php
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* Proxies confiáveis
*
* Opção 1: confiar em todos os proxies (AWS ELB, GCP Load Balancer)
*/
protected $proxies = '*';
/**
* Opção 2: IPs específicos (CloudFlare)
*/
// protected $proxies = [
// // CloudFlare IPv4
// '173.245.48.0/20',
// '103.21.244.0/22',
// '103.22.200.0/22',
// '103.31.4.0/22',
// '141.101.64.0/18',
// '108.162.192.0/18',
// '190.93.240.0/20',
// '188.114.96.0/20',
// '197.234.240.0/22',
// '198.41.128.0/17',
// '162.158.0.0/15',
// '104.16.0.0/13',
// '104.24.0.0/14',
// '172.64.0.0/13',
// '131.0.72.0/22',
// ];
/**
* Headers que devem ser confiáveis
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}
// Config no .env
APP_URL=https://example.com
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=lax
// Configuração Nginx (atrás do load balancer)
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
}
// Configuração CloudFlare
// 1. No CloudFlare: SSL/TLS -> Full (Strict)
// 2. Ligar "Always Use HTTPS"
// 3. Ligar HSTS
// app/Http/Middleware/CloudFlareProxies.php
class CloudFlareProxies extends Middleware
{
protected $proxies;
public function __construct()
{
// Busca os IPs do CloudFlare dinamicamente
$this->proxies = $this->getCloudFlareIPs();
}
private function getCloudFlareIPs(): array
{
// Cache de 1 dia
return cache()->remember('cloudflare_ips', 86400, function () {
try {
$ipv4 = file_get_contents('https://www.cloudflare.com/ips-v4');
$ipv6 = file_get_contents('https://www.cloudflare.com/ips-v6');
return array_merge(
explode("\n", trim($ipv4)),
explode("\n", trim($ipv6))
);
} catch (\Exception $e) {
// Fallback: confiar em todos os proxies se não conseguir buscar
return ['*'];
}
});
}
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_CF_CONNECTING_IP; // CloudFlare header
}
// Checa se IP e protocolo foram detectados certo
Route::get('/debug-request', function (Request $request) {
return response()->json([
'ip' => $request->ip(),
'scheme' => $request->getScheme(),
'secure' => $request->secure(),
'host' => $request->getHost(),
'headers' => [
'X-Forwarded-For' => $request->header('X-Forwarded-For'),
'X-Forwarded-Proto' => $request->header('X-Forwarded-Proto'),
'X-Forwarded-Host' => $request->header('X-Forwarded-Host'),
'CF-Connecting-IP' => $request->header('CF-Connecting-IP'),
],
]);
})->middleware('auth');
// Test
class TrustProxiesTest extends TestCase
{
public function test_https_is_detected_behind_proxy(): void
{
$response = $this->withHeaders([
'X-Forwarded-Proto' => 'https',
'X-Forwarded-For' => '1.2.3.4',
])->get('/');
$this->assertTrue($response->baseResponse->isSecure());
}
}Parte do PHP/Laravel Interview Handbook | Feito com ❤️ pela equipe CodeMate