Spring Authorization Server 打造认证中心(零)基础篇

203 阅读11分钟

简介

Spring Authorization Server 是 Spring 官方推出的、用于构建授权服务器的新一代框架,由Spring Security OAuth升级而来,支持最新的 OAuth 2.1 规范,并内置了对 OpenID Connect (OIDC) 的支持。与 Spring Boot、Spring Security 5.x+ 及 Spring Cloud 有着天生的亲和力。通过简单的依赖和配置,就能快速搭建一个生产就绪的授权服务器,大大降低了开发复杂度。

核心功能

  • SSO(Single Sign On)单点登录
  • 搭建授权服务器
  • 身份认证中心

OAuth2协议

要更深入理解Spring Authorization Server,首先要清晰认识OAuth2的各个职责及模式。不然很容易越做越晕,不知道自己要干什么。

简介

OAuth 2.0是一种开放授权协议。它的核心目的是让你(作为资源的拥有者)能够安全地授权某个第三方应用(比如一个新上线的网站或手机App)访问你在另一个大平台(如微信、微博、Google)上存储的个人信息(如头像、昵称、好友列表等),而无需将你的平台账号和密码告诉这个第三方应用

💡 一个生活化的比喻

为了更好地理解,我们可以用一个“小区快递”的比喻:

  • 你(用户)业主(资源所有者),拥有家里的所有包裹(受保护的资源)。
  • 你家(放包裹的地方)资源服务器
  • 快递员(想送你快递的人)第三方应用(客户端) ,他的目标是把包裹送到你家。
  • 物业中心授权服务器,负责管理门禁权限。

传统(不安全)的方式:你直接把家里的门禁密码告诉快递员。这样,快递员虽然能进门,但也知道了你的密码,拥有了和你一样的权限,这显然风险很高。

OAuth 2.0(安全)的方式

  1. 快递员在门禁系统上按下“请求进入”的按钮(这相当于引导你到授权页面)。

  2. 你接到系统的电话或App通知,确认是某位快递员,并选择授权他“仅本次进入小区,且只能到你家门口”(这设定了权限范围)。

  3. 物业中心核实后,生成一个有时效性的数字门禁码(例如有效期1小时)发给快递员。这个码就是 访问令牌(Access Token)

  4. 快递员用这个临时门禁码进入小区,将包裹送到你家门口。时间一到,门禁码自动失效。

  5. 如果快递员需要多次上门(比如安装大家电),物业还可能给他一个“刷新令牌”(Refresh Token),让他在临时门禁码过期后,能申请一个新的,而不用每次都麻烦你重新授权。

🌟 核心优势

通过这个比喻,可以看出OAuth 2.0的精髓在于令牌(Token) 代替了 密码(Password) ,这带来了三大安全优势:

  1. 权限受限:令牌可以限定权限范围(比如只能读取头像,不能发布内容)。
  2. 时效性强:令牌是短期的,过期自动失效。
  3. 权责分离:令牌可被单独撤销,且不会影响你的主密码。你可以随时让某个应用的令牌失效,而不必修改平台密码。

角色

通过以上的例子我们发现,在OAuth2体系中,涉及到三个核心角色:

角色英文核心职责生活化比喻(延续快递的例子)
授权服务器Authorization Server1. 认证用户身份(让你登录) 2. 获取用户授权(问你“是否同意?”) 3. 颁发令牌(发放访问令牌和刷新令牌)物业中心/门禁系统 - 核实你的业主身份。 - 确认你同意授权。 - 生成并发放临时门禁码(令牌)。
客户端Client1. 发起授权请求(引导你去授权服务器) 2. 获取访问令牌(从授权服务器处拿到令牌) 3. 访问受保护资源(拿着令牌去资源服务器取数据)快递员 - 向物业申请进入许可。 - 从物业拿到临时门禁码。 - 凭门禁码进入小区,把包裹送到你家。
资源服务器Resource Server1. 存放用户资源(存储你的数据,如头像、邮件) 2. 验证访问令牌(检查令牌是否有效、有权限) 3. 返回请求的资源(验证通过后,提供数据)你的家和单元楼 - 里面放着你的包裹(资源)。 - 楼门禁系统会验证快递员的门禁码是否有效。 - 验证通过,开门让他把包裹送到你家门口。

对应到我们传统的前后端分离项目中,这三种角色一般为:

  • 授权服务器: 独立的认证服务,如使用Spring Authorization Server集成开发的微服务,或是Keylock这种开源方案独立部署,提供认证接口或登录页面。
  • 客户端:前端项目,用户登录系统后,前端检测到未登录(无令牌),向授权服务器发起授权请求,授权成功后最终拿到令牌(token)
  • 资源服务器:后端项目,使用Spring Security或全局拦截器拦截请求,检测其有没有携带有效的访问令牌,有则放行正常访问接口,没有则返回未授权异常(一般为401)

以上为最标准且经典的一套OAuth2体系,然而在大部分项目中,前端的话语权一般较轻,若前端作为客户端这一角色,意味着前端要完成请求授权服务器授权接口获取授权码(授权码模式)用授权码交换令牌这些操作,这对前端开发人员有着较高的要求,虽然也有现成的一些库可使用,但终究是不如spring-security-oauth2-client这种后端库来得成熟,因此,也有另一种架构就是后端兼任上客户端的职责:

  • 授权服务器: 同上
  • 客户端 + 资源服务器:后端项目,由后端集成spring-security-oauth2-client或是自行实现client逻辑,前端按经典逻辑来就可以,无需复杂改造。

