打开网易新闻 查看精彩图片

OAuth2每天被调用数十亿次,但90%的开发者说不清它到底在干什么。

这不是你的错。市面上99%的教程都在讲"授权码模式""隐式授权"这些黑话,却没人告诉你:OAuth2本质上是一场精心设计的"甩锅游戏"——用户不想把密码给第三方,第三方也不想存密码背锅,授权服务器正好当这个中间商赚差价。

今天我用一个完整的Java Servlet实现,把这场三方博弈拆成6个HTTP请求,每一步都能抓包验证。

OAuth2不是登录,是"代持访问权"

OAuth2不是登录,是"代持访问权"

很多人误以为OAuth2是做身份认证的。错。它解决的是:如何让A应用访问B服务器上的用户数据,而用户不用把B的密码告诉A

四个角色分工明确:资源所有者(用户)只负责点头或摇头;客户端(比如Airbnb)是求数据的;授权服务器(Google账号体系)发"临时通行证";资源服务器(Google相册API)验票放行。

用租房类比:租客(客户端)想进房东(资源服务器)的房子拿东西,但房东不认租客。这时中介(授权服务器)出面,让房东给租客一把限时钥匙(访问令牌),租客凭钥匙进门,房东只认钥匙不认人。

整个流程6步闭环:/login发起→/authorize用户确认→/callback拿授权码→/token换访问令牌→/data访问资源。下面逐行代码拆解。

Step 1:/login,客户端甩出第一口锅

Step 1:/login,客户端甩出第一口锅

用户点击"用Google登录"时,Airbnb的后台做了什么?它把用户一脚踢到Google的授权页面,同时附带三个参数:response_type=code(我要授权码)、client_id(我是谁)、redirect_uri(踢完记得还回来)。

代码极简,就一行重定向:

resp.sendRedirect("http://host/oauth/authorize?response_type=code&client_id=test&redirect_uri=...")

这一步的精髓在于"甩锅"——Airbnb说:密码的事别找我,找Google去。用户数据的事也别找我,我只是个传话的。

打开网易新闻 查看精彩图片

注意redirect_uri必须预先在授权服务器备案,否则会被拒。这是OAuth2的第一道防线:防止恶意应用劫持回调。

Step 2:/authorize,授权服务器接过锅

Step 2:/authorize,授权服务器接过锅

用户被踢到Google登录页,输入账号密码。这里Google干了两件事:验证你是谁,问你同不同意Airbnb访问你的数据。

代码层面,这是一个POST表单:

resp.getWriter().write("

User: ...Allow")

用户点击Allow后,授权服务器生成一个一次性授权码(authorization code),通过302重定向附带在URL里返回给客户端:http://airbnb.com/callback?code=xxx。

为什么非要绕这一圈?直接发令牌不行吗?这里藏着OAuth2的安全设计:授权码通过浏览器传递(前端信道),但换令牌需要客户端密钥(后端信道)。即使授权码被截获,攻击者没有密钥也换不到令牌。

Step 3:/callback,客户端拿到"临时收据"

Step 3:/callback,客户端拿到"临时收据"

Airbnb的回调地址收到code=xxx,但这张收据还不能用。它必须拿着收据+自己的密钥(client_secret),向授权服务器的/token端点发起后端请求,才能兑换真正的访问令牌。

这一步用户浏览器不参与,是服务器对服务器的通信。代码用HttpClient实现:

HttpPost post = new HttpPost("http://host/oauth/token"); post.setEntity(new UrlEncodedFormEntity(Arrays.asList(new BasicNameValuePair("grant_type", "authorization_code"), new BasicNameValuePair("code", code), new BasicNameValuePair("client_id", "test"), new BasicNameValuePair("client_secret", "secret"))));

授权服务器验证:code没过期、client_secret对得上、redirect_uri匹配。全过就签发访问令牌(access token)和刷新令牌(refresh token)。

access token通常JWT格式,含过期时间(比如3600秒)。refresh token有效期更长(比如30天),用来在access token过期后静默续期,避免频繁打扰用户。

打开网易新闻 查看精彩图片

Step 4:/data,资源服务器只认票不认人

Step 4:/data,资源服务器只认票不认人

Airbnb终于拿到令牌,可以向Google相册API请求用户头像了。请求头里加一行:Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

资源服务器的验票逻辑:

String authHeader = req.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); // 验签、验过期时间、查权限范围 }

关键点:资源服务器不需要知道用户密码,甚至不需要知道用户是谁。它只认授权服务器的签名,令牌里写明"允许访问相册只读",它就放行只读请求。

这种解耦让架构极度灵活。Google可以独立升级账号系统,Airbnb不用改代码;Airbnb可以换用Facebook登录,只需要改client_id和授权服务器地址。

那些教程不会告诉你的坑

那些教程不会告诉你的坑

state参数必须加。/login时生成随机字符串,回调时比对,防止CSRF攻击。很多Demo为了省事省略,生产环境必栽。

PKCE扩展(RFC 7636)必须加。原生App没有client_secret保密能力,PKCE用code_challenge和code_verifier替代,确保授权码只能被原请求发起者兑换。2021年后OAuth 2.1已把PKCE列为强制要求。

redirect_uri必须全字匹配。http://example.com/callback和http://example.com/callback/被视为不同地址,配置错一个字符就报错。

令牌别存浏览器。access token存内存,refresh token存httpOnly cookie,这是底线。见过有团队把refresh token放localStorage,XSS攻击一打一个准。

GitHub仓库地址在文末,含完整可运行的6个Servlet和docker-compose一键启动。建议亲手抓包看每一步的HTTP细节,比读十篇文章都管用。

这套协议2007年萌芽,2012年定稿OAuth 2.0,至今仍是互联网事实上的授权标准。但设计它的那群人可能没想到,15年后会有程序员为了搞懂它,在Stack Overflow上留下超过12万个问题——其中最高赞回答是:"别自己实现,用现成的库。"

所以最后一个问题:当你下次看到"微信登录""QQ登录"的按钮时,你能否一眼看出当前处于6步流程的哪一步?还是说,你依然选择相信那行"正在安全登录中..."的旋转动画?