前言
为提高人员效率,近期为已有的一些业务系统接入了统一认证,这些系统的技术栈都非常老,主要包括 C/S 架构以及一小部分 B/S 架构的。统一认证系统开放了基于 HTTP 的接口,业务系统通过与接口交互实现统一认证登录。
本文对老旧系统接入OAuth2认证的相关工作进行一个总结和记录。
OAuth2 原理
注意:以下内容由 AI 生成
首先,要理解 OAuth 2.0 的核心目标:它不是一个认证协议,而是一个授权框架。 它的主要作用是让一个应用(第三方应用)能够安全地、受限制地访问用户在另一个服务(如 Google、微信、GitHub)上存储的资源(如个人信息、联系人、照片),而无需将用户的用户名和密码交给这个第三方应用。
简单来说,OAuth 2.0 解决的是 “授权” 问题,即“应用A能否代表用户去操作应用B的某些数据?”。而我们常说的“用微信登录”是建立在 OAuth 2.0 授权基础上的 “认证” 行为。
一个生动的比喻:酒店钥匙卡
想象一下你去住酒店:
-
前台登记(认证和授权): 你出示身份证(认证),证明你是你。然后前台给你一张钥匙卡,并设定好权限(只能打开你的房间门,有效期到退房那天,这就是授权)。
-
使用设施(访问资源): 你不需要每次都去前台证明身份。要去健身房(第三方应用)时,只需在门口刷一下钥匙卡(Access Token)。健身房系统会检查:
- 这张卡是不是我们酒店发的?(Token 是否有效)
- 这张卡有没有权限进健身房?(Scope 权限范围)
- 这张卡过期了吗?(Token 是否过期)
如果检查通过,门就开了。健身房始终不知道你的身份证信息,非常安全。
OAuth 2.0 的核心参与角色
在 OAuth 2.0 流程中,有四个关键角色:
- 资源所有者 (Resource Owner): 就是用户本人。
- 客户端 (Client): 想要访问用户资源的第三方应用(比如一个想读取你 GitHub 头像的网站)。
- 授权服务器 (Authorization Server): 负责对用户进行认证,并颁发令牌(Token)的服务器。这是流程的核心(比如 Google 的登录页面和令牌颁发端点)。
- 资源服务器 (Resource Server): 存放用户受保护资源的服务器(比如 Google 的 API,存储着你的个人资料信息)。通常授权服务器和资源服务器属于同一个服务提供商。
以最常见的授权码模式举例
第三方应用如何知道“用户是谁”?
既然拿不到用户名,第三方应用怎么显示“欢迎,Alice!”呢?这里就引出了一个重要的概念:OAuth 2.0 用于授权,而 OpenID Connect (OIDC) 建立在 OAuth 2.0 之上,用于认证。
OpenID Connect (OIDC) 是一个建立在 OAuth 2.0 之上的简单身份层。它解决了“你是谁?”(认证)的问题。
-
当第三方应用在请求权限时,除了请求
scope=contacts
(访问联系人)这样的授权范围,它还可以请求一个特殊的范围:scope=openid profile email
。 -
如果用户同意,授权服务器在返回访问令牌的同时,还会返回一个 ID Token。
-
ID Token 是一个加密的、可验证的令牌(通常是 JWT 格式),里面包含了用户的身份信息,比如:
-
sub
(Subject):用户的唯一标识符。 -
name
:用户的全名。 -
email
:用户的邮箱地址(通常也作为用户名)。 -
picture
:用户的头像链接。
-
第三方应用解密这个 ID Token 后,就能安全地获取到用户的身份信息(如邮箱/名称),而无需知道用户的密码。
JWT 的结构
ID Token 是一个 JWT,由三部分组成,用点号分隔:Header.Payload.Signature
- Header(头部) :包含令牌类型和签名算法,如
{"alg": "RS256", "typ": "JWT"}
。这部分是 Base64Url 编码,不是加密。 - Payload(负载) :包含用户的声明信息,如
sub
(用户ID)、name
、email
等。这部分同样是 Base64Url 编码,不是加密。 - Signature(签名) :对前两部分的签名,用于验证令牌的完整性和来源。
重要提示:由于 Header 和 Payload 只是 Base64Url 编码,任何人都可以轻松解码并查看其中的内容。你可以把 JWT 粘贴到 jwt.io 这类调试工具中直接查看其内容。所以,JWT 本身并不隐藏信息。
遇到的问题
需要接入的业务系统中有一个是基于 SpringBoot 1.5 和 Angular 5.2 的 B/S 架构的应用,前端的路由模式使用了 HashLocationStrategy ,使得所有的 URL 中包含一个 # ,而认证系统却不支持配置携带 # 的 URL 作为 ReturnURL 。
一方面,认证系统的代码因为一些原因无法修改;另一方面业务系统因为运行维护了很多年(屎山代码),代码中包含了大量写死了的路由地址,修改业务系统的难度大,无法切换到 PathLocationStrategy。这就导致两个系统均无法向对方做出妥协。
最终使用了一种折中的方案:通过 Nginx 配置一个静态的 HTML 页面(URL 中不包含 # )接收授权码,再由这个页面将授权码转发至业务系统的前端(URL 中包含 # )去做 OAuth2 的回调操作。中转页面的代码非常简单,如下所示:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>OAuth Callback</title><script type="text/javascript">window.onload = function() {const urlParams = new URLSearchParams(window.location.search);const code = urlParams.get('code');if (code) {sessionStorage.setItem("redirectUri", window.location.origin + '/infer.html')window.location.href = `http://localhost:8000/#/oauth2/callback?code=${code}`;} else {console.error('No authorization code found in URL parameters.');window.location.href = '/'; // 或者其他错误页面}};</script>
</head>
<body>
<p>Loading...</p>
</body>
</html>
这样就可以最小限度的降低代码的修改量,也可以正常实现认证流程
结语
复杂的问题往往只需要简单的解决方法。