按照设计OAuth2AuthorizationCodeRequestAuthenticationToken支持的AuthenticationProvider可以有多个,目前只有一个。
成员属性及其作用
这个AuthenticationProvider的属性有:


根据token的consent布尔值决定是否进入授权确认流程,分为两个分支:
只有GET请求会进入这个方法。下面的条件有一个不满足就会进入同意授权流程:
客户端设置ClientSettings没有设置需要用户确认。
授权请求的scope只有openid一个值,openid(OIDC标识)这个值本身不参与授权确认。
如果用户确认信息OAuth2AuthorizationConsent存在而且其所有的scope已经被用户同意授权。

只有POST请求会进入这个方法。
这个逻辑是用户在授权确认页面进行了提交操作后,再次被OAuth2AuthorizationEndpointFilter拦截并交给OAuth2AuthorizationCodeRequestAuthenticationProvider认证。也就是说提交URI必须是授权URI而且是POST提交。这里是一个参考提交的报文:
POST /oauth2/authorize HTTP/1.1
Host: localhost:9000
Content-Type: application/x-www-form-urlencoded
client_id=felord&state=eX926CEl4pvrri0ETQ6b9MsYs8VHzdBD-X9RZmBO4vw%3D&scope=message.write&scope=message.read
这里的state值是在authenticateAuthorizationRequest方法中生成的


代码在customconsent 分支
在OAuth2AuthorizationEndpointFilter的流程分析中我们针对用户授权(consent)确认302重定向默认情况下由一个自动生成的页面承担。而大多数情况下我们希望能够自定义用户确认页面。以下是自定义用户确认授权URI的代码:
String redirectUri = UriComponentsBuilder.fromUriString(resolveConsentUri(request))
.queryParam(OAuth2ParameterNames.SCOPE, String.join(" ", requestedScopes))
.queryParam(OAuth2ParameterNames.CLIENT_ID, clientId)
.queryParam(OAuth2ParameterNames.STATE, state)
.toUriString();
this.redirectStrategy.sendRedirect(request, response, redirectUri);
从上面我们可以知道,如果自定义的consentPage是一个绝对URL直接拼上上面的参数,如果是一个相对路径,就根据当前请求生成一个URL。而且携带有以下几个参数:
从前面DEMO的授权确认页可以看出这里需要以下几种信息:
根据前面对OAuth2AuthorizationEndpointFilter的分析,请求提交必须通过/oauth2/authorize(默认)进行POST表单提交。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
然后写一个consent.html,放在Spring Boot 模板目录resources/templates下面。主要有客户端信息、用户名称、选择权限、登录按钮。
控制器
/**
* 自定义用户确认页
*
* @author felord.cn
*/
@Controller
public class AuthorizationConsentController {
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationConsentService authorizationConsentService;
private final ScopeService scopeService;
public AuthorizationConsentController(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationConsentService authorizationConsentService,
ScopeService scopeService) {
this.registeredClientRepository = registeredClientRepository;
this.authorizationConsentService = authorizationConsentService;
this.scopeService = scopeService;
}
/**
* {@link OAuth2AuthorizationEndpointFilter} 会302重定向到{@code /oauth2/consent}并携带入参
*
* @param principal 当前用户
* @param model 视图模型
* @param clientId oauth2 client id
* @param scope 请求授权的scope
* @param state state 值
* @return 自定义授权确认页面 consent.html
*/
@GetMapping(value = "/oauth2/consent")
public String consent(Principal principal, Model model,
@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
@RequestParam(OAuth2ParameterNames.SCOPE) String scope,
@RequestParam(OAuth2ParameterNames.STATE) String state) {
//1、 通过client_id获取注册OAuth2客户端信息。
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
String id = registeredClient.getId();
// 2、查询当前用户在该客户端下有没有授权确认信息。
OAuth2AuthorizationConsent currentAuthorizationConsent =
this.authorizationConsentService.findById(id, principal.getName());
//3、从授权确认信息中提取已经授权的scope集合,保证结果不能为null。
Set<String> authorizedScopes = currentAuthorizationConsent != null ?
currentAuthorizationConsent.getScopes() : Collections.emptySet();
//4、scopesToApprove用来存放用户没有授权的scope,肯定不为空。
Set<String> scopesToApprove = new HashSet<>();
//5、previouslyApprovedScopes用来存放上次授权的scope,可能为空,授权确认流程是可以重入的。
Set<String> previouslyApprovedScopes = new HashSet<>();
for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
if (authorizedScopes.contains(requestedScope)) {
previouslyApprovedScopes.add(requestedScope);
} else {
scopesToApprove.add(requestedScope);
}
}
Set<OAuth2Scope> scopesToApproves = scopeService.findByNames(scopesToApprove);
Set<OAuth2Scope> previouslyApprovedScopesSet = scopeService.findByNames(previouslyApprovedScopes);
String clientName = registeredClient.getClientName();
model.addAttribute("clientId", clientId);// 6、 clientId原样放到页面上用于授权确认提交。
model.addAttribute("clientName", clientName);// 7、clientName用来渲染页面,用来描述应用的,一般clientId都是乱字符,你也可以放个应用logo啥的。
model.addAttribute("state", state);// state值原路返回用于提交。
model.addAttribute("scopes", scopesToApproves);//scopesToApproves是对④的加强,带权限描述。
model.addAttribute("previouslyApprovedScopes", previouslyApprovedScopesSet);//previouslyApprovedScopesSet是对⑤的加强,带权限描述
model.addAttribute("principalName", principal.getName());//principalName当前授权用户的名称也要展示到页面上。
// 返回自定义的consent视图。
return "consent";
}
}
上面的自定义页面要配置到OAuth2AuthorizationEndpointConfigurer,非常简单:
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
// 把自定义的授权确认URI加入配置
authorizationServerConfigurer.authorizationEndpoint(
authorizationEndpointConfigurer->
authorizationEndpointConfigurer.consentPage("/oauth2/consent")
);
首先需要用户(Resource Owner)登录返回token,然后才能用户授权确认。这里可行的流程为:
consentPage必须是是可以访问的全路径页面路由,例如https://auth.test.cn/consent.html。
用户登录授权服务器,授权服务器返回一个登录认证凭证(例如token)给前端保存。
前端携带token去GET请求授权/oauth2/authorize,注意按OAuth2要求带参数,除此之外,token的鉴权还要在这里搞定,需要你去扩展OAuth2AuthorizationCodeRequestAuthenticationConverter加入解析token的操作,否则会一直重定向的登录页面。
接下来OAuth2AuthorizationEndpointFilter会重定向到你自己配置的consentPage并携带文章开头的三个参数,302重定向如何取取到这三个参数自己想办法?
根据这三个参数写一个接口跟上面差不多只不过返回的是JSON数据。
然后的流程跟前后不分离是一样的,切记提交的URI一定是授权请求URI,而且是POST提交。
OAuth2客户端认证由另一个配置类OAuth2ClientAuthenticationConfigurer完成,
OAuth2ClientAuthenticationConfigurer是OAuth2ClientAuthenticationFilter的配置类。

