PKCE 到底防什么?公开客户端为什么不需要 client_secret?授权码被截获了怎么办?为什么 SPA 拿不到刷新令牌?这篇在 Spring Boot 4.0.8 的 Spring Authorization Server 上,用 Python 手工走完整个授权码加 PKCE 的流程并逐个触发错误。
GET /oauth2/authorize 的跳转链:302 http://127.0.0.1:9601/login → 302 http://127.0.0.1:9601/oauth2/authorize?response_type=code&client_id=de → 302 http://127.0.0.1:9999/cb?code=UDtqyGWF84Ze8NqK09nLXplR5ebTgfrZNBb_vL2P
最终回到 redirect_uri,带 code(长度 128)和 state = ['st1']
code_verifier 长度 43,code_challenge = BASE64URL(SHA256(code_verifier)),method = S256
POST /oauth2/token → HTTP 200,返回的字段 ['access_token', 'expires_in', 'id_token', 'scope', 'token_type'],token_type = Bearer,expires_in = 299,scope = read openid
access_token 是 JWT:header {'kid': '0df54381-b90c-45ac-bd35-2cbf7cdb238c', 'alg': 'RS256'};claims 的键 ['aud', 'exp', 'iat', 'iss', 'jti', 'nbf', 'scope', 'sub'];iss = http://127.0.0.1:9601;sub = alice;aud = demo;有效期 = 300 秒
id_token 的 claims 的键 ['aud', 'auth_time', 'azp', 'exp', 'iat', 'iss', 'jti', 'sid', 'sub']授权请求带 code_challenge,登录后拿到 code,换令牌时带 code_verifier;返回 JWT 形式的 access_token 与 id_token。
同一个 code 再换一次令牌 → HTTP 400 {"error":"invalid_grant"}
换令牌时 code_verifier 写错 → HTTP 400 {"error":"invalid_grant"}
换令牌时不带 code_verifier → HTTP 401
授权请求里不带 code_challenge(公开客户端要求 PKCE)→ 最终 HTTP 302,Location http://127.0.0.1:9999/cb?error=invalid_request&error_description=OAuth%202.0%20Parameter%3A%20code_challenge&error_uri=https%3A%2F%2Fdatatra
授权请求里 redirect_uri 不是注册的那个 → HTTP 500,;页面内容 "<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Tue Oct 20 12:26:28 CST 2026</div><div>Ther"
code_challenge_method = plain → 最终 HTTP 302,Location http://127.0.0.1:9999/cb?error=invalid_request&error_description=OAuth%202.0%20Parameter%3A%20code_challenge_method&error_uri=https%3A%2F%2F授权码只能用一次;code_verifier 缺失或错误换不到令牌;缺 code_challenge 或 plain 被拒绝;未注册的 redirect_uri 不重定向。
公开客户端 demo(client-authentication-methods = none)已注册 refresh_token 授权类型,scope 请求 "openid read" 时,令牌响应里有 refresh_token:False scope 再加上 offline_access 之后(仍是公开客户端):令牌响应的字段 ['access_token', 'expires_in', 'id_token', 'scope', 'token_type'] 机密客户端 conf(client_secret_basic)+ PKCE:POST /oauth2/token → HTTP 200,返回的字段 ['access_token', 'expires_in', 'id_token', 'refresh_token', 'scope', 'token_type'] 用 refresh_token 换新令牌 → HTTP 200,返回的字段 ['access_token', 'expires_in', 'id_token', 'refresh_token', 'scope', 'token_type'];新的 refresh_token 与旧的相同:True;新的 access_token 与旧的相同:False 再用同一个旧的 refresh_token 换一次 → HTTP 200 不带客户端凭据用 refresh_token → HTTP 401 /.well-known/openid-configuration → HTTP 200,issuer = http://127.0.0.1:9601,code_challenge_methods_supported = ['S256'],token_endpoint_auth_methods_supported = ['client_secret_basic', 'client_secret_post', 'client_secret_jwt', 'private_key_jwt', 'tls_client_auth', 'self_signed_tls_client_auth'],grant_types_supported = ['authorization_code', 'client_credentials', 'refresh_token', 'urn:ietf:params:oauth:grant-type:token-exchange'] /oauth2/jwks → HTTP 200,1 把密钥,kty = RSA,kid 长度 36,use = None
公开客户端拿不到 refresh_token;机密客户端换新令牌时复用同一个 refresh_token;发现文档只支持 S256。