篇三:让OAuth2 server支持密码模式

由于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类:
如果你注意了上面类截图的话,可以注意到:
在这里插入图片描述
所以同样,参照OAuth2AuthorizationCodeAuthenticationProviderOAuth2ClientCredentialsAuthenticationProvider两个类来编写咱们的密码模式的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) &&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 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) &&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())))));//注入新的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模式也支持不受影响:
在这里插入图片描述
演示用户名或密码错误:
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/602331.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

1-02VS的安装与测试

一、概述 对于一名C语言程序员而言&#xff0c;进行C语言程序的开发一般需要一个文本编辑器加上一个编译器就足够了。但为了方便起见&#xff0c;我们选择使用集成开发环境——Visual Studio&#xff08;简称VS&#xff09;。安装Visual Studio 下面讲一下如何安装VS&#xff0…

【AI视野·今日Sound 声学论文速览 第三十八期】Mon, 1 Jan 2024

AI视野今日CS.Sound 声学论文速览 Mon, 1 Jan 2024 Totally 5 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Sound Papers The Arrow of Time in Music -- Revisiting the Temporal Structure of Music with Distinguishability and Unique Orientability as the …

金和OA C6 MailTemplates.aspx SQL注入漏洞复现

0x01 产品简介 金和OA协同办公管理系统软件(简称金和OA),本着简单、适用、高效的原则,贴合企事业单位的实际需求,实行通用化、标准化、智能化、人性化的产品设计,充分体现企事业单位规范管理、提高办公效率的核心思想,为用户提供一整套标准的办公自动化解决方案,以帮助…

【DevOps-07-3】Jenkins集成Sonarqube

一、简要说明 Jenkins安装Sonarqube插件Jenkins安装和配置Sonar-Scanner信息Jenkins打包项目中,增加Sonar-Scanner代码质量扫描二、Jenkins安装Sonarqube插件 1、登录Jenkins管理后台,搜索安装Sonar-Scanner插件 Jenkins管理后台示例:http://192.168.95.131:8080/jenkins/

Oracle数据库新手零基础入门,Oracle安装配置和操作使用详解

一、教程描述 本套教程是专门为初学者量身定制的&#xff0c;无需任何Oracle数据库基础&#xff0c;课程采用循序渐进的教学方式&#xff0c;从Oracle数据库的基础知识开始讲起&#xff0c;并不会直接涉及到一项具体的技术&#xff0c;而是随着课程的不断深入&#xff0c;一些…

docker部署mysql主从复制篇

环境准备&#xff1a;docker服务安装&#xff0c;mysql镜像 配置文件方式&#xff1a;可以挂载目录&#xff0c;也可以写好配置文件&#xff0c;利用docker cp 到容器内&#xff0c;这里直接在启动镜像创建容器时候挂载目录方式服务器上配置文件内容(下图标红路径)&#xff1a…

WEB 3D技术 three.js 顶点缩放

本文 我们来说 顶点缩放 我们官网搜索 BufferGeometry 下面有一个 scale 函数 例如 我们先将代码写成这样 上面图片和资源文件 大家需要自己去加一下 import ./style.css import * as THREE from "three"; import { OrbitControls } from "three/examples/j…

MySQL 临时表

MySQL 临时表 MySQL 临时表在我们需要保存一些临时数据时是非常有用的。 临时表只在当前连接可见&#xff0c;当关闭连接时&#xff0c;MySQL 会自动删除表并释放所有空间。 在 MySQL 中&#xff0c;临时表是一种在当前会话中存在的表&#xff0c;它在会话结束时会自动被销毁…

【教学类-09-04】20240102《游戏棋N*N》数字填写,制作棋子和骰子

作品展示 背景需求&#xff1a; 最近在清理学具材料库&#xff0c;找到一套1年多前的《N*N游戏棋》&#xff0c;把没有用完的棋盘拿出来&#xff0c;&#xff0c;想给大4班换花样&#xff0c;并把它们用掉。 程序代码在这里 【教学类-09-03】20221120《游戏棋10*10数字如何直接…

一篇五分生信临床模型预测文章代码复现——Figure 10.机制及肿瘤免疫浸润(九)——Estimate——倒数第二节

