1. 选型
选用 spring security 5.2.x+作为框架,在 spring security 5.2.x+ 框架中进行扩展。
Spring 曾经有旧版支持 OAuth2 的方案:Spring Security OAuth 项目,该项目已经被逐步淘汰。但网上有不少仍然是这个方案,需要充分注意他们的区别。
1.1. Spring Security 5.2.x+
1.1.1. 说明
spring security 实现 OAuth 的框架分为 spring security OAuth 项目和 spring security 5.2.x 自带的 OAuth 功能,目前 spring security OAuth 已作废,官方推荐使用 spring security 5.2.x 自带的 OAuth
1.1.2. Spring Security 5.2.x+自带 OAuth
spring security 5.2.x+ 只有资源服务器和客户端,并不包含授权服务器,官方推荐使用 spring-authorization-server,目前版本 0.2.3。
1.spring security 5.2.x+
项目地址:Spring Security
GitHub:https://github.com/spring-projects/spring-security
2.授权服务器
GitHub:https://github.com/spring-projects/spring-authorization-server
1.1.3. Spring Security OAuth 项目
spring security OAuth 项目已作废
GitHub:https://github.com/spring-projects/spring-security-oauth
1.1.4. 两个项目对比分析
https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Features-Matrix
2. 核心过滤链分析
OAuth2 方案,支持在单体应用下启用。在单体应用中使用时,该应用的角色将会同时为“授权服务器”和“资源服务器”。
单体应用下,应用会同时成为“授权服务器”和“资源服务器”。分别对应两条由 Spring Security 创建的过滤器链,用于处理授权请求和资源服务的鉴权安全。
2.1. 授权服务器 SecurityFilterChain 过滤链
配置类 AuthorizationServerSecurityAutoConfiguration
该过滤器链,会匹配下图的 URL 和 Method 的请求。
在授权服务器的配置类生效后,将会对上图中的符合的 URL 和 Method 的请求进行拦截,也就是说,只要请求符合,都会进入这个过滤器链中被处理。
符合的请求,会经过上图中的过滤器链被处理。
过滤器链中包含比较多的过滤器,这里,比较重要的过滤器有如下介绍的。
2.1.1. OAuth2AuthorizationEndpointFilter
这个过滤器用来处理授权码认证过程中获取 code 的请求。
http://{{AuthorizationServer}}/oauth2/authorize?client_id=client&response_type=code&scope=testScope
若请求成功,返回的 302 跳转路径是:
http://localhost:8080/callback?code=EuO9WT96cMPoTB777pTuEk9TW5kZEuX-WKwbtZBJfLoEUlfGLBBmceP_qsEuOiypnkDL_wO0FyNlc9qtAxupFSx0hRBntifGjipA2zJNk897VoXd5wlib26W3PaRBkIw
这样,就能获取到授权码,进行下一步的认证了。
2.1.2. OAuth2ClientAuthenticationFilter
客户端认证过滤器,主要是对请求的客户端进行认证。无论 grant type 是授权码还是客户端认证,请求中都会包含 client id 和 client secret。此过滤器就是确认客户端的 client id 和 client secret 是否正确。
与其他过滤器不同,如果客户端认证成功,会把客户端信息转为 Authentication 对象,保存在 SecurityContext 中,然后流转到下一个过滤器。
2.1.3. OAuth2TokenEndpointFilter
处理不同 Grant Type,并真正颁发 Token(AccessToken 和 RefreshToken)的过滤器。这个过滤器是颁发的核心,并且处理的事情比较复杂,在后面从流程介绍时再进一步说明。
2.1.4. OAuth2TokenIntrospectionEndpointFilter
授权服务器会颁发 Token,同时,也负责要验证颁发出的 Token 的有效性。此过滤器被调用用于确认 Token 的有效性,Token 有效则返回属于这个 Token 的一些认证授权信息。
http://{{AuthorizationServer}}/oauth2/introspect?token={{AccessTokenUUID}}
2.1.5. OAuth2TokenRevocationEndpointFilter
负责 Token 的注销
http://{{AuthorizationServer}}/oauth2/revoke?token={{AccessTokenUUID}}
2.2. 资源服务器 SecurityFilterChain 过滤链
除授权服务器拦截的 url 外,其他任意请求都进资源服务器配置类的过滤器链。注意,任意请求的意思,也就是所有请求都要经过这里进行安全的鉴权和控制。这就是在 OAuth2 方案下进行鉴权的关键过滤器链了。
这个过滤器链,与传统(单体)应用下的过滤器链中的过滤器类(数量顺序)基本上是一致的。除了在资源服务器下新增的 BearerTokenAuthenticationFilter
过滤器。
2.2.1. BearerTokenAuthenticationFilter
在资源服务器的角度下,任何请求都需要验证该请求的有效性。请求中必须要附上 Token,那 Token 会经过此过滤器,调用认证管理器 AuthenticationManager 来对此 Token 进行校验(一般校验流程与上述授权服务器的 OAuth2TokenIntrospectionEndpointFilter 一致)。
成功后,则把校验成功的信息存储在 SecurityContext 中,然后转到下一步的过滤器进行鉴权。
3. 授权模式
对不同授权许可类型进行分析与流程的梳理、记录,并整理对此进行扩展补充的地方。
3.1. 客户端凭据许可
客户端凭据许可:grant_type=client_credentials
。
下面会介绍一下客户端凭据许可的使用场景。在解释的案例中,小兔软件代指一个软件应用。
客户端凭据许可主要使用在访问的资源,没有明确的资源拥有者。换句话说就是,如果小兔软件访问了一个不需要用户小明授权的数据,比如获取京东 LOGO 的图片地址,这个 LOGO 信息不属于任何一个第三方用户,再比如其它类型的第三方软件来访问平台提供的省份信息,省份信息也不属于任何一个第三方用户。
此时,在授权流程中,就不再需要资源拥有者这个角色了。当然了,你也可以形象地理解为 “资源拥有者被塞进了第三方软件中” 或者 “第三方软件就是资源拥有者”。这种场景下的授权,便是客户端凭据许可,第三方软件可以直接使用注册时的 app_id
和 app_secret
来换回访问令牌 token 的值。
还是以小明使用小兔软件为例,来看下客户端凭据许可的整个授权流程,如下图所示:
这里,图示的app_id
和app_secret
等同于client_id
和client_secret
。
3.1.1. 获取访问 token
3.1.1.1. 请求响应说明
请求格式
名称 | 说明 |
---|---|
url | /oauth2/token |
Method | POST |
Content-Type | application/x-www-form-urlencoded |
Authorization | 格式为:Basic 。 为 base64 编码的 {client_id}:{client_secret}。 |
请求参数
名称 | 说明 |
---|---|
grant_type | 授权许可类型。 固定为 client_credentials(客户端凭据许可) |
scope | 可选。申请的权限范围 |
响应
名称 | 说明 |
---|---|
access_token | 访问 token,格式为 uuid |
scope | 申请并获得授权的 scope |
token_type | token 的类型。固定值 Bearer |
expires_in | 访问 token 有效期,单位为秒 |
客户端凭据许可中,根据 OAuth2 规范,不会返回 refresh_token 。因此,如果 token 过期,则重新发起申请获取一次就可以了。
3.1.1.2. postman 测试结果
请求
POST /oauth2/token?grant_type=client_credentials&scope=test1 test2 HTTP/1.1
Host: localhost:8080
Authorization: Basic Y2xpZW50OjEyMzQ1Ng==
其中,client 客户端信息要以 Header Authorization: Basic Y2xpZW50OjEyMzQ1Ng==
发送。
Y2xpZW50OjEyMzQ1Ng==
是把client:123456
按 Base64 编码算出来的值。
响应
{
"access_token": "d9787169-3b82-4477-9c5f-8e8b640582ed",
"scope": "test2 test3",
"token_type": "Bearer",
"expires_in": "7200"
}
3.1.1.3. 请求序列图
请求序列图详细分析如下:
客户端发起请求/oauth2/token
。
<1>对客户端信息进行认证
/oauth2/token
接口需要客户端认证通过才能访问,OAuth2ClientAuthenticationFilter
(客户端认证过滤器)拦截请求,调用OAuth2ClientAuthenticationProvider
(客户端认证管理器的提供者)对传入的client_id
和client_secret
进行判断。登录认证成功后,设置认证成功的结果(OAuth2ClientAuthenticationToken
,内含客户端信息的RegisteredClient
结果)到SecurityContext
中。然后跳到下一步的过滤器中。
<2>对客户端信息进行二次认证
过滤器OAuth2TokenEndpointFilter
继续拦截此请求,然后在颁发 Token 之前,请求OAuth2ClientCredentialsAuthenticationProvider
(客户端凭据许可认证管理器的提供者)的authenticate()
方法进行第二次认证。因为第一步的OAuth2ClientAuthenticationFilter
已经校验过client_secret
,这里主要对 client 的授权模式是否吻合,以及 scope 的授权范围进行校验就通过了。
在OAuth2TokenEndpointFilter
中,调用 OAuth2XxxxAuthenticationProvider
的 authenticate()
方法进行认证是固定流程。实际会根据不同的grant_type
去选择调用不同的 OAuth2XxxxAuthenticationProvider
进行认证。因此,对客户端信息进行了二次认证,第一次和第二次的认证的职责是不通的。
<3>创建访问 Token
<3.1>开始颁发 Token:增强 Token 中包含的信息
首先是要确定 Token 中包含的信息。在OAuth2ClientCredentialsAuthenticationProvider
中通过 OAuth2TokenCustomizer
定制 token 相关信息(本质是定制 JwtEncodingContext 对象的 Claims 属性,相当于在一个 Map 中放进自定义的 Key 和 Value 值)。
<3.2>开始颁发 Token:修改 tokenValue 值为 uuid 格式
然后 JwtEncoder
根据定制之后的JwtEncodingContext
,生成 jwtAccessToken,这里的 jwtAccessToken 可以认为只是一个以 jwt 为容器存储了 token 的不同属性信息的候选 AccessToken。
<3.3>开始颁发 Token:正式颁发 OAuth2AccessToken
之后,真正颁发 AccessToken。通过 jwtAccessToken 构建 OAuth2AccessToken
对象,使用了 jwtAccessToken 的 tokenValue 值(该值是 UUID)。OAuth2AccessToken
才是真正要颁发的 AccessToken 对象。
<4>持久化 Token 及认证过程中的所有信息
为了在颁发 Token 后,能对 Token 进行验证其合法性,以及返回 Token 包含的信息,因此需要对 Token 及其相关的信息进行持久化。
结合OAuth2AccessToken
和jwtAccessToken
这两个对象,把它们中相关的属性抽出构建为OAuth2Authorization
对象(最终的这个对象包含了客户端信息、token 信息、GrantType 信息、authorizedScopes 信息,token 中的所有 Claims 信息的集合),然后通过接口OAuth2AuthorizationService
保存OAuth2Authorization
对象,因为这个对象包含的信息最完整,这里保存下来后便于后续对 Token 进行校验。
<5>返回响应信息
最终,OAuth2ClientCredentialsAuthenticationProvider
返回新 New 的OAuth2AccessTokenAuthenticationToken
对象到OAuth2TokenEndpointFilter
,这个 Filter 过滤器处理转换为返回给前端的响应OAuth2AccessTokenResponse
。
OAuth2AccessTokenResponse
对象
3.1.2. 校验 token,访问需要 API 权限的接口
第三方客户端,请求访问资源时,需要携带 token 来进行访问。
这里以访问接口 /api/users/{{user_id}}
为例说明。
3.1.2.1. postman 测试
访问需要权限的接口
将获取的 access_token 复制到请求头 Authorization 然后值为 Bearer accessToken 值。如下图所示。
3.1.2.2. token校验方式
3.1.2.3. 通过HTTP校验Token请求序列图
这里的请求序列图,主要描述在过滤器链中的过滤器如何对 token 的有效性进行判断,不会涉及对 /api/users/{{user_id}}
这个请求具体的业务处理逻辑进行描述。
请求权限保护的接口 /api/users/{{user_id}}
,接口被部署在角色为资源服务器的应用上。
在资源服务器上,
<1>BearerTokenAuthenticationFilter
认证过滤器,处理 token 请求。首先,由BearerTokenResolver
类负责从 request 中读取解析出 token 值。这里。成功读取 Token 后,封装到 BearerTokenAuthenticationToken
对象中,进行下一步对 Token 的验证。
<2>OpaqueTokenAuthenticationProvider
是 Token 的认证者。
校验 Token 成功后,此处可以扩展,比如根据授权服务器校验 token 返回的信息,组装权限信息,详细后述,这里先按流程继续分析。
<3>NimbusOpaqueTokenIntrospector
通过配置好的 url 地址/oauth2/introspect
,请求授权服务器。这个请求授权服务器的过程,有 Spring 内部处理,为了方便后续研发的理解,这里列出这个远程服务调用的 HTTP 调用的详细参数说明。
请求响应说明
请求
名称 | 说明 |
---|---|
url | /oauth2/introspect |
Method | POST |
Content-Type | application/x-www-form-urlencoded |
Authorization | Basic 值 值为 base64 [客户端 ID]:[客户端密码] |
请求参数
名称 | 说明 | |
---|---|---|
token | 访问 token |
响应
名称 | 说明 |
---|---|
active | true 表示 token 有效 false 表示无效 |
client_id | 客户端 ID |
iat | token 的签发时间 |
exp | token 的过期时间,这个过期时间必须要大于签发时间 |
scope | 申请的 scope |
token_type | token 的类型 |
nbf | 定义在什么时间之前,该 jwt 都是不可用的 |
sub | token 所面向的用户 |
aud | 接收 tokent 的一方 |
jti | token 的唯一身份标识,该值与 token 值应该一致,主要用来作为一次性 token,从而回避重放攻击。 |
当发起该请求后,后续步骤转到角色为授权服务器的应用上。
在授权服务器上:
<4>OAuth2TokenIntrospectionEndpointFilter
拦截 /oauth2/introspect
请求,调用OAuth2TokenIntrospectionAuthenticationProvider
校验 token。
<5>OAuth2TokenIntrospectionAuthenticationProvider
处理 token 校验请求,调用 OAuth2AuthorizationService
类获取 OAuth2Authorization
对象。
之后,从 Provider 中返回 token 对应的tokenClaims
认证信息。OAuth2TokenIntrospection
对象实际上就是包含tokenClaims
认证信息的对象。
再次转到资源服务器:
<6>NimbusOpaqueTokenIntrospector
接收授权服务器的校验 token 响应,组装为 OAuth2AuthenticatedPrincipal
对象。
<7>OpaqueTokenAuthenticationProvider
根据 OAuth2AuthenticatedPrincipal
信息,返回 BearerTokenAuthentication
对象
<8>BearerTokenAuthenticationFilter
设置认证对象 BearerTokenAuthentication
到 SecurityContext
中。
<9>最后,认证信息存储好,会经过 API 接口鉴权的 Filter 进行权限的鉴定,通过后,则进入业务处理的 Controller 。
3.1.3. 注销 token
3.1.3.1. 请求响应说明
请求
名称 | 说明 |
---|---|
url | /oauth2/revoke |
Method | POST |
Content-Type | application/x-www-form-urlencoded |
Authorization | Basic 值 值为 base64 [客户端 ID]:[客户端密码] |
请求参数
名称 | 说明 | |
---|---|---|
token | 访问 token,必填 |
响应
Http 状态码为 200 即成功。
3.1.3.2. postman 测试
3.1.3.3. 请求时序图
请求/oauth2/revoke
<1>/oauth2/revoke 接口需要登录才能访问,OAuth2ClientAuthenticationFilter
(客户端认证过滤器)拦截请求,调用 OAuth2ClientAuthenticationProvider
(客户端认证提供者)对传入的 clientId 和 clientSeceret 进行判断。登录认证成功后,设置认证对象到 SecurityContext 中
<2>登录成功后,OAuth2TokenRevocationEndpointFilter
拦截地址/oauth2/revoke
<3>OAuth2TokenRevocationAuthenticationProvider
处理 token 注销,调用 OAuth2AuthorizationService
类获取 OAuth2Authorization
,处理注销。处理注销的方式是对这个 token 增加一个 meta 标签"metadata.token.invalidated"=true
,并重新保存 此OAuth2Authorization
对象到 InMemory 内存或者 Redis 中。这里,通过增加 metadata 而不是直接删除存储中的 token 信息,是因为需要非常清晰明确该 token 已经被注销,如果提早删去或清理掉,则授权服务器端就无法判断该 token 最正确的状态(区分不了 token 错误 还是 token 已注销)。
<4>OAuth2TokenRevocationEndpointFilter
设置响应状态为 OK。
3.2. 授权码许可类型
3.2.1. 获取授权码
获取授权码时,会引导用户到授权页面,当未登录时,需要用户登录。登录成功后,如果客户端未配置自动授权,则会显示授权页面,如果配置自动授权,则直接跳过授权页面,302 跳转返回授权码。
这里,再分不同小节,说明 用户登录 - 授权页面 - 获取授权码 的整套流程。
3.2.1.1. 用户登录
1.请求说明
请求格式
名称 | 说明 |
---|---|
url | api/login |
Method | POST |
Content-Type | application/x-www-form-urlencoded |
请求参数
名称 | 说明 | 注意 |
---|---|---|
username | 用户名 | |
password | 用户密码 |
2.postman 测试
执行 用户登录 操作,返回两个有用的信息给浏览器和前端:
- 登录成功后的 Cookie(含 JSession),后续读取授权码需要从 Session 中读取当前用户的信息;
- LoginUserDto 的信息(JSON 对象),前端会缓存该用户信息到前端,用于判断当前是否有用户登录,用于给前端根据用户登录状态显示不同的界面。
3.2.1.2. 授权页面
授权页面,是用于呈现给用户,第三方客户端请求获取当前用户的什么权限。作为权限的拥有者(用户)必须要确认是否允许第三方客户端访问自己的信息。
因此,这个授权页面(Code 页面)是需要用户确认并做出选择的。每一个客户端的配置选项中,通过字段 requiry_user_consent
来设置是否展现该页面。
强烈建议对于第三方的客户端,都要求开启授权页面的展示。如果是受信任的第一方客户端,则可以自行决定是否展示。
如果客户端配置表中的 require_user_consent=1
,用户登录成功后的下一步,前端应当展现授权页面。
请求
名称 | 说明 |
---|---|
url | oauth2/authorize |
Method | GET |
请求参数
名称 | 说明 | 注意 |
---|---|---|
client_id | 客户端 ID | |
response_type | 授权码凭证许可固定值为 code | |
scope | 授权的 scope 编码 分配多个则用空格分隔 |
访问上述请求后,会返回授权页面 JSON 数据的请求地址,如下示例。
http://{{AuthorizationServer}}/oauth2/consent?client_id={{client}}&scope={{scope}}&state={{consentState}}
继续转去请求此地址,则会返回 JSON 格式的响应。
JSON 格式:
{
"clientId": "客户端ID",
"clientName": "客户端名称",
"principalName": "登录用户名",
"state": "state值",
"scopes": [
{
"scope": "scope编码",
"scopeName": "scope名称",
"scopeProfileInfo": "scope的简介"
}
//...
]
}
前端就可以根据上述 JSON 显示授权页面了。
前端根据授权的 JSON 响应,展示页面,在页面中会有一个“允许”的授权控制按钮,如果用户点击“允许”,则会继续发送确认授权的请求。
当用户选择授权的 scope,点击确认授权授权的请求格式如下:
请求
名称 | 说明 |
---|---|
url | oauth2/authorize |
Method | POST |
Content-Type | application/x-www-form-urlencoded |
请求参数
名称 | 说明 | 注意 |
---|---|---|
client_id | 客户端 ID | |
state | 授权页面响应的 json 中的 state 值 | |
scope | 授权的 scope 编码 分配多个则参数名有多个同名的 scope 键 |
这里需要注意,首先 method 从 GET 请求,改为 POST 请求,POST 请求必须注明是 form-urlencoded 格式的表单提交。
第二,这里的 scope 不再是用空格分割,而是会在 form 表单中用相同的键(键值就是 scope)来填写多个不同的 scope。
postman 模拟用户授权提交
可以从截图中看出,响应信息为 302 状态码,并从 Header 的 Location 中读取出授权码
3.2.1.3. 获取授权码
1.请求说明
请求
名称 | 说明 |
---|---|
url | oauth2/authorize |
Method | GET/POST |
Content-Type | application/x-www-form-urlencoded |
请求参数
名称 | 说明 | 注意 |
---|---|---|
client_id | 客户端 ID | |
response_type | 响应类型 固定为 code | |
scope | 申请的 scope 多个以空格分割 可选 | |
redirect_uri | 跳转的 uri 可选 | draft-ietf-oauth-v2-1-01 地址中本机不能是 localhost,可使用 127.0.0.1,且和客户端中的 redirect_uri 一直 |
2.响应说明
<1>当客户端配置不需要用户授权同意时,则直接跳过授权页面,直接 302 跳转
http://localhost:8080/callback?code=vNAXkiS70FzBEsZ1wyXYhDC6eRQ8W7T7kQ8ieSR97CsJzNSQxCZmcJzm6wIEAQ_toe7nIqiJ7qHzPLX4HJ-hro647F5j3ow0TX5P-k-OhcbSfzNcj9qR8S03BoOvjxjw
postman 测试获取授权码
使用 Postman 做测试的话,因为它不是一个浏览器,所以需要在 Settings 里面关闭自动跟踪重定向的新 URL 地址,确保在 Header 头的 Location 中能截取出授权码 code 的值。
<2>当客户端配置需要用户授权同意时,则会跳转到授权页面
postman 测试跳转到授权页面
使用 Postman 做测试的话,因为它不是一个浏览器,所以需要在 Settings 里面打开自动跟踪重定向的新 URL 地址以 ,JSON 格式响应。
3.请求序列图
<1>OAuth2AuthorizationEndpointFilter
处理获取授权码的相关逻辑,创建 OAuth2Authorization
,调用 OAuth2AuthorizationService
保存。
3.2.2. 用授权码获取 token
1.请求响应格式说明
请求
名称 | 说明 |
---|---|
url | /oauth2/token |
Method | POST |
Content-Type | application/x-www-form-urlencoded |
Authorization | Basic 值 值为 base64 [客户端 ID]:[客户端密码] |
请求参数
名称 | 说明 | |
---|---|---|
grant_type | 授权类型 固定为 authorization_code | |
code | 授权码的值 |
响应
名称 | 说明 |
---|---|
access_token | 访问 token |
refresh_token | 刷新 token |
scope | 申请的 scope |
token_type | token 的类型 |
expires_in | 访问 token 有效期,单位为秒 |
2.postman 测试
3.请求序列图
客户端发起请求/oauth2/token
。
<1>对客户端信息进行认证
/oauth2/token
接口需要客户端认证通过才能访问,OAuth2ClientAuthenticationFilter
(客户端认证过滤器)拦截请求,调用OAuth2ClientAuthenticationProvider
(客户端认证管理器的提供者)对传入的client_id
和client_secret
进行判断。登录认证成功后,设置认证成功的结果(OAuth2ClientAuthenticationToken
,内含客户端信息的RegisteredClient
结果)到SecurityContext
中。然后跳到下一步的过滤器中。
<2>对客户端信息进行二次认证
过滤器OAuth2TokenEndpointFilter
继续拦截此请求,然后在颁发 Token 之前,请求OAuth2AuthorizationCodeAuthenticationProvider
(授权码凭据许可认证管理器的提供者)的authenticate()
方法进行第二次认证。因为第一步的OAuth2ClientAuthenticationFilter
已经校验过client_secret
,这里主要对 client 的授权模式是否吻合,以及 scope 的授权范围进行校验就通过了。
在OAuth2TokenEndpointFilter
中,调用 OAuth2XxxxAuthenticationProvider
的 authenticate()
方法进行认证是固定流程。实际会根据不同的grant_type
去选择调用不同的 OAuth2XxxxAuthenticationProvider
进行认证。因此,对客户端信息进行了二次认证,第一次和第二次的认证的职责是不同的。
<3>创建访问 Token
<3.1>开始颁发 Token:增强 Token 中包含的信息
首先是要确定 Token 中包含的信息。在OAuth2AuthorizationCodeAuthenticationProvider
中通过 OAuth2TokenCustomizer
定制 token 相关信息(定制 JwtEncodingContext
对象的 Claims 属性)
<3.2>开始颁发 Token:修改 tokenValue 值为 uuid 格式
然后JwtEncoder
根据定制之后的JwtEncodingContext
,生成 jwtAccessToken,这里的 jwtAccessToken 可以认为只是一个以 jwt 为容器存储了 token 的不同属性信息的候选 AccessToken。
<3.3>开始颁发 Token:正式颁发 OAuth2AccessToken
之后,真正颁发 AccessToken。通过 jwtAccessToken 构建 OAuth2AccessToken
对象,使用了 jwtAccessToken 的 tokenValue 值(该值是 UUID)。OAuth2AccessToken
才是真正要颁发的 AccessToken 对象。
<4>创建刷新 Token
如果客户端支持刷新 Token,则会创建OAuth2RefreshToken
<5>持久化 Token 及认证过程中的所有信息
为了在颁发 Token 后,能对 Token 进行验证其合法性,以及返回 Token 包含的信息,因此需要对 Token 及其相关的信息进行持久化。
结合OAuth2AccessToken
和jwtAccessToken
这两个对象,把它们中相关的属性抽出构建为OAuth2Authorization
对象(最终的这个对象包含了客户端信息、token 信息、GrantType 信息、authorizedScopes 信息,token 中的所有 Claims 信息的集合),然后通过接口OAuth2AuthorizationService
保存OAuth2Authorization
对象,因为这个对象包含的信息最完整,这里保存下来后便于后续对 Token 进行校验。
<6>返回响应信息
最终,OAuth2AuthorizationCodeAuthenticationProvider
返回新 New 的OAuth2AccessTokenAuthenticationToken
对象到OAuth2TokenEndpointFilter
,这个 Filter 过滤器处理转换为返回给前端的响应OAuth2AccessTokenResponse
。
OAuth2AccessTokenResponse 对象
3.2.3. 刷新 token
1.请求响应格式说明
请求
名称 | 说明 |
---|---|
url | /oauth2/token |
Method | POST |
Content-Type | application/x-www-form-urlencoded |
Authorization | Basic 值 值为 base64 [客户端 ID]:[客户端密码] |
请求参数
名称 | 说明 | |
---|---|---|
grant_type | 授权类型 固定为 refresh_token | |
refresh_token | 刷新 refresh_token 的值 |
响应
名称 | 说明 |
---|---|
access_token | 访问 token |
refresh_token | 刷新 token |
scope | 申请的 scope |
token_type | token 的类型 |
expires_in | 访问 token 有效期,单位为秒 |
2.postman 测试
3.请求时序图
请求/oauth2/token?grant_type=refresh_token
<1>/oauth2/token 接口需要登录才能访问,OAuth2ClientAuthenticationFilter
(客户端认证过滤器)拦截请求,调用 OAuth2ClientAuthenticationProvider
(客户端认证提供者)对传入的 clientId 和 clientSeceret 进行判断。登录认证成功后,设置认证对象到 SecurityContext 中
<2>登录成功后,OAuth2TokenEndpointFilter
拦截地址/oauth2/token,请求 OAuth2RefreshTokenAuthenticationProvider
(刷新 Token 认证提供者)
<3>OAuth2RefreshTokenAuthenticationProvider
根据 refresh_token,通过 OAuth2AuthorizationService
找到 OAuth2Authorization
对象
<4>OAuth2RefreshTokenAuthenticationProvider
中生成新的访问 Token 设置到OAuth2Authorization
对象中,生成访问 token 的处理过程和获取访问 token 时处理逻辑一致
<5>OAuth2TokenEndpointFilter
将返回的 OAuth2AccessTokenAuthenticationToken
处理转换为返回给前端的响应OAuth2AccessTokenResponse
3.2.4. 校验 token,访问需要 API 权限的接口
同客户端模式,区别就是第 5 和第 6 步中的对象属性不一样
<5>OAuth2TokenIntrospection 对象
3.2.5. 注销 token
同客户端模式
3.3. 资源拥有者凭据许可
从“资源拥有者凭据许可”这个命名上,你可能就已经理解它的含义了。没错,资源拥有者的凭据,就是用户的凭据,就是用户名和密码。可见,这是最糟糕的一种方式。那为什么 OAuth 2.0 还支持这种许可类型,而且编入了 OAuth 2.0 的规范呢?
我们先来思考一下。正如上面我提到的,小兔此时就是京东官方出品的一款软件,小明也是京东的用户,那么小明其实是可以使用用户名和密码来直接使用小兔这款软件的。原因很简单,那就是这里不再有“第三方”的概念了。
但是呢,如果每次小兔都是拿着小明的用户名和密码来通过调用 Web API 的方式,来访问小明店铺的订单数据,甚至还有商品信息等,在调用这么多 API 的情况下,无疑增加了用户名和密码等敏感信息的攻击面。
如果是使用了 token 来代替这些“满天飞”的敏感信息,不就能很大程度上保护敏感信息数据了吗?这样,小兔软件只需要使用一次用户名和密码数据来换回一个 token,进而通过 token 来访问小明店铺的数据,以后就不会再使用用户名和密码了。
接下来,我们一起看下这种许可类型的流程,如下图所示:
下图的“第三方”实际上应为“第一方”
3.3.1. 获取访问 token
1.请求响应说明
请求格式
名称 | 说明 |
---|---|
url | /oauth2/token |
Method | POST |
Content-Type | application/x-www-form-urlencoded |
Authorization | 格式为:Basic 。 为 base64 编码的 {client_id}:{client_secret}。 |
请求参数
名称 | 说明 |
---|---|
grant_type | 授权许可类型。 固定为 password |
scope | 可选。申请的权限范围 |
username | 用户名 必须 |
password | 用户密码 必须 |
响应
名称 | 说明 |
---|---|
access_token | 访问 token |
refresh_token | 刷新 token |
scope | 申请的 scope |
token_type | token 的类型 |
expires_in | 访问 token 有效期,单位为秒 |
2.postman 测试
请求
POST /oauth2/token?username=guest&password=guest&grant_type=password&scope=test1 test2 HTTP/1.1
Host: localhost:8080
Authorization: Basic Y2xpZW50OjEyMzQ1Ng==
响应
{
"access_token": "747558ce-e130-4efe-b73e-7a0f5789a571",
"refresh_token": "e6DEmAQt4-1t-BrlqHz1mCtyne2ujD5drpwk_ohSQqqnaEiEgJq",
"scope": "test2 test1",
"token_type": "Bearer",
"expires_in": 7200
}
3.请求序列图
核心流程和授权码基本一致,不同之处如下:
1.OAuth2ResourceOwnerPasswordAuthenticationProvider
中会调用AuthenticationManager
验证用户名密码是否正确。
2.在授权码的第<5>步中持久化 Token 及认证过程中的所有信息 OAuth2Authorization 对象属性值不一样
3.3.2. 刷新 token
同授权码处理模式基本一样
3.3.3. 校验 token,访问需要 API 权限的接口
同授权码处理模式基本一样
3.3.4. 注销 token
同授权码处理模式基本一样
6. 附录
https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide
Announcing the Spring Authorization Server
https://github.com/spring-projects-experimental/spring-authorization-server