如何在Flask-Ask+Apache2+WSGI环境下验证Alexa请求?
Great question! I’ve tackled this exact scenario with Flask-Ask and Apache/WSGI before, so let’s walk through how to implement both parts of Amazon’s request validation properly. Amazon’s requirements (verifying request origin and timestamp to prevent replay attacks) are non-negotiable, even for private home skills, and we can integrate these checks directly into your Flask-Ask workflow.
Prerequisites
First, install the dependencies we’ll need for certificate handling, request parsing, and date checks:
pip install flask-ask pyopenssl requests python-dateutil
Step 1: Build Core Validation Functions
We’ll create two key functions: one to verify the request signature (ensuring it’s from Amazon) and another to check the timestamp. Here’s the code with explanations:
import requests from functools import lru_cache from datetime import datetime from dateutil import parser from OpenSSL.crypto import load_certificate, FILETYPE_PEM, verify, X509Store, X509StoreContext, X509StoreContextError from flask import Flask, request, abort from flask_ask import Ask, statement # Initialize your Flask-Ask app as usual app = Flask(__name__) ask = Ask(app, "/") # Cache certificates for 1 hour to avoid repeated downloads (adjust as needed) @lru_cache(maxsize=128, typed=False) def get_amazon_cert(cert_url): """Download and cache Amazon's signature certificate""" try: response = requests.get(cert_url, timeout=5) response.raise_for_status() return response.content except requests.exceptions.RequestException: return None def validate_alexa_signature(): """Verify the request signature and certificate chain""" # Extract required headers from the request signature = request.headers.get("Signature") cert_chain_url = request.headers.get("SignatureCertChainUrl") # Check if required headers exist if not signature or not cert_chain_url: abort(403, description="Missing signature or certificate URL") # Validate the certificate URL domain (must be Amazon-owned) allowed_domains = ("amazon.com", "amazonaws.com") if not cert_chain_url.endswith(allowed_domains): abort(403, description="Invalid certificate domain") # Get the raw request body (critical for signature verification) raw_body = request.get_data() if not raw_body: abort(403, description="Empty request body") # Download and load the certificate cert_data = get_amazon_cert(cert_chain_url) if not cert_data: abort(403, description="Failed to retrieve certificate") try: cert = load_certificate(FILETYPE_PEM, cert_data) except Exception: abort(403, description="Invalid certificate format") # Verify the certificate is issued by Amazon's root CA store = X509Store() try: store.add_cert(cert) store_ctx = X509StoreContext(store, cert) store_ctx.verify_certificate() except X509StoreContextError: abort(403, description="Invalid certificate chain") # Verify the request body signature matches the certificate try: verify( cert, signature.encode(), raw_body, "sha1" # Amazon uses SHA1-RSA for signatures; confirm with latest docs if needed ) except Exception: abort(403, description="Invalid request signature") def validate_timestamp(): """Verify the request timestamp is within the last 150 seconds (Amazon's recommended window)""" try: request_data = request.get_json() timestamp_str = request_data["request"]["timestamp"] timestamp = parser.isoparse(timestamp_str) current_time = datetime.utcnow() time_diff = (current_time - timestamp).total_seconds() if abs(time_diff) > 150: abort(403, description="Request timestamp is too old/new") except (KeyError, ValueError, TypeError): abort(403, description="Invalid or missing timestamp")
Step 2: Integrate Validation with Flask-Ask
Now we’ll add a before_request handler to run these validation checks before any skill logic is executed. This ensures every incoming Alexa request is validated automatically:
@app.before_request def alexa_request_validation(): # Only apply validation to the Flask-Ask endpoint if request.path == ask.route: validate_alexa_signature() validate_timestamp() # Example skill intent (keep your existing intents here) @ask.intent("HelloIntent") def hello(): return statement("Hello from your Alexa home skill!") if __name__ == "__main__": app.run(debug=False) # Disable debug in production!
Step 3: Apache2+WSGI Deployment Notes
Since you’re running on Apache2+WSGI, there are a few critical configurations to ensure validation works:
- Pass all request headers: In your Apache virtual host config, make sure headers like
SignatureandSignatureCertChainUrlaren’t blocked. Ensure your WSGI setup forwards all incoming headers to the app. - Raw request body access: Mod_wsgi sometimes parses the request body before your app gets it. Stick to
request.get_data()(as we did) to get the unaltered body for signature checks—avoid middleware that modifies JSON formatting or whitespace. - Enable NTP: Timestamp validation relies on accurate server time. Sync your server with an NTP service to avoid false failures from clock drift.
- Cache optimization: The
lru_cacheinget_amazon_certcaches certificates for 1 hour, but you can adjust themaxsizeor add a TTL to balance performance and certificate freshness.
Troubleshooting Tips
- Signature mismatch errors: Double-check that you’re using the raw request body. Any modification to the body (like sorted JSON keys or extra whitespace) will break the signature.
- Certificate chain failures: Ensure your server can access Amazon’s certificate URLs (no firewall blocking outgoing requests to
*.amazon.com). - Timestamp issues: If your server time is off, use
timedatectl(on Linux) to sync with a public NTP server.
内容的提问来源于stack exchange,提问作者Richard

