Spring Security OAuth2

Spring Security OAuth2

第一节 简介

Spring Security's OAuth2结构


第二节 Spring新版本迁移

  OAuth 2.0客户端和资源服务器从Spring Security OAuth 2.x移动到Spring Security 5.2.x的指南。由于Spring Security不提供授权服务器支持,因此迁移Spring Security OAuth授权服务器超出了本文档的范围

2.1 客户端 Client

  通过添加@EnableOAuth2Client注释可以启用Spring Security OAuth对Authorization Code flow的支持,而其他的flows则需要构造并公开OAuth2ClientContext实例。

  Spring Security的OAuth 2.0 Client则是通过oauth2Client的DSL方法启用支持的。

2.1.1 RestTemplate and WebClient

  Spring Security OAuth扩展了RestTemplate, 引入了OAuth2RestTemplate,这个类需要注册为@Bean并实例化。

  Spring Security则选择了支持组合并提供了OAuth2AuthorizedClientService,用来帮助创建RestTemplate拦截器或WebClient交换过滤器函数。Spring Security同时为基于Servlet-和基于WebFlux-的应用程序提供了ExchangeFilterFunction。

2.1.2 Simplified Client Resolution

  在Spring Security OAuth中检索当前授权的客户端,需要自动注入(autowire)OAuth2ClientContext实例。Spring Security OAuth通过Spring MVCs request以及session scope来存储OAuth2ClientContext实例。

  在Spring Security中检索当前授权的客户端,需要@RegisteredOAuth2AuthorizedClient方法参数注解。Spring Security 把已授权的客户端存储在OAuth2AuthorizedClientRepository中。

2.1.3 Enhanced Client Resolution

  Spring Security OAuth通过Spring Boot properties配置单个客户端。

  Spring Security使用ClientRegistrationRepository来表示客户端,客户端可以通过Spring Security DSL来提供或仍使用Spring Boot配置。

2.1.4 Simplified JWT Support

  Spring Security OAuth通过spring-security-jwt提供JWT支持。

  Spring Security则依赖于Nimbus提供JWT支持。

2.1.5 示例 Examples Matrix

  Spring Security和Spring Security OAuth2都提供了如何配置客户端的示例:

用例 Spring Security Spring Security OAuth
Authorization Code Sample Sample
Refresh Token Sample Sample
Client Credentials Sample Sample
Resource Owner Password Credentials Sample Sample

2.2 登录 Login

2.2.1 方法变更 Changes In Approach

  Spring Security称呼此功能为OAuth 2.0 Login而Spring Security OAuth则称为SSO。

  Spring Security OAuth的SSO支持通过添加@EnableOAuth2Sso注解开启。

  Spring Security的OAuth 2.0 Login支持通过oauth2Login() DSL方法启用的。

2.3 资源服务器 Resource Server