而这种架构也有一些缺点:首先是如果多个客户端和授权服务器对接,每个客户端都要做一遍重复工作;其次是后端不像前端与浏览器环境直接交互,这会带来很多跨域问题。

在此基础之上,可以进一步优化,抽离出一个代理客户端出来,即代替所有前端以及后端,来向授权服务器发起授权请求以及交换令牌,以及将令牌带到浏览器,指挥重定向,作为一个调度中心,在这种架构下,三种角色又分别对应:

  • 授权服务器: 同上
  • 客户端:代理服务器,作为所有业务系统的中转和代理
  • 资源服务器:后端项目,仅校验令牌(token)即可

而我们一般在做授权服务器的时候不光希望其能授权,更希望其作为一个认证中心,所以授权服务器加代理服务器的职责又可以都做到同一个服务上,成为认证中心服务,本系列也是以这样的架构为背景而进行的一系列实践。

搞懂这些的话,再去网上看其他的讲Spring Authorization Server的博客才不会晕头转向,因为每个人的设计思路不一定是一样的,要根据场景选择合适的模式。

授权模式

清楚了要选择的架构后,再来看另一个核心的问题,上面反复提到授权这个概念,那么客户端是如何向授权服务器完成授权的?在OAuth2体系中,常规的授权模式有以下几种:

授权模式核心流程简述安全性典型应用场景
授权码模式1. 客户端引导用户至授权服务器 2. 用户同意授权,服务器返回授权码 3. 客户端用授权码向服务器换取访问令牌⭐⭐⭐⭐⭐ (最安全,令牌不暴露于浏览器)有后端服务器的传统Web应用
简化模式1. 客户端引导用户至授权服务器 2. 用户同意授权后,授权服务器直接将访问令牌返回给客户端(通常通过URL片段)⭐⭐ (令牌暴露在前端,有泄露风险)无后端的纯前端应用(如单页应用SPA)
密码模式用户将用户名和密码直接交给客户端,客户端以此向授权服务器申请令牌⭐ (需高度信任,用户凭证可能泄露)仅限于官方或高度信任的内部应用(如同一公司产品)
客户端模式客户端直接使用自身凭证(如Client ID和Secret)向授权服务器申请令牌,无用户参与⭐⭐⭐ (用于服务器间通信,不涉及用户授权)微服务间API调用、机器对机器通信

我们重点来了解这其中最重要最常用的授权码模式,其核心流程如下:

image.png

拆解来说,首先客户端向授权服务器发起授权请求:

GET /authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&state=STATE&scope=SCOPE167

授权服务器接收到该请求后,会发起重定向到授权服务器的登录页面,用户在该页面输入用户名和密码(常规情况)点击登录后,将会产生一个授权码(code),并将其携带并重定向回客户端,客户端再用这个授权码去请求授权服务器令牌接口:

POST /token HTTP/1.1 Host: authorization-server.com Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=AUTHORIZATION_CODE&redirect_uri=REDIRECT_URI&client_id=CLIENT_ID&client_secret=CLIENT_SECRET167

授权服务器校验该授权码后,会向客户端返回令牌:

HTTP/1.1 200 OK Content-Type: application/json { "access_token": "ACCESS_TOKEN", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "REFRESH_TOKEN", "scope": "read" }

到这里授权流程算是结束了。

可以发现,在这个过程中,浏览器会进行客户端 -> 登录页(授权服务器)-> 客户端 这样的重定向过程,这与单点登录的流程不谋而合,也就是说,当使用OAuth2的授权码模式进行认证时,自然而然就实现了单点登录。这也是容易混淆的一点,即 OAuth2 ≠ 单点登录,而是其中的授权码模式恰好能实现单点登录。

我们知道,单点登录仅适用于浏览器环境,如果你还有APP,那这种授权模式就行不通了,后面我们还会讲到如何去扩展授权模式,比如使用手机号+验证码来获得token,这里先有个概念即可,就是说OAuth2不仅可以做单点登录,还可以做各种各样定制化的授权,适用于浏览器、APP各种使用场景。

还有一点就是,现在主流网站都有使用OAuth2协议,如Github、Gitee、微博、微信等,你可以在各种网站的登录页发现可以使用其他社交网站登录的功能,这正是借助授权码模式实现的。

image.png

此时,被授权的页面是作为客户端,要去跳转出去的网站又作为授权服务器;如果你的站点也使用OAuth2协议,后面也可以很方便和第三方系统进行单点对接,这正是OAuth2的核心价值所在。

总结

借这篇文章总结和归纳了下Spring Authorization ServerOAuth2协议里的一些核心概念,这在我们正式开搞之前是很重要的,包括现在经常会借助AI查询一些问题,首先要清楚自己开发的功能是涉及到什么角色,自己要去干什么,后面的文章我将展示如何借助Spring Authorization Serve从零来构建一个认证中心服务,包含一个多功能登录页(类似知乎这种):包含帐号密码登录、手机验证码登录、第三方网站登录,再加上各种授权接口:如APP手机验证码授权。

题外话

在决定使用Spring Authorization Server做认证中心之前,也考虑过和Keycloak的对比选型,后者开箱即用功能很强大,但是定制性还是差了一些,而且要想用的好的话也要比较全面的去学习一下它的官方文档,给后面的人交接的复杂性也会增加,最后还是决定选用Spring Authorization Server,能与Spring Cloud生态无缝集成。