由于Spring-Security-Oauth2停止维护,官方推荐采用 spring-security-oauth2-authorization-server,而后者默认不支持密码授权模式,本篇实战中采用的版本如下:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.3.1</version>
</dependency>
尝试使用密码模式结果如下:
但是可能业务场景中需要使用到密码授权模式,所以参照spring oauth2-server源码自己实现。先上一张总图:需要编写的类:
编写它们的依据来源于spring源码中对authorization_code
以及client_credentials
的实现。
先简单介绍下上述4个类:
1.工具类:大部分代码来源于spring源码片断,复制而来
2.AuthenticationConverter
实现类:官方描述如下:A strategy used for converting from a HttpServletRequest to an Authentication of particular type. Used to authenticate with appropriate AuthenticationManager.(一种策略:把HttpServletRequest转换为特定类型的Authentication)
3.AuthenticationProvider
实现类:官方描述:Indicates a class can process a specific Authentication implementation.(可处理特定Authentication的实现)
4.Authentication
实现类:官方描述如下:Represents the token for an authentication request or for an authenticated principal once the request has been processed by the AuthenticationManager.authenticate(Authentication) method
编写好后,最后在我们的配置类中改造代码,本篇后面部分说明,先说上述4个类实现。
一.参照spring支持授权码以及client_credentials实现源码:
可以从上图中确认spring本身确实没有对密码模式的支持。我们先看spring对授权码和client_credentials
两种授权模式的实现:
它们代码都不多,而且都继承自OAuth2AuthorizationGrantAuthenticationToken
类。所以咱们要支持密码模式的Authentication实现类,同样继承OAuth2AuthorizationGrantAuthenticationToken
实现,代码如下:
package com.example.security;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @author: jelex.xu
* @Date: 2024/1/5 20:39
* @desc: 由于Spring-Security-Oauth2停止维护,官方推荐采用 spring-security-oauth2-authorization-server
* <dependency>
* <groupId>org.springframework.security</groupId>
* <artifactId>spring-security-oauth2-authorization-server</artifactId>
* <version>0.3.1</version>
* </dependency>
* 因为 spring-security-oauth2-authorization-server不支持 password模式的oauth2认证,所以需要自己手工编写代码添加支持。
* 可参照 {@see OAuth2ClientCredentialsAuthenticationToken} and OAuth2AuthorizationCodeAuthenticationToken写,
* 它们共同继承同一个父类,咱们也这样做:
**/
public class OAuth2PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
private final Set<String> scopes;
/**
* Sub-class constructor.
*
* @param clientPrincipal the authenticated client principal
* @param additionalParameters the additional parameters 比client_credentials 多出来的username+password参数在这里
*/
public OAuth2PasswordAuthenticationToken(Authentication clientPrincipal,
@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
super(AuthorizationGrantType.PASSWORD, clientPrincipal, additionalParameters);
this.scopes = Collections.unmodifiableSet(
scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
}
/**
* Returns the requested scope(s).
*
* @return the requested scope(s), or an empty {@code Set} if not available
*/
public Set<String> getScopes() {
return this.scopes;
}
}
思路很清晰,完成。
二.编写AuthenticationProvider
类:
如果你注意了上面类截图的话,可以注意到:
所以同样,参照OAuth2AuthorizationCodeAuthenticationProvider
和OAuth2ClientCredentialsAuthenticationProvider
两个类来编写咱们的密码模式的provider类:它们都直接implements AuthenticationProvider
.
下面是要实现的方法:
@Slf4j
public class OAuth2PasswordAuthenticationProvider implements AuthenticationProvider {
// 这部分代码和OAuth2ClientCredentialsAuthenticationProvider类似,只是添加了AuthenticationManager
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
// 密码模式需要 AuthenticationManager
private final AuthenticationManager authenticationManager;
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
// 构造方法和OAuth2AuthorizationCodeAuthenticationProvider类似,只是多了authenticationManager的初始化
/**
* Constructs an {@code OAuth2PasswordAuthenticationProvider} using the provided parameters.
*
* @param authorizationService the authorization service
* @param tokenGenerator the token generator
* @since 0.2.3
*/
public OAuth2PasswordAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authenticationManager = authenticationManager;
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
// 此方法实现和OAuth2AuthorizationCodeAuthenticationProvider类似,照猫画虎而已。
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2PasswordAuthenticationToken passwordAuthentication = (OAuth2PasswordAuthenticationToken) authentication;
OAuth2ClientAuthenticationToken clientPrincipal =
OAuth2AuthenticationUtils.getAuthenticatedClientElseThrowInvalidClient(passwordAuthentication);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
Authentication usernamePasswordAuthentication = OAuth2AuthenticationUtils
.getUsernamePasswordAuthentication(authenticationManager, passwordAuthentication);
Set<String> authorizedScopes = registeredClient.getScopes(); // Default to configured scopes
Set<String> scopes = passwordAuthentication.getScopes();
if (!CollectionUtils.isEmpty(scopes)) {
// 因为数据量不大,双重for循环先不优化(源码中也是这样做的)
for (String requestedScope : scopes) {
if (!registeredClient.getScopes().contains(requestedScope)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
}
}
authorizedScopes = new LinkedHashSet<>(scopes);
}
// @formatter:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthentication)
.providerContext(ProviderContextHolder.getProviderContext())
.authorizedScopes(authorizedScopes)
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.authorizationGrant(passwordAuthentication);
// @formatter:on
// ----- Access token -----
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the access token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
if (log.isInfoEnabled()) {
log.info("OAuth2PasswordAuthenticationProvider::start to generate token.");
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
// @formatter:off
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(usernamePasswordAuthentication.getName())
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes)
.attribute(Principal.class.getName(), usernamePasswordAuthentication);
// @formatter:on
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
} else {
authorizationBuilder.accessToken(accessToken);
}
// ----- Refresh token -----
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// Do not issue refresh token to public client
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the refresh token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
if (log.isInfoEnabled()) {
log.info("OAuth2PasswordAuthenticationProvider:: set refresh token.");
}
authorizationBuilder.refreshToken(refreshToken);
}
// ----- ID token -----
OidcIdToken idToken;
if (scopes.contains(OidcScopes.OPENID)) {
// @formatter:off
tokenContext = tokenContextBuilder
.tokenType(ID_TOKEN_TOKEN_TYPE)
.authorization(authorizationBuilder.build()) // ID token customizer may need access to the access token and/or refresh token
.build();
// @formatter:on
OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedIdToken instanceof Jwt)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the ID token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
if (log.isInfoEnabled()) {
log.info("OAuth2PasswordAuthenticationProvider:: generate id token.");
}
idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
authorizationBuilder.token(idToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
} else {
idToken = null;
}
OAuth2Authorization authorization = authorizationBuilder.build();
this.authorizationService.save(authorization);
if (log.isInfoEnabled()) {
log.info("OAuth2PasswordAuthenticationProvider:: saved authorization.");
}
Map<String, Object> additionalParameters = Collections.emptyMap();
if (idToken != null) {
additionalParameters = new HashMap<>();
additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue());
}
return new OAuth2AccessTokenAuthenticationToken(
registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
}
// 第十个简单的方法:不解释
@Override
public boolean supports(Class<?> authentication) {
return OAuth2PasswordAuthenticationProvider.class.isAssignableFrom(authentication);
}
}
所以上述代码看起来比较复杂,其实也只不过是照着spring对授权码模式的源码复制改动很小的一部分而已。
三.编写Converter实现类:同理,spring默认没有对密码模式的实现,我们参照 另两种支持的模式实现复制改造:
我们参照简单的OAuth2ClientCredentialsAuthenticationConverter类
实现,先看看完整源码:
/*
* 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 org.springframework.security.oauth2.server.authorization.web.authentication;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Attempts to extract an Access Token Request from {@link HttpServletRequest} for the OAuth 2.0 Client Credentials Grant
* and then converts it to an {@link OAuth2ClientCredentialsAuthenticationToken} used for authenticating the authorization grant.
*
* @author Joe Grandja
* @since 0.1.2
* @see AuthenticationConverter
* @see OAuth2ClientCredentialsAuthenticationToken
* @see OAuth2TokenEndpointFilter
*/
public final class OAuth2ClientCredentialsAuthenticationConverter implements AuthenticationConverter {
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// scope (OPTIONAL)
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) &&
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.SCOPE,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.SCOPE)) {
additionalParameters.put(key, value.get(0));
}
});
return new OAuth2PasswordAuthenticationToken(
clientPrincipal, requestedScopes, additionalParameters);
}
}
我们支持密码模式的类实现 照着上述spring源码复制一份,稍等改动如下:
加了 用户名 和 密码 两个参数的校验
package com.example.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
/**
* @author: jelex.xu
* @Date: 2024/1/6 17:10
* @desc: 参考 {@link OAuth2ClientCredentialsAuthenticationConverter} 编写
**/
public class OAuth2PasswordAuthenticationConverter implements AuthenticationConverter {
@Override
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) {
return null;
}
MultiValueMap<String, String> parameters = OAuth2AuthenticationUtils.getParameters(request);
// scope (OPTIONAL)
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) &&
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
OAuth2AuthenticationUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.SCOPE,
OAuth2AuthenticationUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
// username (REQUIRED)
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
OAuth2AuthenticationUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.USERNAME,
OAuth2AuthenticationUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// password (REQUIRED)
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
OAuth2AuthenticationUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.PASSWORD,
OAuth2AuthenticationUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
if (clientPrincipal == null) {
OAuth2AuthenticationUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ErrorCodes.INVALID_CLIENT,
OAuth2AuthenticationUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.SCOPE)) {
additionalParameters.put(key, value.get(0));
}
});
return new OAuth2PasswordAuthenticationToken(
clientPrincipal, requestedScopes, additionalParameters);
}
}
四.最后是工具类实现,因为可见性问题,我们自己编写的上述三个类无法访问到工具类方法,所以简单粗暴,直接把用到的工具代码复制出来。当然也涉及在整合配置的时候需要的工具方法,一并放这里,完整代码如下:
FYI: 不用担心,它们真的只是spring源码使用到的工具类的复制而已,当然在整合配置的时候有部分改动,但主体结构完整是spring源码的复制,所以别慌!
package com.example.security;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* Utility methods for the OAuth 2.0 {@link AuthenticationProvider}'s.
* 从 OAuth2AuthenticationProviderUtils 复制部分而来,因为它不是public级别,自定义密码模式无法访问
* @author Joe Grandja & jelex.xu
* @since 0.0.3
*/
public final class OAuth2AuthenticationUtils {
private OAuth2AuthenticationUtils() {
}
public static final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
public static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
OAuth2ClientAuthenticationToken clientPrincipal = null;
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
}
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
return clientPrincipal;
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
public static Authentication getUsernamePasswordAuthentication(AuthenticationManager authenticationManager,
OAuth2PasswordAuthenticationToken passwordAuthenticationToken) {
Map<String, Object> additionalParameters = passwordAuthenticationToken.getAdditionalParameters();
String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);
String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(usernamePasswordAuthenticationToken);
}
public 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;
}
public static void throwError(String errorCode, String parameterName, String errorUri) {
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
throw new OAuth2AuthenticationException(error);
}
public static <T> T getOptionalBean(HttpSecurity http, Class<T> type) {
Map<String, T> beansMap = BeanFactoryUtils.beansOfTypeIncludingAncestors(http.getSharedObject(ApplicationContext.class), type);
if (beansMap.size() > 1) {
throw new NoUniqueBeanDefinitionException(type, beansMap.size(),
"Expected single matching bean of type '" + type.getName() + "' but found " +
beansMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(beansMap.keySet()));
}
return (!beansMap.isEmpty() ? beansMap.values().iterator().next() : null);
}
public static JwtGenerator getJwtGenerator(HttpSecurity http) {
JwtGenerator jwtGenerator = http.getSharedObject(JwtGenerator.class);
if (jwtGenerator == null) {
JwtEncoder jwtEncoder = getJwtEncoder(http);
if (jwtEncoder != null) {
jwtGenerator = new JwtGenerator(jwtEncoder);
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = getJwtCustomizer(http);
if (jwtCustomizer != null) {
jwtGenerator.setJwtCustomizer(jwtCustomizer);
}
http.setSharedObject(JwtGenerator.class, jwtGenerator);
}
}
return jwtGenerator;
}
private static JwtEncoder getJwtEncoder(HttpSecurity http) {
JwtEncoder jwtEncoder = http.getSharedObject(JwtEncoder.class);
if (jwtEncoder == null) {
jwtEncoder = getOptionalBean(http, JwtEncoder.class);
if (jwtEncoder == null) {
JWKSource<SecurityContext> jwkSource = getJwkSource(http);
if (jwkSource != null) {
jwtEncoder = new NimbusJwtEncoder(jwkSource);
}
}
if (jwtEncoder != null) {
http.setSharedObject(JwtEncoder.class, jwtEncoder);
}
}
return jwtEncoder;
}
static <B extends HttpSecurityBuilder<B>> JWKSource<SecurityContext> getJwkSource(HttpSecurity http) {
JWKSource<SecurityContext> jwkSource = http.getSharedObject(JWKSource.class);
if (jwkSource == null) {
ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class);
jwkSource = getOptionalBean(http, type);
if (jwkSource != null) {
http.setSharedObject(JWKSource.class, jwkSource);
}
}
return jwkSource;
}
static <T> T getOptionalBean(HttpSecurity http, ResolvableType type) {
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
String[] names = context.getBeanNamesForType(type);
if (names.length > 1) {
throw new NoUniqueBeanDefinitionException(type, names);
}
return names.length == 1 ? (T) context.getBean(names[0]) : null;
}
private static OAuth2TokenCustomizer<JwtEncodingContext> getJwtCustomizer(HttpSecurity http) {
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, JwtEncodingContext.class);
return getOptionalBean(http, type);
}
public static OAuth2TokenCustomizer<OAuth2TokenClaimsContext> getAccessTokenCustomizer(HttpSecurity http) {
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, OAuth2TokenClaimsContext.class);
return getOptionalBean(http, type);
}
}
五.终于到了激动人心的时刻:编写好支撑密码模式的类后,开始整合进配置:
@Configuration
public class OAuth2AuthorizeSecurityConfig {
/**
* 为了支持密码模式,改造下:
* @param http
* @return
* @throws Exception
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// 从这里开始到下面结束标识,其实是上一行代码
// OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);的实现,
// 只是为了拿到OAuth2AuthorizationServerConfigurer对象,不得不这样做而已.
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
// 结束标识??结束标识??结束标识??结束标识??--------
// 加入的额外配置逻辑 支持密码模式:
http.apply(
authorizationServerConfigurer.tokenEndpoint(
oAuth2TokenEndpointConfigurer -> oAuth2TokenEndpointConfigurer.accessTokenRequestConverter(
new DelegatingAuthenticationConverter(Arrays.asList(
new OAuth2ClientCredentialsAuthenticationConverter(),
// 加入密码模式转换器
new OAuth2PasswordAuthenticationConverter(),
new OAuth2AuthorizationCodeAuthenticationConverter(),
new OAuth2RefreshTokenAuthenticationConverter())
)
)
)
);
//注入新的AuthenticationManager
http.authenticationManager(authenticationManager(http));
/**
* Custom configuration for Password grant type, which current implementation has no support for.
*/
addOAuth2PasswordAuthenticationProvider(http);
return http.formLogin(Customizer.withDefaults()).build();
}
// 中间省略其它很多配置。。。
/**
*构造一个AuthenticationManager,使用自定义的userDetailsService和passwordEncoder
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder())
.and()
.build();
return authenticationManager;
}
// 中间省略其它很多配置。。。
// 下面大段代码逻辑也是从spring官方源码复制改动而来:
// 比如 OAuth2TokenEndpointConfigurer#createDefaultAuthenticationProviders
// 方法中处理逻辑
private void addOAuth2PasswordAuthenticationProvider(HttpSecurity http) throws Exception {
// AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
AuthenticationManager authenticationManager = authenticationManager(http);
OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class);
if (authorizationService == null) {
authorizationService = OAuth2AuthenticationUtils.getOptionalBean(http, OAuth2AuthorizationService.class);
if (authorizationService == null) {
authorizationService = new InMemoryOAuth2AuthorizationService();
}
http.setSharedObject(OAuth2AuthorizationService.class, authorizationService);
}
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator = http.getSharedObject(OAuth2TokenGenerator.class);
if (tokenGenerator == null) {
tokenGenerator = OAuth2AuthenticationUtils.getOptionalBean(http, OAuth2TokenGenerator.class);
if (tokenGenerator == null) {
JwtGenerator jwtGenerator = OAuth2AuthenticationUtils.getJwtGenerator(http);
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer = OAuth2AuthenticationUtils.getAccessTokenCustomizer(http);
if (accessTokenCustomizer != null) {
accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer);
}
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
if (jwtGenerator != null) {
tokenGenerator = new DelegatingOAuth2TokenGenerator(
jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
} else {
tokenGenerator = new DelegatingOAuth2TokenGenerator(
accessTokenGenerator, refreshTokenGenerator);
}
}
http.setSharedObject(OAuth2TokenGenerator.class, tokenGenerator);
}
OAuth2PasswordAuthenticationProvider passwordAuthenticationProvider =
new OAuth2PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator);
// 额外补充添加一个认证provider
http.authenticationProvider(passwordAuthenticationProvider);
}
}
六.测试验证,启动服务,然后如下所示:
当然basic auth传递client_id 和 client_secret也是支持的:
已有的client_credential模式也支持不受影响: