选用 spring security 5.2.x+作为框架,在 spring security 5.2.x+ 框架中进行扩展。
Spring 曾经有旧版支持 OAuth2 的方案:Spring Security OAuth 项目,该项目已经被逐步淘汰。但网上有不少仍然是这个方案,需要充分注意他们的区别。
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 项目已作废
项目地址: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
OAuth2 方案,支持在单体应用下启用。在单体应用中使用时,该应用的角色将会同时为“授权服务器”和“资源服务器”。
单体应用下,应用会同时成为“授权服务器”和“资源服务器”。分别对应两条由 Spring Security 创建的过滤器链,用于处理授权请求和资源服务的鉴权安全。
配置类 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}}
除授权服务器拦截的 url 外,其他任意请求都进资源服务器配置类的过滤器链。注意,任意请求的意思,也就是所有请求都要经过这里进行安全的鉴权和控制。这就是在 OAuth2 方案下进行鉴权的关键过滤器链了。
这个过滤器链,与传统(单体)应用下的过滤器链中的过滤器类(数量顺序)基本上是一致的。除了在资源服务器下新增的 BearerTokenAuthenticationFilter
过滤器。
2.2.1. BearerTokenAuthenticationFilter
在资源服务器的角度下,任何请求都需要验证该请求的有效性。请求中必须要附上 Token,那 Token 会经过此过滤器,调用认证管理器 AuthenticationManager 来对此 Token 进行校验(一般校验流程与上述授权服务器的 OAuth2TokenIntrospectionEndpointFilter 一致)。
成功后,则把校验成功的信息存储在 SecurityContext 中,然后转到下一步的过滤器进行鉴权。
对不同授权许可类型进行分析与流程的梳理、记录,并整理对此进行扩展补充的地方。
客户端凭据许可: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.1. 获取授权码
获取授权码时,会引导用户到授权页面,当未登录时,需要用户登录。登录成功后,如果客户端未配置自动授权,则会显示授权页面,如果配置自动授权,则直接跳过授权页面,302 跳转返回授权码。
这里,再分不同小节,说明 用户登录 - 授权页面 - 获取授权码 的整套流程。
3.2.1.1. 用户登录
1.请求说明
请求格式
名称 | 说明 |
---|---|
url | api/login |
Method | POST |
Content-Type | application/x-www-form-urlencoded |
请求参数
名称 | 说明 | 注意 |
---|---|---|
username | 用户名 | |
password | 用户密码 |
2.postman 测试
执行 用户登录 操作,返回两个有用的信息给浏览器和前端:
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
同客户端模式
从“资源拥有者凭据许可”这个命名上,你可能就已经理解它的含义了。没错,资源拥有者的凭据,就是用户的凭据,就是用户名和密码。可见,这是最糟糕的一种方式。那为什么 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
同授权码处理模式基本一样
Spring Security OAuth
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
oauth2.0的授权流程详解 - charlyFeng - 博客园