之前讲过临床模型预测的专栏,但那只是基础版本,下面我们以自噬相关基因为例子,模仿一篇五分文章,将图和代码复现出来,学会本专栏课程,可以具备发一篇五分左右文章的水平: 本专栏目录如下: Figure 1:差异表达基因及预后基因筛选(图片仅供参考) Figure 2. 生存分析,…

马目标检测数据集VOC格式500张

马&#xff0c;一种优雅而强健的哺乳动物&#xff0c;以其独特的形态和特点而闻名于世。 马的体型高大&#xff0c;四肢修长&#xff0c;有着强健的肌肉和结实的骨骼。马头一般较长&#xff0c;眼睛炯炯有神&#xff0c;耳朵直立&#xff0c;鼻子和嘴巴都较大。毛发根据品种不…

黑马程序员SSM框架-Maven进阶

视频链接&#xff1a;Maven-01-分模块开发的意义_哔哩哔哩_bilibili 分模块开发与设计 分模块开发意义 分模块开发 依赖管理 依赖传递 依赖传递冲突问题 可以点击红框按钮查看依赖情况。 可选依赖和排除依赖 继承和聚合 聚合 聚合工程开发 继承 聚合和继承的区别 属性 属性…

大模型在现代应用中的多元实例

目录 前言1 GPT-3、GPT-3.5、GPT-4&#xff1a;自然语言处理的新纪元1.1 GPT-3与传统NLP方法的区别1.2 GPT-3.5 和 GPT-4 的进展1.3 技术背后的革新 2 自然语言转换为Python代码2.1 简介2.2 技术原理2.3 应用和优势 3 DALL-E 2&#xff08;5B&#xff09;图像生成3.1 简介3.2 技…

【Bootstrap学习 day11】

Bootstrap5字体图标 字体图标是在Web项目中使用的图标字体。 使用字体图标的好处是&#xff0c;可以通过应用CSS color属性来创建任何颜色的图标。此外&#xff0c;要更改图标的大小&#xff0c;只需使用CSS font-size属性即可。 获取字体图标 在网页中包含Bootstrap5图标的最…

深入了解Apache 日志,Apache 日志分析工具

Apache Web 服务器在企业中广泛用于托管其网站和 Web 应用程序&#xff0c;Apache 服务器生成的原始日志提供有关 Apache 服务器托管的网站如何处理用户请求以及访问您的网站时经常遇到的错误的重要信息。 什么是 Apache 日志 Apache 日志包含 Apache Web 服务器处理的所有事…

B+树索引及其原理

MySQL索引的底层结构是B树&#xff0c;为什么它会选择这个结构&#xff1f;联合索引是怎么实现的&#xff1f;最左侧匹配原则的原理是什么&#xff1f;本文将一一解答这些疑惑。 1 前置知识 在学习B树之前&#xff0c;我们先了解下其他的树形结构&#xff1a;二叉树、平衡二叉…

locust 快速入门--异常(Exceptions)与失败(Failures)

背景&#xff1a; 使用locust进行压测的时候&#xff0c;服务器响应已经异常了&#xff0c;但是从UI页面上看到的还是正常的响应。直至服务完全挂掉&#xff0c;才会出现异常信息。 locust认为HTTP响应代码是OK&#xff08;<400&#xff09;是成功的。实际服务的响应代码是2…

数据结构之堆——学习笔记

1.堆的简介&#xff1a; 接下来看一下堆的建立&#xff1b; 接下来是如何在堆中插入数据以及删除数据&#xff1a; 大根堆的插入操作类似只是改变了一下大于和小于符号&#xff0c;同时插入操作的时间复杂度为O&#xff08;logn&#xff09;。 来看几个问题&#xff1a; 答案当…

每日一题——LeetCode1051.高度检查器

方法一 sort排序&#xff1a; 创建一个元素和heights一模一样的expect数组 &#xff0c;将expect数组从小到大进行排序&#xff0c;比较heights和expect相同位置不同的元素个数 var heightChecker function(heights) {var expect [],count0for(const n of heights){expect.…

Ubuntu同步两个剪切板

众所周知&#xff0c;ubuntu系统中有两套剪切板。第一个剪切板是用鼠标操作&#xff0c;鼠标选中则复制&#xff0c;点击鼠标中键则粘贴&#xff08;这个剪切板通常叫做——选择缓冲区&#xff09;。第二个剪切板则是真正的剪切板&#xff0c;使用ctrlc&#xff08;在终端中默认…