全球87%的互联网服务都在用OAuth2,但90%的开发者讲不清它到底怎么跑通。
这不是夸张。你去搜教程,要么卡在概念层反复念叨"授权码模式",要么直接甩代码让你猜。用户(User)、客户端(Client)、授权服务器(Authorization Server)、资源服务器(Resource Server)——四个角色像四个部门互相踢皮球,没人告诉你它们怎么握手。
这篇文章用一个能跑的Java Servlet Demo,把OAuth2的完整链路拆开给你看。没有Spring Security封装,没有框架黑箱,纯手搓HTTP重定向和表单提交。
OAuth2不是让你"登录",是让你"授权"
很多人把OAuth2和登录混为一谈。错了。
登录是证明"你是谁",OAuth2是回答"你能不能碰我的数据"。它的核心设计是代持访问——你不需要把密码交给第三方应用,而是让信任的服务器发一张"临时通行证"(令牌)。
四个角色的分工很清晰:用户拥有数据,客户端想要数据,授权服务器验证身份并签发令牌,资源服务器保管数据、验令牌后才放行。拿"用Google账号登录Airbnb"举例:你是用户,Airbnb是客户端,Google同时扮演授权服务器和资源服务器。
这个Demo模拟的正是这个场景。我们自建一个迷你OAuth2生态,用四个Servlet分别对应四个关键端点:/login、/authorize、/callback、/token、/data。
Step 1:/login——客户端的第一声招呼
流程从用户点击"登录"开始。LoginServlet只做一件事:把浏览器踢到授权服务器。
代码里这一行是核心:
resp.sendRedirect("http://172.21.236.75:9090/oauth/authorize?response_type=code&client_id=test&redirect_uri=...")
三个参数缺一不可。response_type=code告诉授权服务器"我要授权码模式";client_id=test表明身份;redirect_uri是事后回调的地址。注意这个地址必须和授权服务器白名单里的配置完全一致,差一个斜杠都会被拒。
这里的IP用的是内网地址,说明作者是在本地集群或容器环境里测试。生产环境当然要用HTTPS和域名,但Demo故意保留这种"粗糙感"——它提醒你,OAuth2的本质是HTTP参数约定,不是魔法。
Step 2:/authorize——授权服务器的拷问
浏览器带着参数跳到/authorize,授权服务器甩出一个登录表单。
AuthorizeServlet的doGet方法直接写死了一段HTML:用户名输入框、密码输入框、允许/拒绝按钮。没有CSS,没有JS验证,最原始的Servlet输出。
这个设计刻意暴露了OAuth2的"信任边界"。用户在这里输入的密码,只交给授权服务器,客户端永远碰不到。表单提交的action指向同一个/authorize端点,但用POST方法处理。
用户点"允许"后,授权服务器生成一个授权码(Authorization Code),并通过302重定向把它送回客户端的redirect_uri。这个码只能用一次,有效期通常10分钟,且必须搭配client_secret才能换令牌——就算攻击者截获了授权码,没有客户端密钥也是废纸。
Step 3:/callback——客户端的收条
浏览器带着授权码跳回/callback,客户端终于拿到"收据"。
但这时候客户端还不能访问用户数据。它手里只有一张"兑换券",必须立刻去授权服务器的/token端点,用授权码+client_id+client_secret换真正的访问令牌(Access Token)。
这一步是OAuth2安全设计的关键:令牌绝不经过浏览器。授权码走前端通道(浏览器可见),换令牌走后端通道(客户端服务器直接请求授权服务器)。攻击者就算劫持了用户的浏览器会话,也看不到client_secret,更偷不到令牌。
Demo里的CallbackServlet会解析URL参数中的code,然后发起一个后台HTTP POST到/token。这个POST的Content-Type必须是application/x-www-form-urlencoded,body里包含grant_type=authorization_code、code=xxx、redirect_uri=xxx(必须和第一步一致,用于校验)。
Step 4:/token——令牌的铸造车间
授权服务器收到换令牌请求,先验client_secret,再查授权码是否过期、是否被用过、是否匹配这个client_id。全部通过后,签发访问令牌。
令牌通常是一个随机字符串,或者JWT(JSON Web Token)。Demo里为了简化,可能直接返回一个UUID。响应格式是标准的OAuth2令牌响应:
{ "access_token": "abc123", "token_type": "Bearer", "expires_in": 3600 }
expires_in单位是秒,告诉客户端这枚令牌1小时后失效。有些实现还会返回refresh_token,用于令牌过期后静默续期,不用用户重新登录。
客户端拿到令牌,终于可以去资源服务器要数据了。
Step 5:/data——令牌的实战检验
DataServlet扮演资源服务器。它检查HTTP请求头的Authorization: Bearer ,验签、验过期时间、查权限范围(scope),全部通过才返回用户数据。
Demo到这里闭环。五个端点、四次HTTP往返、两次用户可见的浏览器跳转、三次后端服务器之间的秘密通信——这就是授权码模式的完整骨架。
对比其他模式,授权码模式是最重的,但也是唯一推荐用于Web应用的。简化模式(Implicit)把令牌直接塞给浏览器,已经被OAuth 2.1废弃;密码模式(Password)让客户端直接收用户密码,违背了OAuth2的设计初衷;客户端凭证模式(Client Credentials)干脆没有用户参与,只用于服务器之间的API调用。
这个Java Demo的价值在于"去框架化"。Spring Security OAuth、Keycloak、Auth0这些工具把流程封装得太好,反而让人忘记底下在发生什么。手搓一遍Servlet,你会记住:授权码为什么必须一次性使用、state参数为什么能防CSRF、PKCE扩展为什么对移动端至关重要。
GitHub仓库链接在原文末尾。作者把代码按步骤拆成五个Servlet,每个都能独立编译部署。建议用Tomcat 10+(Jakarta EE命名空间)跑,旧版Servlet API需要改包名。
最后留一个问题:如果你的客户端是单页应用(SPA),没有后端服务器代收令牌,授权码模式还适用吗?OAuth 2.1给出的答案是PKCE+浏览器存储,但那个方案的安全边界,和本文Demo的后端通道设计,本质上是两套信任模型——你更愿意把令牌交给谁保管?
热门跟贴