2.3.1 方法变更 Changes In Approach

  Spring Security OAuth的资源服务器支持通过添加@EnableResourceServer注解开启。

  Spring Security的资源服务器支持通过oauth2ResourceServer DSL方法启用的。

  Spring Security OAuth为资源服务器提供了两种不同的DSL方法,通过扩展ResourceServerConfigurerAdapter来配置。

  Spring Security通过Spring Security DSL提供相同的功能, 通过扩展WebSecurityConfigurerAdapter来配置。

  Spring Security OAuth为具体授权规则指定了两个位置,第一个是通过ResourceServerConfigurerAdapter - 此处的任意规则都是为存在(present)的bearer token设定,第二个是通过WebSecurityConfigurerAdapter - 此处的任意规则都是为缺席(absent)的bearer token设定。

  Spring Security则标明所有的授权规则都是通过一个或多个WebSecurityConfigurerAdapter配置而来的。

  Spring Security OAuth提供了一个自定义的SpEL变量oauth2。为了基于scope去进行授权请求或方法, 可以编写一个表达式如access(“#oauth2.hasScope(‘scope’)”)。

  Spring Security转换scope遵循授权命名规范。为了基于scope去进行授权请求或方法, 可以编写一个表达式如hasAuthority(“SCOPE_scope”)。

2.3.2 示例 Examples Matrix

用例 Spring Security Spring Security OAuth
JWT + JWK Sample Sample
JWT + Key Sample Doc
Opaque Token Sample Sample
w/ Actuator Doc Sample
Audience Validation Doc
Authorizing Requests Doc Doc

2.3.3 未移植功能 Unported Features

  There are some features that we currently have no plans to port over.

  In Spring Security OAuth, you can configure a UserDetailsService to look up a user that corresponds with the incoming bearer token. There are no plans for Spring Security’s Resource Server support to pick up a UserDetailsService. This is still simple in Spring Security, though, via the jwtAuthenticationConverter DSL method. Notably, one can return a BearerTokenAuthentication which takes an instance of OAuth2AuthenticatedPrincipal for a principal.

  In Spring Security OAuth, you can assign an identifier to the resource server via the ResourceServerSecurityConfigurer#resourceId method. This configures the realm name used by the authentication entry point as well as adds audience validation. No such identifier is planned for Spring Security. However, audience validation and a custom realm name are both simple to achieve by configuring an OAuth2TokenValidator and AuthenticationEntryPoint respectively.


第三节 实战:实现单点登录

  


第四节 实战:APP平台第三方应用授权

  首先在平台官网注册成为开发者。

  然后注册应用信息,并搭建好项目。

  OAuth时序图如下,通过OAuth来对接个人项目和APP平台,之后开发完毕后,只要保证提供一个稳定可用的线上版本即可。

OAuth时序图

  官网文档提供了OAuth2.0的authorize接口、access token接口、获取用户信息接口。

  怎么通过Spring Security配置OAuth2 Client?找了一下Spring官网Demo:OAuth-2.0-Migration-Guide,直接按Demo来实现我们的需求。

  代码就没必要再贴一遍了,了解OAuth2协议流程,对整个技术框架都会比较熟悉了,快速的实现需求后,再慢慢解决遇到的问题。

  以下为部分配置文件内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Spring-Security
spring.security.oauth2.client.registration.cpdemo-client.provider=cpdemo
spring.security.oauth2.client.registration.cpdemo-client.client-id=15764595791470995
spring.security.oauth2.client.registration.cpdemo-client.client-secret=trmCkkXvNTEtURIZjqj0yQ0A13PJ90
spring.security.oauth2.client.registration.cpdemo-client.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.cpdemo-client.client-name=TEST
spring.security.oauth2.client.registration.cpdemo-client.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.cpdemo-client.scope=get_user_info

spring.security.oauth2.client.provider.cpdemo.authorization-uri=https://www.cpdemo.com/connect/oauth2/authorize
spring.security.oauth2.client.provider.cpdemo.token-uri=https://www.cpdemo.com/connect/oauth2/token
spring.security.oauth2.client.provider.cpdemo.user-info-uri=https://api.cpdemo.com/user/campus/get_user_info
spring.security.oauth2.client.provider.cpdemo.user-name-attribute=user_name

# 解决 Possible CSRF detected - state parameter was required but no state could be found 问题
server.servlet.session.cookie.name=OAUTH2CLIENTSESSION

第五节 问题记录:得到授权code后无法获取访问令牌

1.1 问题描述

  正常访问http://localhost:7990/,向server发出授权请求,并重定向到一个PC接口需要用APP端扫码二维码,扫码成功后发生异常,返回/login?error页面提示错误msg:[authorization_request_not_found]

1.2 问题跟踪

  开始调试代码,并记录访问流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
--首先客户发起请求,收到rep重定向到/index
http://localhost:7990/
Status Code: 302
Response:
Location: http://localhost:7990/index

--请求/index,收到rep重定向到/oauth2/authorization/cpdemo-client,并用Cookie保存session:OAUTH2CLIENTSESSION
http://localhost:7990/index
Status Code: 302
Response:
Location: http://localhost:7990/oauth2/authorization/cpdemo-client
Set-Cookie: OAUTH2CLIENTSESSION=907AE53F4C3C1DA967944F7B89E295EA; Path=/; HttpOnly

--请求/oauth2/authorization/cpdemo-client,携带Cookie,收到rep重定向到www.xxx.com的授权接口,注意此时state为WiSvFEChWOuaLw0GtwigPzmRx7ocBPRDGxZJMqzAnkQ%3D
http://localhost:7990/oauth2/authorization/cpdemo-client
Status Code: 302
Request:
Cookie: OAUTH2CLIENTSESSION=907AE53F4C3C1DA967944F7B89E295EA
Response:
Location: https://www.xxx.com/connect/oauth2/authorize?response_type=code&client_id=xxx&scope=get_user_info&state=WiSvFEChWOuaLw0GtwigPzmRx7ocBPRDGxZJMqzAnkQ%3D&redirect_uri=http://localhost:7990/login/oauth2/code/cpdemo-client

--请求www.cpdemo.com的授权接口,收到rep重定向到/pc/qrcode/index.html,并用保存Cookie:acw_tc,授权服务器检测到PC端访问,跳转到一个二维码界面
https://www.cpdemo.com/connect/oauth2/authorize?response_type=code&client_id=xxx&scope=get_user_info&state=WiSvFEChWOuaLw0GtwigPzmRx7ocBPRDGxZJMqzAnkQ%3D&redirect_uri=http://localhost:7990/login/oauth2/code/cpdemo-client
Status Code: 302
Response
Location: https://www.cpdemo.com/connect/pc/qrcode/index.html?response_type=code&scope=get_user_info&client_id=15764595791470995&redirect_uri=http%3A%2F%2Flocalhost%3A7990%2Flogin%2Foauth2%2Fcode%2Fcpdemo-client&state=WiSvFEChWOuaLw0GtwigPzmRx7ocBPRDGxZJMqzAnkQ=&top_redirect=&support_free_login=&_=1577692641980#
Set-Cookie: acw_tc=76b20fec15776926419723186e2e77475066dc07c6ab3101e3cd340c96ff46;path=/;HttpOnly;Max-Age=2678401

--请求/pc/qrcode/index.html,携带Cookie,获取二维码界面
https://www.cpdemo.com/connect/pc/qrcode/index.html?response_type=code&scope=get_user_info&client_id=xxx&redirect_uri=http%3A%2F%2Flocalhost%3A7990%2Flogin%2Foauth2%2Fcode%2Fcpdemo-client&state=WiSvFEChWOuaLw0GtwigPzmRx7ocBPRDGxZJMqzAnkQ=&top_redirect=&support_free_login=&_=xxx
Status Code: 200
Request:
Cookie: acw_tc=76b20fec15776926419723186e2e77475066dc07c6ab3101e3cd340c96ff46

--扫码登陆,并携带新的参数,注意此时state值为login
https://www.cpdemo.com/connect/qrcode/jsLogin
Status Code: 200
Request:
Cookie: acw_tc=76b20fec15776926419723186e2e77475066dc07c6ab3101e3cd340c96ff46
Playload:
clientId: "xxx"
redirectUri: "http://localhost:7990/login/oauth2/code/cpdemo-client"
scope: "get_user_info"
responseType: "code"
state: "login"
supportFreeLogin: ""

--登陆成功,重定向到redirectUri:/login/oauth2/code/cpdemo-client,并携带授权code和state,请求失败,跳转error界面
http://localhost:7990/login/oauth2/code/cpdemo-client?code=cCrxmz1ds745WmV4WPYa1577692667&state=login
Request:
Cookie: OAUTH2CLIENTSESSION=907AE53F4C3C1DA967944F7B89E295EA

  再看DEBUG日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
--本地客户端服务器收到code和state
o.a.coyote.http11.Http11InputBuffer : Received [GET /login/oauth2/code/cpdemo-client?code=PYd42xxZiDccJH5K6Hmz1577674731&state=login HTTP/1.1

Host: localhost:7990
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: OAUTH2CLIENTSESSION=9F0F448755B953A5542B4E2856AC7C54

]

...

--过滤器校验

2019-12-30 15:51:44.889 DEBUG 20732 --- [nio-7990-exec-1] o.s.security.web.FilterChainProxy : /login/oauth2/code/cpdemo-client?code=PYd42xxZiDccJH5K6Hmz1577674731&state=login at position 1 of 15 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
2019-12-30 15:51:44.889 DEBUG 20732 --- [nio-7990-exec-1] o.s.security.web.FilterChainProxy : /login/oauth2/code/cpdemo-client?code=PYd42xxZiDccJH5K6Hmz1577674731&state=login at position 2 of 15 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'

--HttpSessionSecurityContextRepository提示取不到HttpSession

2019-12-30 15:51:44.889 DEBUG 20732 --- [nio-7990-exec-1] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
2019-12-30 15:51:44.889 DEBUG 20732 --- [nio-7990-exec-1] w.c.HttpSessionSecurityContextRepository : No SecurityContext was available from the HttpSession: null. A new one will be created.
2019-12-30 15:51:44.889 DEBUG 20732 --- [nio-7990-exec-1] o.s.security.web.FilterChainProxy : /login/oauth2/code/cpdemo-client?code=PYd42xxZiDccJH5K6Hmz1577674731&state=login at position 3 of 15 in additional filter chain; firing Filter: 'HeaderWriterFilter'
2019-12-30 15:51:44.889 DEBUG 20732 --- [nio-7990-exec-1] o.s.security.web.FilterChainProxy : /login/oauth2/code/cpdemo-client?code=PYd42xxZiDccJH5K6Hmz1577674731&state=login at position 4 of 15 in additional filter chain; firing Filter: 'CsrfFilter'
2019-12-30 15:51:44.889 DEBUG 20732 --- [nio-7990-exec-1] o.s.security.web.FilterChainProxy : /login/oauth2/code/cpdemo-client?code=PYd42xxZiDccJH5K6Hmz1577674731&state=login at position 5 of 15 in additional filter chain; firing Filter: 'LogoutFilter'
2019-12-30 15:51:44.889 DEBUG 20732 --- [nio-7990-exec-1] o.s.s.w.u.matcher.AntPathRequestMatcher : Request 'GET /login/oauth2/code/cpdemo-client' doesn't match 'POST /logout'
2019-12-30 15:51:44.889 DEBUG 20732 --- [nio-7990-exec-1] o.s.security.web.FilterChainProxy : /login/oauth2/code/cpdemo-client?code=PYd42xxZiDccJH5K6Hmz1577674731&state=login at position 6 of 15 in additional filter chain; firing Filter: 'OAuth2AuthorizationRequestRedirectFilter'
2019-12-30 15:51:44.889 DEBUG 20732 --- [nio-7990-exec-1] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/login/oauth2/code/cpdemo-client'; against '/oauth2/authorization/{registrationId}'
2019-12-30 15:51:44.889 DEBUG 20732 --- [nio-7990-exec-1] org.apache.tomcat.util.http.Parameters : Set encoding to UTF-8
2019-12-30 15:51:44.889 DEBUG 20732 --- [nio-7990-exec-1] org.apache.tomcat.util.http.Parameters : Decoding query null UTF-8
2019-12-30 15:51:44.889 DEBUG 20732 --- [nio-7990-exec-1] org.apache.tomcat.util.http.Parameters : Start processing with input [code=PYd42xxZiDccJH5K6Hmz1577674731&state=login]
2019-12-30 15:51:44.889 DEBUG 20732 --- [nio-7990-exec-1] o.s.security.web.FilterChainProxy : /login/oauth2/code/cpdemo-client?code=PYd42xxZiDccJH5K6Hmz1577674731&state=login at position 7 of 15 in additional filter chain; firing Filter: 'OAuth2LoginAuthenticationFilter'
2019-12-30 15:51:44.905 DEBUG 20732 --- [nio-7990-exec-1] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/login/oauth2/code/cpdemo-client'; against '/login/oauth2/code/*'

--OAuth2LoginAuthenticationFilter抛出异常OAuth2AuthenticationException:[authorization_request_not_found]

2019-12-30 15:51:44.905 DEBUG 20732 --- [nio-7990-exec-1] .s.o.c.w.OAuth2LoginAuthenticationFilter : Request is to process authentication
2019-12-30 15:51:44.912 DEBUG 20732 --- [nio-7990-exec-1] .s.o.c.w.OAuth2LoginAuthenticationFilter : Authentication request failed: org.springframework.security.oauth2.core.OAuth2AuthenticationException: [authorization_request_not_found]

org.springframework.security.oauth2.core.OAuth2AuthenticationException: [authorization_request_not_found]
at org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter.attemptAuthentication(OAuth2LoginAuthenticationFilter.java:163) ~[spring-security-oauth2-client-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212) ~[spring-security-web-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.2.1.RELEASE.jar:5.2.1.RELEASE]

2019-12-30 15:51:44.914 DEBUG 20732 --- [nio-7990-exec-1] .s.o.c.w.OAuth2LoginAuthenticationFilter : Updated SecurityContextHolder to contain null Authentication
2019-12-30 15:51:44.914 DEBUG 20732 --- [nio-7990-exec-1] .s.o.c.w.OAuth2LoginAuthenticationFilter : Delegating to authentication failure handler org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler@48edcd38
2019-12-30 15:51:44.914 DEBUG 20732 --- [nio-7990-exec-1] .a.SimpleUrlAuthenticationFailureHandler : Redirecting to /login?error
2019-12-30 15:51:44.914 DEBUG 20732 --- [nio-7990-exec-1] o.s.s.web.DefaultRedirectStrategy : Redirecting to '/login?error'
2019-12-30 15:51:44.915 DEBUG 20732 --- [nio-7990-exec-1] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@93c27ef
2019-12-30 15:51:44.915 DEBUG 20732 --- [nio-7990-exec-1] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
2019-12-30 15:51:44.915 DEBUG 20732 --- [nio-7990-exec-1] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed

  定位到异常发生的代码,部分源码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
OAuth2Error oauth2Error = new OAuth2Error("invalid_request");
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request, response);//此处获取authorizationRequest为null
if (authorizationRequest == null) {
OAuth2Error oauth2Error = new OAuth2Error("authorization_request_not_found");
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());//此处抛出异常
}
...
}
}
...
}

  继续跟踪代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public final class HttpSessionOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {

public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
Assert.notNull(request, "request cannot be null");
String stateParameter = this.getStateParameter(request);//取得state参数值为login
if (stateParameter == null) {
return null;
} else {
Map<String, OAuth2AuthorizationRequest> authorizationRequests = this.getAuthorizationRequests(request);//获得映射:"WiSvFEChWOuaLw0GtwigPzmRx7ocBPRDGxZJMqzAnkQ=" -> req
OAuth2AuthorizationRequest originalRequest = (OAuth2AuthorizationRequest)authorizationRequests.remove(stateParameter);//获得originalRequest为null
if (!authorizationRequests.isEmpty()) {
request.getSession().setAttribute(this.sessionAttributeName, authorizationRequests);
} else {
request.getSession().removeAttribute(this.sessionAttributeName);
}

return originalRequest;//返回null
}
}

...

private String getStateParameter(HttpServletRequest request) {
return request.getParameter("state");
}

private Map<String, OAuth2AuthorizationRequest> getAuthorizationRequests(HttpServletRequest request) {
HttpSession session = request.getSession(false);
Map<String, OAuth2AuthorizationRequest> authorizationRequests = session == null ? null : (Map)session.getAttribute(this.sessionAttributeName);
return (Map)(authorizationRequests == null ? new HashMap() : authorizationRequests);
}

  我们得到的session中有两个attribute:AUTHORIZATION_REQUEST和SPRING_SECURITY_SAVED_REQUEST,前者存储了一组映射(”WiSvFEChWOuaLw0GtwigPzmRx7ocBPRDGxZJMqzAnkQ=” -> req),后者则是默认Req:http://localhost:7990/index。

  所以此处是根据state参数值来获取session中存储的信息,但我们知道在首次请求授权服务器时我们提供的state为:WiSvFEChWOuaLw0GtwigPzmRx7ocBPRDGxZJMqzAnkQ=,扫码登陆后又提交了state为:login,但这里需要的是旧的state参数。

  所以要么授权服务器在扫码登陆时不要替换state为login,而是WiSv;要么授权服务器在扫码登陆后返回code和state时给旧值WiSv;要么我们客户端重构。

  联系对方工程师后,回复:“把state写死试试”。虽然有点无语,但好吧。

1.3 问题处理

1.3.1 自定义Authorization_Code_Request

  写死state,首先要自定义authorization code request,首先自定义OAuth2AuthorizationRequestResolver,覆盖state参数,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private final ClientRegistrationRepository clientRegistrationRepository;
private final AntPathRequestMatcher authorizationRequestMatcher;
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
private final StringKeyGenerator secureKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);

public CustomAuthorizationRequestResolver(
ClientRegistrationRepository clientRegistrationRepository, String authorizationRequestBaseUri) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
Assert.hasText(authorizationRequestBaseUri, "authorizationRequestBaseUri cannot be empty");
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizationRequestMatcher = new AntPathRequestMatcher(authorizationRequestBaseUri + "/{" + "registrationId" + "}");
}

@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
String registrationId = this.resolveRegistrationId(request);
String redirectUriAction = this.getAction(request, "login");
return this.resolve(request, registrationId, redirectUriAction);
}

@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId) {
if (registrationId == null) {
return null;
} else {
String redirectUriAction = this.getAction(request, "authorize");
return this.resolve(request, registrationId, redirectUriAction);
}
}

private String getAction(HttpServletRequest request, String defaultAction) {
String action = request.getParameter("action");
return action == null ? defaultAction : action;
}

private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) {
if (registrationId == null) {
return null;
} else {
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
} else {
Map<String, Object> attributes = new HashMap();
attributes.put("registration_id", clientRegistration.getRegistrationId());
OAuth2AuthorizationRequest.Builder builder;
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
builder = OAuth2AuthorizationRequest.authorizationCode();
Map<String, Object> additionalParameters = new HashMap();
if (!CollectionUtils.isEmpty(clientRegistration.getScopes()) && clientRegistration.getScopes().contains("openid")) {
this.addNonceParameters(attributes, additionalParameters);
}

if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
this.addPkceParameters(attributes, additionalParameters);
}

builder.additionalParameters(additionalParameters);
} else {
if (!AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
throw new IllegalArgumentException("Invalid Authorization Grant Type (" + clientRegistration.getAuthorizationGrantType().getValue() + ") for Client Registration with Id: " + clientRegistration.getRegistrationId());
}

builder = OAuth2AuthorizationRequest.implicit();
}

String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
// OAuth2AuthorizationRequest authorizationRequest = builder.clientId(clientRegistration.getClientId()).authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()).redirectUri(redirectUriStr).scopes(clientRegistration.getScopes()).state(this.stateGenerator.generateKey()).attributes(attributes).build();
//stateGenerator.generateKey()替换为常量login
OAuth2AuthorizationRequest authorizationRequest = builder.clientId(clientRegistration.getClientId()).authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()).redirectUri(redirectUriStr).scopes(clientRegistration.getScopes()).state("login").attributes(attributes).build();
//自定义修改请求
return customizeAuthorizationRequest(authorizationRequest);
}
}
}

//自定义请求参数
private OAuth2AuthorizationRequest customizeAuthorizationRequest(
OAuth2AuthorizationRequest req) {
Map<String,Object> extraParams = new HashMap<>();
System.out.println(req.getAdditionalParameters());
extraParams.putAll(req.getAdditionalParameters());
//覆盖state
extraParams.put("state", "login");
return OAuth2AuthorizationRequest
.from(req)
.additionalParameters(extraParams)
.build();
}

private String resolveRegistrationId(HttpServletRequest request) {
return this.authorizationRequestMatcher.matches(request) ? (String)this.authorizationRequestMatcher.matcher(request).getVariables().get("registrationId") : null;
}

private static String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration, String action) {
Map<String, String> uriVariables = new HashMap();
uriVariables.put("registrationId", clientRegistration.getRegistrationId());
UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)).replacePath(request.getContextPath()).replaceQuery((String)null).fragment((String)null).build();
String scheme = uriComponents.getScheme();
uriVariables.put("baseScheme", scheme == null ? "" : scheme);
String host = uriComponents.getHost();
uriVariables.put("baseHost", host == null ? "" : host);
int port = uriComponents.getPort();
uriVariables.put("basePort", port == -1 ? "" : ":" + port);
String path = uriComponents.getPath();
if (StringUtils.hasLength(path) && path.charAt(0) != '/') {
path = '/' + path;
}

uriVariables.put("basePath", path == null ? "" : path);
uriVariables.put("baseUrl", uriComponents.toUriString());
uriVariables.put("action", action == null ? "" : action);
return UriComponentsBuilder.fromUriString(clientRegistration.getRedirectUriTemplate()).buildAndExpand(uriVariables).toUriString();
}

private void addNonceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
try {
String nonce = this.secureKeyGenerator.generateKey();
String nonceHash = createHash(nonce);
attributes.put("nonce", nonce);
additionalParameters.put("nonce", nonceHash);
} catch (NoSuchAlgorithmException var5) {
;
}
}

private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
String codeVerifier = this.secureKeyGenerator.generateKey();
attributes.put("code_verifier", codeVerifier);

try {
String codeChallenge = createHash(codeVerifier);
additionalParameters.put("code_challenge", codeChallenge);
additionalParameters.put("code_challenge_method", "S256");
} catch (NoSuchAlgorithmException var5) {
additionalParameters.put("code_challenge", codeVerifier);
}

}

private static String createHash(String value) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII));
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
}

  在SecurityConfig中配置应用CustomAuthorizationRequestResolver。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login()
.authorizationEndpoint()
.authorizationRequestRepository(authorizationRequestRepository())
.authorizationRequestResolver(new CustomAuthorizationRequestResolver(
clientRegistrationRepository(), "/oauth2/authorization"))
...
}

@Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {
return new HttpSessionOAuth2AuthorizationRequestRepository();
}

//clientRegistrationId集合
private static List<String> clients = Arrays.asList("cpdemo-client");
private static String CLIENT_PROPERTY_KEY = "spring.security.oauth2.client.registration.";
private static String CLIENT_PROVIDER_KEY = "spring.security.oauth2.client.provider.";
@Autowired
private Environment env;

//@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
//Steam获取clients对应ClientRegistration,筛掉空值
List<ClientRegistration> registrations = clients.stream()
.map(c -> getRegistration(c))
.filter(registration -> registration != null)
.collect(Collectors.toList());
return new InMemoryClientRegistrationRepository(registrations);
}

private ClientRegistration getRegistration(String client) {
String clientId = env.getProperty(CLIENT_PROPERTY_KEY + client + ".client-id");
if (clientId == null) {
return null;
}
String clientSecret = env.getProperty(CLIENT_PROPERTY_KEY + client + ".client-secret");
if (client.equals("cpdemo-client")) {
String provider = "cpdemo";
ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(client);
return builder.clientId(clientId)
.clientSecret(clientSecret)
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUriTemplate(env.getProperty(CLIENT_PROPERTY_KEY + client + ".redirect-uri"))
.scope(env.getProperty(CLIENT_PROPERTY_KEY + client + ".scope"))
.authorizationUri(env.getProperty(CLIENT_PROVIDER_KEY + provider + ".authorization-uri"))
.tokenUri(env.getProperty(CLIENT_PROVIDER_KEY + provider + ".token-uri"))
.userInfoUri(env.getProperty(CLIENT_PROVIDER_KEY + provider + ".user-info-uri")).build();
}
return null;
}

  执行后发现之前的问题解决,但发现授权平台提供的token请求和响应格式与标准不同,需要自定义修改。

1.3.2 自定义Access_Token_Request和Access_Token_Response

  首先实现CustomRequestEntityConverter,为token请求增加参数client_id和client_secret。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CustomRequestEntityConverter implements
Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {
private static String CLIENT_PROPERTY_KEY = "spring.security.oauth2.client.registration.cpdemo-client.";
private Environment env;

private OAuth2AuthorizationCodeGrantRequestEntityConverter defaultConverter;

public CustomRequestEntityConverter(Environment env) {
defaultConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
this.env = env;
}

//自定以请求参数格式,增加client_id和client_secret
@Override
public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest req) {
RequestEntity<?> entity = defaultConverter.convert(req);
MultiValueMap<String, String> params = (MultiValueMap<String,String>) entity.getBody();
params.add("client_id", env.getProperty(CLIENT_PROPERTY_KEY + "client-id"));
params.add("client_secret", env.getProperty(CLIENT_PROPERTY_KEY + "client-secret"));
return new RequestEntity<>(params, entity.getHeaders(),
entity.getMethod(), entity.getUrl());
}
}

  然后实现CustomTokenResponseConverter,为token响应配置参数格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class CustomTokenResponseConverter implements
Converter<Map<String, String>, OAuth2AccessTokenResponse> {
private static final Set<String> TOKEN_RESPONSE_PARAMETER_NAMES = Stream.of(
OAuth2ParameterNames.ACCESS_TOKEN,
OAuth2ParameterNames.TOKEN_TYPE,
OAuth2ParameterNames.EXPIRES_IN,
OAuth2ParameterNames.REFRESH_TOKEN,
OAuth2ParameterNames.SCOPE).collect(Collectors.toSet());

//自定义OAuth2AccessTokenResponse返回参数格式
@Override
public OAuth2AccessTokenResponse convert(Map<String, String> tokenResponseParameters) {
String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);

long expiresIn = Long.valueOf(tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN));
OAuth2AccessToken.TokenType accessTokenType = OAuth2AccessToken.TokenType.BEARER;

Set<String> scopes = Collections.emptySet();
if (tokenResponseParameters.containsKey(OAuth2ParameterNames.SCOPE)) {
String scope = tokenResponseParameters.get(OAuth2ParameterNames.SCOPE);
scopes = Arrays.stream(StringUtils.delimitedListToStringArray(scope, ","))
.collect(Collectors.toSet());
}

//...
return OAuth2AccessTokenResponse.withToken(accessToken)
.tokenType(accessTokenType)
.expiresIn(expiresIn)
.scopes(scopes)
// .refreshToken(refreshToken)
// .additionalParameters(additionalParameters)
.build();
}
}

  最后修改SecurityConfig,增加token端点配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login()
.authorizationEndpoint()
.authorizationRequestRepository(authorizationRequestRepository())
.authorizationRequestResolver(new CustomAuthorizationRequestResolver(
clientRegistrationRepository(), "/oauth2/authorization"))
.and()
.tokenEndpoint()
.accessTokenResponseClient(accessTokenResponseClient())
...
}


//通过自定义OAuth2AccessTokenResponseClient来自定义token请求
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient(){
DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
new DefaultAuthorizationCodeTokenResponseClient();
//自定义req
accessTokenResponseClient.setRequestEntityConverter(new CustomRequestEntityConverter(env));

//启用OAuth2AccessTokenResponseHttpMessageConverter来转换HTTP消息,获取OAuth2AccessTokenResponse
OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter =
new OAuth2AccessTokenResponseHttpMessageConverter();

//自定义rep
tokenResponseHttpMessageConverter.setTokenResponseConverter(new CustomTokenResponseConverter());

//为Rest Http工具配置适用的HttpMessageConverter
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(), tokenResponseHttpMessageConverter));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
accessTokenResponseClient.setRestOperations(restTemplate);
return accessTokenResponseClient;
}
}

  执行后发现之前的问题解决,但获取用户信息的响应格式需要自定义。

1.3.3 自定义User_Info_Response

  创建CustomOAuth2User,对应user-info响应消息格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Data
public class CustomOAuth2User implements OAuth2User {
@JsonIgnore
private List<GrantedAuthority> authorities =
AuthorityUtils.createAuthorityList("ROLE_USER");
@JsonIgnore
private Map<String, Object> attributes;
@NonNull
@JsonProperty(required = true,value = "campus_user_id")
private String id;
@NonNull
@JsonProperty(required = true,value = "campus_id")
private String userId;
@JsonProperty(required = true,value = "tenant_code")
private String tenantCode;
@NonNull
@JsonProperty(required = true,value = "user_name")
private String name;
@JsonProperty(required = true,value = "user_type")
private String type;
@JsonProperty(required = true,value = "gender")
private String gender;
...

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}

@Override
public String getName() {
return this.name;
}

@Override
public Map<String, Object> getAttributes() {
if (this.attributes == null) {
this.attributes = new HashMap<>();
this.attributes.put("id", this.id);
this.attributes.put("userId", this.userId);
this.attributes.put("tenantCode", this.tenantCode);
this.attributes.put("name", this.name);
this.attributes.put("type", this.type);
this.attributes.put("gender", this.gender);
...
}
return attributes;
}
}

  修改SecurityConfig,增加customUserType配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login()
.authorizationEndpoint()
.authorizationRequestRepository(authorizationRequestRepository())
.authorizationRequestResolver(new CustomAuthorizationRequestResolver(
clientRegistrationRepository(), "/oauth2/authorization"))
.and()
.tokenEndpoint()
.accessTokenResponseClient(accessTokenResponseClient())
.and()
.userInfoEndpoint()
.customUserType(CustomOAuth2User.class,"cpdemo-client");
}

  执行后发现get user info流程报错,如下,user-info的响应编码格式为octet-stream,但默认的转换器不支持。

1
no suitable HttpMessageConverter found for response type [XXX] and content type [application/octet-stream]

1.3.4 自定义User_Info_Request

  自定义OAuth2UserService,为RestTemplate配置支持OCTET_STREAM(CustomOAuth2UserRequestEntityConverter此时还为OAuth2UserRequestEntityConverter)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class CustomUserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final Map<String, Class<? extends OAuth2User>> customUserTypes;
private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new CustomOAuth2UserRequestEntityConverter();
private RestOperations restOperations;

public CustomUserService(Map<String, Class<? extends OAuth2User>> customUserTypes) {
Assert.notEmpty(customUserTypes, "customUserTypes cannot be empty");
this.customUserTypes = Collections.unmodifiableMap(new LinkedHashMap(customUserTypes));
//打印RestTemplate的Http请求日志
RestTemplate restTemplate = new RestTemplate(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()));
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(new LoggingRequestInterceptor());
restTemplate.setInterceptors(interceptors);
//解决no suitable HttpMessageConverter found for response type [CustomOAuth2User] and content type [application/octet-stream]
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Arrays.asList(new MediaType[]{MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM}));
restTemplate.setMessageConverters(Arrays.asList(converter, new FormHttpMessageConverter()));
this.restOperations = restTemplate;
}

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
Assert.notNull(userRequest, "userRequest cannot be null");
String registrationId = userRequest.getClientRegistration().getRegistrationId();
Class customUserType;
if ((customUserType = (Class)this.customUserTypes.get(registrationId)) == null) {
return null;
} else {
//添加参数
System.out.println("此时access token :" + userRequest.getAccessToken().getTokenValue());
System.out.println("此时AdditionalParameters :" + userRequest.getAdditionalParameters().toString());

RequestEntity request = (RequestEntity)this.requestEntityConverter.convert(userRequest);

ResponseEntity response;
try {
response = this.restOperations.exchange(request, customUserType);
} catch (RestClientException var8) {
OAuth2Error oauth2Error = new OAuth2Error("invalid_user_info_response", "An error occurred while attempting to retrieve the UserInfo Resource: " + var8.getMessage(), (String)null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), var8);
}

OAuth2User oauth2User = (OAuth2User)response.getBody();
return oauth2User;
}
}

public final void setRequestEntityConverter(Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter) {
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
this.requestEntityConverter = requestEntityConverter;
}

public final void setRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
}
}

  通过实现ClientHttpRequestInterceptor接口来对RestTemplate打印日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class LoggingRequestInterceptor implements ClientHttpRequestInterceptor {
final static Logger log = LoggerFactory.getLogger(LoggingRequestInterceptor.class);

@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution)throws UnsupportedEncodingException,IOException {
traceRequest(httpRequest,bytes);
ClientHttpResponse response = clientHttpRequestExecution.execute(httpRequest,bytes);
traceResponse(response);
return response;
}

private void traceRequest(HttpRequest httpRequest, byte[] bytes)throws UnsupportedEncodingException {
log.info("=============================request begin==============================");
log.debug("URI : {}",httpRequest.getURI());
log.debug("Method : {}",httpRequest.getMethod());
log.debug("Headers : {}",httpRequest.getHeaders());
log.debug("Body : {}",new String(bytes,"UTF-8"));
log.info("=============================request end==============================");
}

private void traceResponse(ClientHttpResponse response)throws IOException {
StringBuilder inputStringBuilder = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(response.getBody(),"UTF-8"));
String line = reader.readLine();
while (line != null){
inputStringBuilder.append(line);
inputStringBuilder.append('\n');
line = reader.readLine();
}
log.info("=============================response begin==============================");
log.debug("StatusCode : {}",response.getStatusCode());
log.debug("StatusText : {}",response.getStatusText());
log.debug("Headers : {}",response.getHeaders());
log.debug("Body : {}",inputStringBuilder.toString());
log.info("=============================response end==============================");
}
}

  修改SecurityConfig,增加userService配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/","/home", "/login**","/callback/", "/webjars/**", "/error**", "/oauth2/authorization/**")
.permitAll()
.anyRequest()
.authenticated();

Map<String,Class<? extends OAuth2User>> customUserTypes = new HashMap<>();
customUserTypes.put("cpdemo-client",CustomOAuth2User.class);

http.oauth2Login()
.authorizationEndpoint()
.authorizationRequestRepository(authorizationRequestRepository())
.authorizationRequestResolver(new CustomAuthorizationRequestResolver(
clientRegistrationRepository(), "/oauth2/authorization"))
.and()
.tokenEndpoint()
.accessTokenResponseClient(accessTokenResponseClient())
.and()
.userInfoEndpoint()
.userService(new CustomUserService(customUserTypes))
.customUserType(CustomOAuth2User.class,"cpdemo-client");
}

@Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {
return new HttpSessionOAuth2AuthorizationRequestRepository();
}

  执行后发现提示请求Token时未提供所需参数,根据接口文档发现授权服务器要求把授权令牌写入Get_Parm,查看源码可以发现默认Converter会根据请求类别把授权令牌放入HEADER或FORM_PARM。

  自定义转换器CustomOAuth2UserRequestEntityConverter,强行将access_token拼入URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class CustomOAuth2UserRequestEntityConverter implements Converter<OAuth2UserRequest, RequestEntity<?>> {
private static final MediaType DEFAULT_CONTENT_TYPE = MediaType.valueOf("application/x-www-form-urlencoded;charset=UTF-8");

public CustomOAuth2UserRequestEntityConverter() {
}
@Override
public RequestEntity<?> convert(OAuth2UserRequest userRequest) {
ClientRegistration clientRegistration = userRequest.getClientRegistration();
HttpMethod httpMethod = HttpMethod.GET;
if (AuthenticationMethod.FORM.equals(clientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod())) {
httpMethod = HttpMethod.POST;
}

HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
// URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()).build().toUri();

//强行拼接
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()
+ "?access_token=" + userRequest.getAccessToken().getTokenValue()).build().toUri();

RequestEntity request;
if (HttpMethod.POST.equals(httpMethod)) {
headers.setContentType(DEFAULT_CONTENT_TYPE);
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap();
formParameters.add("access_token", userRequest.getAccessToken().getTokenValue());
request = new RequestEntity(formParameters, headers, httpMethod, uri);
} else {
headers.setBearerAuth(userRequest.getAccessToken().getTokenValue());
request = new RequestEntity(headers, httpMethod, uri);
}

return request;
}
}

  执行后发现问题解决,流程跑通。


参考博客和文章书籍等:

OAuth-2.0-Migration-Guide

spring-security-oauth

Spring Security OAuth2入门

深入理解Spring Cloud Security OAuth2及JWT

OAuth2实现单点登录SSO

Spring文档-26. WebClient

Spring文档-31.OAuth 2.0 Login

Spring文档中文翻译

Spring Boot OAuth 2.0 客户端

baeldung:Spring WebClient and OAuth2 Support

baeldung:Customizing Authorization and Token Requests with Spring Security 5.1 Client

github:spring-5-security-oauth

github:spring-security-oauth-5-2-migrate

Log your RestTemplate Request and Response without destroying the body

Spring Boot: Solving OAuth2 ERR_TOO_MANY_REDIRECTS [Snippet]

stackoverflow:Spring 5 Security OAuth2 Login Redirect Loop

stackoverflow:authorizationGrantType cannot be null in Spring Security 5 OAuth Client and Spring Boot 2.0

stackoverflow:Spring-boot Resttemplate response.body is null while interceptor clearly shows body

stackoverflow:Using Spring security oauth, using a custom OAuth provider, I get [authorization_request_not_found], should I handle the callback method myself?
y-shows-b “Title”)

stackoverflow:Spring RestTemplate - how to enable full debugging/logging of requests/responses?

因博客主等未标明不可引用,若部分内容涉及侵权请及时告知,我会尽快修改和删除相关内容