根据RFC6749#section-2.1的描述,OAuth2基于客户端保护自己密码的能力定义了两种客户端类型:
无论OAuth2客户端属于哪种类型,它都需要向授权服务器表明身份。OAuth2是一样的道理,要对客户端的凭据进行一个验证,以确保当前客户端的确是资源拥有者授权代理的客户端。OAuth2授权服务器约定了几种验证策略,都封装在ClientAuthenticationMethod中,后面会详细讲这个东西。
这个配置类就是配置客户端认证过滤器OAuth2ClientAuthenticationFilter的。它的配置项跟前面介绍的OAuth2AuthorizationEndpointConfigurer差不多,仅仅缺少了consentPage。所以配置上大致都一样。这里需要强调的是请求匹配规则策略RequestMatcher,目前和token相关的三个OAuth2端点都是需要被认证的,规则很简单:

主要拦截 以下post请求
具体的配置逻辑非常简单,只需要配置好的OAuth2ClientAuthenticationFilter被添加到AbstractPreAuthenticatedProcessingFilter之后。
认证逻辑

认证转换器接口负责将请求信息转换为待认证对象Authentication。OAuth2ClientAuthenticationFilter中默认为DelegatingAuthenticationConverter,它包含四种客户端认证策略:

JwtClientAssertionAuthenticationConverter jtw客户端断言认证
对应private_key_jwt和client_secret_jwt两种客户端认证模式。
ClientSecretBasicAuthenticationConverter 客户密码Basic Auth 证模式
对应client_secret_basic客户端认证模式。
ClientSecretPostAuthenticationConverter 公开客户端认证
对应none客户端认证模式,一般公开客户端才使用这种模式。
PublicClientAuthenticationConverter 客户密码post证模式
对应client_secret_post客户端认证模式
这四种策略的目的都是从请求中提取OAuth2ClientAuthenticationToken,它们都提取了OAuth2ClientAuthenticationToken所需要的四个参数:
请求中包含client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer的由该转换器处理:
client_secret_basic方式中,它主要的逻辑就是从请求头Authorization中提取Basic Auth的值,并BASE64解码解析出client_id:client_secret值对:
公共客户端是没有客户端密钥client_secret的,因此它解析出client_id就完事了,不过它会校验一下是否携带PKCE的关键值code_verifier。
和client_secret_basic不同的是,client_secret_post会把client_id和client_secret放在请求体中,所以从请求体中提取这两个值就行了。