由于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类似,只是添加了AuthenticationManagerprivate 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);// 密码模式需要 AuthenticationManagerprivate 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类似,照猫画虎而已。@Overridepublic 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 scopesSet<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:offDefaultOAuth2TokenContext.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:offOAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient).principalName(usernamePasswordAuthentication.getName()).authorizationGrantType(AuthorizationGrantType.PASSWORD).attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes).attribute(Principal.class.getName(), usernamePasswordAuthentication);// @formatter:onif (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:offtokenContext = 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:onOAuth2Token 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);}// 简单的方法:不解释@Overridepublic boolean supports(Class<?> authentication) {return OAuth2PasswordAuthenticationToken.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@Overridepublic 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) &¶meters.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 OAuth2ClientCredentialsAuthenticationToken(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 {@Overridepublic 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) &¶meters.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())))));//注入新的AuthenticationManagerhttp.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);// 额外补充添加一个认证providerhttp.authenticationProvider(passwordAuthenticationProvider);}
}
六.测试验证,启动服务,然后如下所示:
当然basic auth传递client_id 和 client_secret也是支持的:
已有的client_credential模式也支持不受影响:
演示用户名或密码错误: