spring-authorization-server使用

开始写于: 2021-03-26

一、前言

Spring 官方在近日发布了一则消息:将发起一个新的名为 Spring Authorization Server 的项目。该项目是由Spring Security主导的一个社区驱动项目,旨在向 Spring 社区提供授权服务器支持。

谈及缘由,大约十年前 Spring 官方同样发起了一个社区驱动的开源项目 Spring Security OAuth, 经过社区的不懈努力该项目已经成为一个标杆项目,这完全证明社区驱动完全能够出色的完成工作。

但是由于 Spring Security OAuth 已经不能顺应 OAuth 协议的发展,很多配套设施已经陈旧,不能提供一个和 Spring 产品协调的统一的 OAuth 库。

显然,重写 Spring SecurityOAuth 支持是一项艰巨的工作。Spring 团队决定将工作分解为客户端,资源服务器和授权服务器。随着可供选择的第三方授权服务器的数量已大大增加。Spring 团队不认为创建授权服务器是常见的需求,他们也不认为在没有库支持的框架内提供授权支持是适当的。经过仔细考虑,Spring Security团队在Spring Security OAuth 路线图 中表示不再支持创建授权服务器。当这个消息发布以来,社区反响强烈,一致认为 Spring 生态系统需要对授权服务器的支持。于是 Spring Authorization Server 项目就提上了日程。

在2020年8月24号,首个版本0.0.1已经发布了,目前2021-03-06 最新的版本是 0.1.0 。

目前自己已经使用spring-authorization搭建一个登陆平台,gitee地址: https://gitee.com/xia17/xia17user/tree/master/

二、官方demo

可以在 github上找到不加赘述

image-20210326170521926

三、一些自定义

3.1 从数据库中读取Client信息

新建数据表

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
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_client
-- ----------------------------
DROP TABLE IF EXISTS `sys_client`;
CREATE TABLE `sys_client` (
`client_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '客户端Id',
`client_secret` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '客户端凭证',
`client_name` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '客户端名字',
`authentication_methods` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '身份验证方法',
`authorization_grant_types` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '授权类型',
`scopes` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '授权范围',
`redirect_uris` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '跳转地址',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
`creator` bigint(20) NULL DEFAULT NULL COMMENT '创建人编号',
`is_public` bit(1) NULL DEFAULT NULL COMMENT '是否所有人都可以访问',
`require_user_consent` bit(1) NULL DEFAULT NULL COMMENT '是否需要用户同意',
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

对应实体

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

/**
* 客户端
* @author xia17
* @since 2020/9/13 20:53
*/
@Getter
@Setter
@NoArgsConstructor
@TableName("sys_client")
public class Client extends BaseEntity {



/** 客户端ID */
@TableId(type = IdType.INPUT)
private String clientId;

/** 客户端凭证 */
private String clientSecret ;

/** 客户端名 */
private String clientName;

/** 客户端身份验证方法,多个用 英文, 号分开。可选值{basic,post,none} */
private String authenticationMethods;

/** 授权类型,多个用英文 , 号分开。可选值{authorization_code,client_credentials} */
private String authorizationGrantTypes;

/** 授权范围 多个用英文 , 号分开。 */
private String scopes;

/** 跳转地址 多个用英文 , 号分开。 */
private String redirectUris ;

/** 是否所有用户都可以访问 */
private Boolean isPublic;

/** 是否需要用户同意授权 */
private Boolean requireUserConsent;

/**
* 是否需要用户同意授权 null的时候为 false
* @return /
*/
public boolean getRequireUserConsent(){
return this.requireUserConsent == null ? false : this.requireUserConsent;
}


}

新建 CustomRegisteredClientRepository 实现 RegisteredClientRepository 接口

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
/**
* 获取客户端
* @author xia17
* @since 2020/9/15 13:59
*/
@Component
@RequiredArgsConstructor
public class CustomRegisteredClientRepository implements RegisteredClientRepository {

private final ClientService clientService;

@Override
public RegisteredClient findById(String id) {
throw new RuntimeException("该方法不知道什么时候调用,暂时不实现!");
}

/**
* 通过clientId 查找 RegisteredClient
* @param clientId /
* @return /
*/
@Override
public RegisteredClient findByClientId(String clientId) {
Client client = clientService.findByClientId(clientId);
return client == null ? null : this.of(client);
}


/**
* 将 client 转换成 RegisteredClient
* @param client /
* @return /
*/
private RegisteredClient of(Client client){
// 创建client构建器
RegisteredClient.Builder clientBuilder = RegisteredClient.withId(client.getClientId())
.clientId(client.getClientId())
.clientSecret(client.getClientSecret());

// 身份认证方法
Arrays.stream(client.getAuthenticationMethods().split(","))
.map(e -> {
if (ClientAuthenticationMethod.BASIC.getValue().equals(e)) {
return ClientAuthenticationMethod.BASIC;
} else if (ClientAuthenticationMethod.POST.getValue().equals(e)) {
return ClientAuthenticationMethod.POST;
} else if (ClientAuthenticationMethod.NONE.getValue().equals(e)) {
return ClientAuthenticationMethod.NONE;
}
throw new RuntimeException();
}).forEach(clientBuilder::clientAuthenticationMethod);

// 授权方式
Arrays.stream(client.getAuthorizationGrantTypes().split(","))
.map(e->{
if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(e)){
return AuthorizationGrantType.AUTHORIZATION_CODE;
}else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(e)){
return AuthorizationGrantType.CLIENT_CREDENTIALS;
}else if (AuthorizationGrantType.PASSWORD.getValue().equals(e)){
throw new RuntimeException("暂时不支持PASSWORD授权方式");
}else if (AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(e)){
throw new RuntimeException("暂时不支持REFRESH_TOKEN授权方式");
}
throw new RuntimeException();
}).forEach(clientBuilder::authorizationGrantType);

// 回调URI
Arrays.stream(client.getRedirectUris().split(","))
.forEach(clientBuilder::redirectUri);

// Scopes 授权范围
Arrays.stream(client.getScopes().split(","))
.forEach(clientBuilder::scope);

// 一些客户端设置
clientBuilder.clientSettings(setting->{
// 是否需要用户同意
setting.requireUserConsent(client.getRequireUserConsent());
});


// 构建
return clientBuilder.build();
}




}

这样就可以了

3.2 自定义登录界面

这个只需要自定义security 的默认表单登录即可 , 这里不讲述。

3.3 授权关键过滤器源码分析

为什么要分析这个过滤器,因为其实oauth2认证过程的主要代码,读懂了这个代码就会更好的进行自定义操作。

OAuth2AuthorizationEndpointFilter 这是源码,下面的代码是我自定义过后的。(添加了注释)

为什么下面不放源码而是放了我自定义后的代码?答:太长了不想再写一遍注释

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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ss.project.xia17user.config.security.auth2;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.ss.project.xia17user.common.utils.ResponseUtil;
import com.ss.project.xia17user.config.security.SecurityUtils;
import com.ss.project.xia17user.service.ClientService;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.*;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UriComponentsBuilder;

/**
* Oauth2 client 认证Filter
* 代码大部分来源 OAuth2AuthorizationEndpointFilter , 这里增加了判断用户是否可以访问Client的机制
* @author xia17
* @date 2021/03/25 15:38
* @see OAuth2AuthorizationEndpointFilter
* 一个{@code 过滤器} 用于OAuth 2.0授权代码授予
* 用于处理OAuth 2.0授权请求的处理。
* @author Joe Grandja
* @author Paurav Munshi
* @author Daniel Garnier-Moiroux
* @since 0.0.1
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
* @see OAuth2Authorization
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
*/
public class CustomAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for authorization requests.
* 授权请求的默认端点 {@code URI}。
*/
public static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";

/** State Token类型 */
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
private static final String PKCE_ERROR_URI = "https://tools.ietf.org/html/rfc7636#section-4.4.1";

private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
private final RequestMatcher authorizationRequestMatcher;
private final RequestMatcher userConsentMatcher;
private final StringKeyGenerator codeGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private final ClientService clientService;
private final UserConsentPage userConsentPage;

/**
* Constructs an {@code OAuth2AuthorizationEndpointFilter} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authorizationService the authorization service
*/
public CustomAuth2AuthorizationEndpointFilter(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService,
ClientService clientService ,
UserConsentPage userConsentPage) {
this(registeredClientRepository, authorizationService, DEFAULT_AUTHORIZATION_ENDPOINT_URI ,clientService ,userConsentPage);
}

/**
* Constructs an {@code OAuth2AuthorizationEndpointFilter} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authorizationService the authorization service
* @param authorizationEndpointUri the endpoint {@code URI} for authorization requests
*/
public CustomAuth2AuthorizationEndpointFilter(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService,
String authorizationEndpointUri ,
ClientService clientService ,
UserConsentPage userConsentPage) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.hasText(authorizationEndpointUri, "authorizationEndpointUri cannot be empty");
this.registeredClientRepository = registeredClientRepository;
this.authorizationService = authorizationService;
this.clientService = clientService;
this.userConsentPage = userConsentPage;
// 匹配get请求
RequestMatcher authorizationRequestGetMatcher = new AntPathRequestMatcher(
authorizationEndpointUri, HttpMethod.GET.name());
// 匹配post请求
RequestMatcher authorizationRequestPostMatcher = new AntPathRequestMatcher(
authorizationEndpointUri, HttpMethod.POST.name());
// 匹配openid 请求
RequestMatcher openidScopeMatcher = request -> {
String scope = request.getParameter(OAuth2ParameterNames.SCOPE);
return StringUtils.hasText(scope) && scope.contains(OidcScopes.OPENID);
};
// 匹配用户同意请求
RequestMatcher consentActionMatcher = request ->
request.getParameter(UserConsentPage.CONSENT_ACTION_PARAMETER_NAME) != null;
// oauth2 授权匹配
this.authorizationRequestMatcher = new OrRequestMatcher(
authorizationRequestGetMatcher,
new AndRequestMatcher(
authorizationRequestPostMatcher, openidScopeMatcher,
new NegatedRequestMatcher(consentActionMatcher)));
// 用户同意授权匹配
this.userConsentMatcher = new AndRequestMatcher(
authorizationRequestPostMatcher, consentActionMatcher);
}

/**
* 入口
* @param request /
* @param response /
* @param filterChain /
* @throws ServletException /
* @throws IOException /
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

if (this.authorizationRequestMatcher.matches(request)) {
// 执行oauth2认证过程
processAuthorizationRequest(request, response, filterChain);
} else if (this.userConsentMatcher.matches(request)) {
// 执行用户同意授权认证过程
processUserConsent(request, response);
} else {
// 下一个过滤器
filterChain.doFilter(request, response);
}
}

/**
* oauth2认证过程
* @param request /
* @param response /
* @param filterChain /
* @throws ServletException /
* @throws IOException /
*/
private void processAuthorizationRequest(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

// oauth2认证请求上下文
OAuth2AuthorizationRequestContext authorizationRequestContext =
new OAuth2AuthorizationRequestContext(
request.getRequestURL().toString(),
OAuth2EndpointUtils.getParameters(request));

// 验证请求
validateAuthorizationRequest(authorizationRequestContext);

// 是否有错误
if (authorizationRequestContext.hasError()) {
if (authorizationRequestContext.isRedirectOnError()) {
// 跳转至客户端报错
sendErrorResponse(request, response, authorizationRequestContext.resolveRedirectUri(),
authorizationRequestContext.getError(), authorizationRequestContext.getState());
} else {
// 在server中报错
sendErrorResponse(request,response, authorizationRequestContext.getError());
}
return;
}

// ---------------
// The request is valid - ensure the resource owner is authenticated
// 该请求有效-确保对资源所有者进行身份验证
// ---------------

Authentication principal = SecurityContextHolder.getContext().getAuthentication();
if (!isPrincipalAuthenticated(principal)) {
// Pass through the chain with the expectation that the authentication process 期望通过认证过程通过链
// will commence via AuthenticationEntryPoint 将通过身份验证入口点开始
// 执行下一个过滤器 , 这里说一下,security默认的登录成功后会跳转到上一个页面,也就是说,登录成功后会重新执行这个请求。从而实现登录成功后继续认证过程
filterChain.doFilter(request, response);
return;
}

// 客户端信息
RegisteredClient registeredClient = authorizationRequestContext.getRegisteredClient();

// ------------- xia17 写的以下代码 ---------------------
// 判断用户是否可以访问这个客户端 , 如果不可以访问报错
if (!clientService.checkUserClientPermission(SecurityUtils.userId(),registeredClient.getClientId())){
logger.error("当前登录的用户[" + SecurityUtils.userId() + "]不能访问ClientId[" + registeredClient.getClientId() + "]");
sendError(request,response,new OAuth2Error(OAuth2ErrorCodes.ACCESS_DENIED,"你没有权限访问该项目",""));
return;
}
// --------------xia17 写的代码结束 ---------------------

// 建立授权请求
OAuth2AuthorizationRequest authorizationRequest = authorizationRequestContext.buildAuthorizationRequest();
// 授权信息构建器
OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(principal.getName())
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.attribute(Principal.class.getName(), principal)
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest);

// 是否需要用户同意
if (requireUserConsent(registeredClient, authorizationRequest)) {
// 生成state
String state = this.stateGenerator.generateKey();
// 构建授权信息
OAuth2Authorization authorization = builder
.attribute(OAuth2ParameterNames.STATE, state)
.build();
// 将授权信息存入
this.authorizationService.save(authorization);

// TODO Need to remove 'in-flight' authorization if consent step is not completed (e.g. approved or cancelled)
// TODO 如果同意步骤未完成(例如,已批准或已取消),则需要删除“进行中”的授权

// 跳转至用户同意界面
this.userConsentPage.display(request, response, registeredClient, authorization);
} else {
// 当前时间
Instant issuedAt = Instant.now();
// +5分钟
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES);
// TODO Allow configuration for authorization code time-to-live
// TODO 允许配置授权码的生存时间
OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
this.codeGenerator.generateKey(), issuedAt, expiresAt);
// 构建code 信息
OAuth2Authorization authorization = builder
.token(authorizationCode)
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizationRequest.getScopes())
.build();
// 存入
this.authorizationService.save(authorization);

// TODO security checks for code parameter
// The authorization code MUST expire shortly after it is issued to mitigate the risk of leaks.
// A maximum authorization code lifetime of 10 minutes is RECOMMENDED.
// The client MUST NOT use the authorization code more than once.
// If an authorization code is used more than once, the authorization server MUST deny the request
// and SHOULD revoke (when possible) all tokens previously issued based on that authorization code.
// The authorization code is bound to the client identifier and redirection URI.

// TODO security checks for code parameter
// 授权码必须在发布后不久过期,以减少泄漏的风险。
// 建议最长授权码寿命为10分钟。
// 客户端不得多次使用授权码。
// 如果多次使用授权码,授权服务器必须拒绝该请求
// 并且应该(如果可能)撤消之前根据该授权码发行的所有令牌。
// 授权代码绑定到客户端标识符和重定向URI。

// 发送结果
sendAuthorizationResponse(request, response,
authorizationRequestContext.resolveRedirectUri(), authorizationCode, authorizationRequest.getState());
}
}

/**
* 是否需要用户同意
* @param registeredClient 已注册得客户端
* @param authorizationRequest /
* @return /
*/
private static boolean requireUserConsent(RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
// openid scope does not require consent
// 范围openid 不需要征得同意
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID) &&
authorizationRequest.getScopes().size() == 1) {
return false;
}
return registeredClient.getClientSettings().requireUserConsent();
}

/**
* 处理用户同意
* @param request /
* @param response /
* @throws IOException /
*/
private void processUserConsent(HttpServletRequest request, HttpServletResponse response)
throws IOException {

// 用户同意请求上下文
UserConsentRequestContext userConsentRequestContext =
new UserConsentRequestContext(
request.getRequestURL().toString(),
OAuth2EndpointUtils.getParameters(request));

// 验证请求参数
validateUserConsentRequest(userConsentRequestContext);

// 是否有错误
if (userConsentRequestContext.hasError()) {
if (userConsentRequestContext.isRedirectOnError()) {
sendErrorResponse(request, response, userConsentRequestContext.resolveRedirectUri(),
userConsentRequestContext.getError(), userConsentRequestContext.getState());
} else {
sendErrorResponse(request,response, userConsentRequestContext.getError());
}
return;
}

// 用户是否同意
if (!this.userConsentPage.isConsentApproved(request)) {
// 不同意
this.authorizationService.remove(userConsentRequestContext.getAuthorization());
OAuth2Error error = createError(OAuth2ErrorCodes.ACCESS_DENIED, "用户已取消授权");
sendErrorResponse(request, response, userConsentRequestContext.resolveRedirectUri(),
error, userConsentRequestContext.getAuthorizationRequest().getState());
return;
}

// 同意后执行授权
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES);
// TODO Allow configuration for authorization code time-to-live
OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
this.codeGenerator.generateKey(), issuedAt, expiresAt);
Set<String> authorizedScopes = userConsentRequestContext.getScopes();
if (userConsentRequestContext.getAuthorizationRequest().getScopes().contains(OidcScopes.OPENID)) {
// openid scope is auto-approved as it does not require consent
authorizedScopes.add(OidcScopes.OPENID);
}
OAuth2Authorization authorization = OAuth2Authorization.from(userConsentRequestContext.getAuthorization())
.token(authorizationCode)
.attributes(attrs -> {
attrs.remove(OAuth2ParameterNames.STATE);
attrs.put(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
})
.build();
this.authorizationService.save(authorization);

sendAuthorizationResponse(request, response, userConsentRequestContext.resolveRedirectUri(),
authorizationCode, userConsentRequestContext.getAuthorizationRequest().getState());
}

/**
* 验证请求
* @param authorizationRequestContext /
*/
private void validateAuthorizationRequest(OAuth2AuthorizationRequestContext authorizationRequestContext) {
// ---------------
// Validate the request to ensure all required parameters are present and valid
// 验证请求以确保所有必需参数都存在并有效
// ---------------

// client_id (REQUIRED)
if (!StringUtils.hasText(authorizationRequestContext.getClientId()) ||
authorizationRequestContext.getParameters().get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
authorizationRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_REQUEST, "client_id 参数异常"));
return;
}
// 获取client信息
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
authorizationRequestContext.getClientId());
if (registeredClient == null) {
authorizationRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_REQUEST, "没有找到已注册的客户端:" + authorizationRequestContext.getClientId()));
return;
} else if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
// 0.1.0 也只支持 AUTHORIZATION_CODE 授权
authorizationRequestContext.setError(
createError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, "暂时支持AUTHORIZATION_CODE方式授权"));
return;
}
authorizationRequestContext.setRegisteredClient(registeredClient);

// redirect_uri (OPTIONAL) 跳转地址 可选
if (StringUtils.hasText(authorizationRequestContext.getRedirectUri())) {
if (!registeredClient.getRedirectUris().contains(authorizationRequestContext.getRedirectUri()) ||
authorizationRequestContext.getParameters().get(OAuth2ParameterNames.REDIRECT_URI).size() != 1) {
authorizationRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_REQUEST, "授权回调地址异常"));
return;
}
} else if (authorizationRequestContext.isAuthenticationRequest() || // redirect_uri is REQUIRED for OpenID Connect
registeredClient.getRedirectUris().size() != 1) {
authorizationRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI));
return;
}
authorizationRequestContext.setRedirectOnError(true);

// response_type (REQUIRED) response_type(必填)
if (!StringUtils.hasText(authorizationRequestContext.getResponseType()) ||
authorizationRequestContext.getParameters().get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
authorizationRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE +"参数异常"));
return;
} else if (!authorizationRequestContext.getResponseType().equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
authorizationRequestContext.setError(
createError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE+"参数异常"));
return;
}

// scope (OPTIONAL) 范围(可选)
Set<String> requestedScopes = authorizationRequestContext.getScopes();
Set<String> allowedScopes = registeredClient.getScopes();
if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
authorizationRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_SCOPE, "授权范围异常"));
return;
}

// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE) code_challenge(公共客户端需要)
String codeChallenge = authorizationRequestContext.getParameters().getFirst(PkceParameterNames.CODE_CHALLENGE);
if (StringUtils.hasText(codeChallenge)) {
if (authorizationRequestContext.getParameters().get(PkceParameterNames.CODE_CHALLENGE).size() != 1) {
authorizationRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI));
return;
}

String codeChallengeMethod = authorizationRequestContext.getParameters().getFirst(PkceParameterNames.CODE_CHALLENGE_METHOD);
if (StringUtils.hasText(codeChallengeMethod)) {
if (authorizationRequestContext.getParameters().get(PkceParameterNames.CODE_CHALLENGE_METHOD).size() != 1 ||
(!"S256".equals(codeChallengeMethod) && !"plain".equals(codeChallengeMethod))) {
authorizationRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI));
}
}
} else if (registeredClient.getClientSettings().requireProofKey()) {
authorizationRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI));
}
}

/**
* 验证用户同意请求
* @param userConsentRequestContext /
*/
private void validateUserConsentRequest(UserConsentRequestContext userConsentRequestContext) {
// ---------------
// Validate the request to ensure all required parameters are present and valid
// ---------------

// state (REQUIRED)
if (!StringUtils.hasText(userConsentRequestContext.getState()) ||
userConsentRequestContext.getParameters().get(OAuth2ParameterNames.STATE).size() != 1) {
userConsentRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE+"参数异常"));
return;
}
OAuth2Authorization authorization = this.authorizationService.findByToken(
userConsentRequestContext.getState(), STATE_TOKEN_TYPE);
if (authorization == null) {
userConsentRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_REQUEST, "没有找到state"));
return;
}
userConsentRequestContext.setAuthorization(authorization);

// The 'in-flight' authorization must be associated to the current principal
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
userConsentRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_REQUEST, "登录已经失效"));
return;
}

// client_id (REQUIRED)
if (!StringUtils.hasText(userConsentRequestContext.getClientId()) ||
userConsentRequestContext.getParameters().get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
userConsentRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_REQUEST, "client_id参数异常" ));
return;
}
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
userConsentRequestContext.getClientId());
if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
userConsentRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_REQUEST, "没有找到已注册的客户端:" + userConsentRequestContext.getClientId()));
return;
}
userConsentRequestContext.setRegisteredClient(registeredClient);
userConsentRequestContext.setRedirectOnError(true);

// scope (OPTIONAL)
Set<String> requestedScopes = userConsentRequestContext.getAuthorizationRequest().getScopes();
Set<String> authorizedScopes = userConsentRequestContext.getScopes();
if (!authorizedScopes.isEmpty() && !requestedScopes.containsAll(authorizedScopes)) {
userConsentRequestContext.setError(
createError(OAuth2ErrorCodes.INVALID_SCOPE,"授权范围异常"));
return;
}
}

private void sendAuthorizationResponse(HttpServletRequest request, HttpServletResponse response,
String redirectUri, OAuth2AuthorizationCode authorizationCode, String state) throws IOException {

UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(redirectUri)
.queryParam(OAuth2ParameterNames.CODE, authorizationCode.getTokenValue());
if (StringUtils.hasText(state)) {
uriBuilder.queryParam(OAuth2ParameterNames.STATE, state);
}
this.redirectStrategy.sendRedirect(request, response, uriBuilder.toUriString());
}

private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
String redirectUri, OAuth2Error error, String state) throws IOException {

UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(redirectUri)
.queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode());
if (StringUtils.hasText(error.getDescription())) {
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription());
}
if (StringUtils.hasText(error.getUri())) {
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_URI, error.getUri());
}
if (StringUtils.hasText(state)) {
uriBuilder.queryParam(OAuth2ParameterNames.STATE, state);
}
this.redirectStrategy.sendRedirect(request, response, uriBuilder.toUriString());
}

private void sendErrorResponse(HttpServletRequest request ,HttpServletResponse response, OAuth2Error error) throws IOException {
// TODO Send default html error response
// response.sendError(HttpStatus.BAD_REQUEST.value(), error.toString());
this.sendError(request,response,error);
}

/**
* 抛出异常
* @param error /
*/
private void sendError(HttpServletRequest request, HttpServletResponse response, OAuth2Error error){
ResponseUtil.sendError(request,response,new OAuth2AuthenticationException(error));
}


private static OAuth2Error createError(String errorCode, String desc) {
return createError(errorCode, desc, "https://tools.ietf.org/html/rfc6749#section-4.1.2.1");
}

private static OAuth2Error createError(String errorCode, String desc, String errorUri) {
return new OAuth2Error(errorCode, desc, errorUri);
}

private static boolean isPrincipalAuthenticated(Authentication principal) {
return principal != null &&
!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
principal.isAuthenticated();
}

private static class OAuth2AuthorizationRequestContext extends AbstractRequestContext {
private final String responseType;
private final String redirectUri;

private OAuth2AuthorizationRequestContext(
String authorizationUri, MultiValueMap<String, String> parameters) {
super(authorizationUri, parameters,
parameters.getFirst(OAuth2ParameterNames.CLIENT_ID),
parameters.getFirst(OAuth2ParameterNames.STATE),
extractScopes(parameters));
this.responseType = parameters.getFirst(OAuth2ParameterNames.RESPONSE_TYPE);
this.redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
}

private static Set<String> extractScopes(MultiValueMap<String, String> parameters) {
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
return StringUtils.hasText(scope) ?
new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))) :
Collections.emptySet();
}

private String getResponseType() {
return this.responseType;
}

private String getRedirectUri() {
return this.redirectUri;
}

private boolean isAuthenticationRequest() {
return getScopes().contains(OidcScopes.OPENID);
}

@Override
protected String resolveRedirectUri() {
return StringUtils.hasText(getRedirectUri()) ?
getRedirectUri() :
getRegisteredClient().getRedirectUris().iterator().next();
}

private OAuth2AuthorizationRequest buildAuthorizationRequest() {
return OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri(getAuthorizationUri())
.clientId(getClientId())
.redirectUri(getRedirectUri())
.scopes(getScopes())
.state(getState())
.additionalParameters(additionalParameters ->
getParameters().entrySet().stream()
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.RESPONSE_TYPE) &&
!e.getKey().equals(OAuth2ParameterNames.CLIENT_ID) &&
!e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI) &&
!e.getKey().equals(OAuth2ParameterNames.SCOPE) &&
!e.getKey().equals(OAuth2ParameterNames.STATE))
.forEach(e -> additionalParameters.put(e.getKey(), e.getValue().get(0))))
.build();
}
}

private static class UserConsentRequestContext extends AbstractRequestContext {
private OAuth2Authorization authorization;

private UserConsentRequestContext(
String authorizationUri, MultiValueMap<String, String> parameters) {
super(authorizationUri, parameters,
parameters.getFirst(OAuth2ParameterNames.CLIENT_ID),
parameters.getFirst(OAuth2ParameterNames.STATE),
extractScopes(parameters));
}

private static Set<String> extractScopes(MultiValueMap<String, String> parameters) {
List<String> scope = parameters.get(OAuth2ParameterNames.SCOPE);
return !CollectionUtils.isEmpty(scope) ? new HashSet<>(scope) : Collections.emptySet();
}

private OAuth2Authorization getAuthorization() {
return this.authorization;
}

private void setAuthorization(OAuth2Authorization authorization) {
this.authorization = authorization;
}

@Override
protected String resolveRedirectUri() {
OAuth2AuthorizationRequest authorizationRequest = getAuthorizationRequest();
return StringUtils.hasText(authorizationRequest.getRedirectUri()) ?
authorizationRequest.getRedirectUri() :
getRegisteredClient().getRedirectUris().iterator().next();
}

private OAuth2AuthorizationRequest getAuthorizationRequest() {
return getAuthorization().getAttribute(OAuth2AuthorizationRequest.class.getName());
}
}

private abstract static class AbstractRequestContext {
private final String authorizationUri;
private final MultiValueMap<String, String> parameters;
private final String clientId;
private final String state;
private final Set<String> scopes;
private RegisteredClient registeredClient;
private OAuth2Error error;
private boolean redirectOnError;

protected AbstractRequestContext(String authorizationUri, MultiValueMap<String, String> parameters,
String clientId, String state, Set<String> scopes) {
this.authorizationUri = authorizationUri;
this.parameters = parameters;
this.clientId = clientId;
this.state = state;
this.scopes = scopes;
}

protected String getAuthorizationUri() {
return this.authorizationUri;
}

protected MultiValueMap<String, String> getParameters() {
return this.parameters;
}

protected String getClientId() {
return this.clientId;
}

protected String getState() {
return this.state;
}

protected Set<String> getScopes() {
return this.scopes;
}

protected RegisteredClient getRegisteredClient() {
return this.registeredClient;
}

protected void setRegisteredClient(RegisteredClient registeredClient) {
this.registeredClient = registeredClient;
}

protected OAuth2Error getError() {
return this.error;
}

protected void setError(OAuth2Error error) {
this.error = error;
}

protected boolean hasError() {
return getError() != null;
}

protected boolean isRedirectOnError() {
return this.redirectOnError;
}

protected void setRedirectOnError(boolean redirectOnError) {
this.redirectOnError = redirectOnError;
}

protected abstract String resolveRedirectUri();
}

// private static class UserConsentPage {
// private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
// private static final String CONSENT_ACTION_PARAMETER_NAME = "consent_action";
// private static final String CONSENT_ACTION_APPROVE = "approve";
// private static final String CONSENT_ACTION_CANCEL = "cancel";
//
// private static void displayConsent(HttpServletRequest request, HttpServletResponse response,
// RegisteredClient registeredClient, OAuth2Authorization authorization) throws IOException {
//
// String consentPage = generateConsentPage(request, registeredClient, authorization);
// response.setContentType(TEXT_HTML_UTF8.toString());
// response.setContentLength(consentPage.getBytes(StandardCharsets.UTF_8).length);
// response.getWriter().write(consentPage);
// }
//
// private static boolean isConsentApproved(HttpServletRequest request) {
// return CONSENT_ACTION_APPROVE.equalsIgnoreCase(request.getParameter(CONSENT_ACTION_PARAMETER_NAME));
// }
//
// private static boolean isConsentCancelled(HttpServletRequest request) {
// return CONSENT_ACTION_CANCEL.equalsIgnoreCase(request.getParameter(CONSENT_ACTION_PARAMETER_NAME));
// }
//
// private static String generateConsentPage(HttpServletRequest request,
// RegisteredClient registeredClient, OAuth2Authorization authorization) {
//
// OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
// OAuth2AuthorizationRequest.class.getName());
// Set<String> scopes = new HashSet<>(authorizationRequest.getScopes());
// scopes.remove(OidcScopes.OPENID); // openid scope does not require consent
// String state = authorization.getAttribute(
// OAuth2ParameterNames.STATE);
//
// StringBuilder builder = new StringBuilder();
//
// builder.append("<!DOCTYPE html>");
// builder.append("<html lang=\"en\">");
// builder.append("<head>");
// builder.append(" <meta charset=\"utf-8\">");
// builder.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">");
// builder.append(" <link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css\" integrity=\"sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z\" crossorigin=\"anonymous\">");
// builder.append(" <title>Consent required</title>");
// builder.append("</head>");
// builder.append("<body>");
// builder.append("<div class=\"container\">");
// builder.append(" <div class=\"py-5\">");
// builder.append(" <h1 class=\"text-center\">Consent required</h1>");
// builder.append(" </div>");
// builder.append(" <div class=\"row\">");
// builder.append(" <div class=\"col text-center\">");
// builder.append(" <p><span class=\"font-weight-bold text-primary\">" + registeredClient.getClientId() + "</span> wants to access your account <span class=\"font-weight-bold\">" + authorization.getPrincipalName() + "</span></p>");
// builder.append(" </div>");
// builder.append(" </div>");
// builder.append(" <div class=\"row pb-3\">");
// builder.append(" <div class=\"col text-center\">");
// builder.append(" <p>The following permissions are requested by the above app.<br/>Please review these and consent if you approve.</p>");
// builder.append(" </div>");
// builder.append(" </div>");
// builder.append(" <div class=\"row\">");
// builder.append(" <div class=\"col text-center\">");
// builder.append(" <form method=\"post\" action=\"" + request.getRequestURI() + "\">");
// builder.append(" <input type=\"hidden\" name=\"client_id\" value=\"" + registeredClient.getClientId() + "\">");
// builder.append(" <input type=\"hidden\" name=\"state\" value=\"" + state + "\">");
//
// for (String scope : scopes) {
// builder.append(" <div class=\"form-group form-check py-1\">");
// builder.append(" <input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" value=\"" + scope + "\" id=\"" + scope + "\" checked>");
// builder.append(" <label class=\"form-check-label\" for=\"" + scope + "\">" + scope + "</label>");
// builder.append(" </div>");
// }
//
// builder.append(" <div class=\"form-group pt-3\">");
// builder.append(" <button class=\"btn btn-primary btn-lg\" type=\"submit\" name=\"consent_action\" value=\"approve\">Submit Consent</button>");
// builder.append(" </div>");
// builder.append(" <div class=\"form-group\">");
// builder.append(" <button class=\"btn btn-link regular\" type=\"submit\" name=\"consent_action\" value=\"cancel\">Cancel</button>");
// builder.append(" </div>");
// builder.append(" </form>");
// builder.append(" </div>");
// builder.append(" </div>");
// builder.append(" <div class=\"row pt-4\">");
// builder.append(" <div class=\"col text-center\">");
// builder.append(" <p><small>Your consent to provide access is required.<br/>If you do not approve, click Cancel, in which case no information will be shared with the app.</small></p>");
// builder.append(" </div>");
// builder.append(" </div>");
// builder.append("</div>");
// builder.append("</body>");
// builder.append("</html>");
//
// return builder.toString();
// }
// }

private static class OAuth2EndpointUtils {

private OAuth2EndpointUtils() {
}

/**
* 从请求钟获取参数 这里只是做了一个转换
* @param request /
* @return /
*/
static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
for (String value : values) {
parameters.add(key, value);
}
}
});
return parameters;
}

static boolean matchesPkceTokenRequest(HttpServletRequest request) {
return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(
request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) &&
request.getParameter(OAuth2ParameterNames.CODE) != null &&
request.getParameter(PkceParameterNames.CODE_VERIFIER) != null;
}
}

}

3.4 自定义用户同意授权界面

改写上面我注释的代码就行了。然后这里推荐一个使用thymleaf模板来自定义html页面的方法。

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
package com.ss.project.xia17user.config.security.auth2;

import com.ss.project.xia17user.dao.entity.Client;
import com.ss.project.xia17user.service.ClientService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.stereotype.Component;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;

/**
* 用户同意界面
* @author xia17
* @date 2021/3/26 11:01
*/
@Component
@RequiredArgsConstructor
public class UserConsentPage {

private final ThymeleafViewResolver thymeleafViewResolver;
private final ClientService clientService;

private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
public static final String CONSENT_ACTION_PARAMETER_NAME = "consent_action";
private static final String CONSENT_ACTION_APPROVE = "approve";
private static final String CONSENT_ACTION_CANCEL = "cancel";

/**
* 显示
* @param request /
* @param response /
* @param registeredClient /
* @param authorization /
* @throws IOException /
*/
public void display(HttpServletRequest request, HttpServletResponse response,
RegisteredClient registeredClient, OAuth2Authorization authorization) throws IOException {
Client client = clientService.findByClientId(registeredClient.getClientId());
// 生成html页面
String consentPage = this.generateConsentPage(request, client, authorization);
// 返回给浏览器
response.setContentType(TEXT_HTML_UTF8.toString());
response.setContentLength(consentPage.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(consentPage);
}

/**
* 是否同意授权
* @param request 请求信息
* @return /
*/
public boolean isConsentApproved(HttpServletRequest request) {
return CONSENT_ACTION_APPROVE.equalsIgnoreCase(request.getParameter(CONSENT_ACTION_PARAMETER_NAME));
}
/**
* 是否拒绝授权
* @param request 请求信息
* @return /
*/
public boolean isConsentCancelled(HttpServletRequest request) {
return CONSENT_ACTION_CANCEL.equalsIgnoreCase(request.getParameter(CONSENT_ACTION_PARAMETER_NAME));
}

/**
* 生成页面
* @param request /
* @param client /
* @param authorization /
* @return /
*/
private String generateConsentPage(HttpServletRequest request, Client client, OAuth2Authorization authorization) {
// 参数准备
Context context = new Context();
// 客户端信息
context.setVariable("clientName",client.getClientName());
context.setVariable("clientId",client.getClientId());
// 请求地址
context.setVariable("actionUrl",request.getRequestURI());
// 当前登录用户名
context.setVariable("username",authorization.getPrincipalName());
// state变量
context.setVariable(OAuth2ParameterNames.STATE,authorization.getAttribute(OAuth2ParameterNames.STATE));
// 授权范围
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
OAuth2AuthorizationRequest.class.getName());
assert authorizationRequest != null;
Set<String> scopes = new HashSet<>(authorizationRequest.getScopes());
context.setVariable("scopes",scopes);
// 使用thymeleaf模版渲染成html 字符串
return thymeleafViewResolver.getTemplateEngine().process("userConsent", context);
}


}

然后使用参考 3.3

时间线

2021-03-26 开始写

2021-03-26 写完