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

news2024/10/3 0:22:14

由于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类似,只是添加了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 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
	@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 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 {

    @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模式也支持不受影响:
在这里插入图片描述
演示用户名或密码错误:
在这里插入图片描述
在这里插入图片描述

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

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

相关文章

HarmonOS 通用组件(Button)

本文 我们来看看基础组件中的 Button 这是 ArkTS ui 原生支持的一个组件 用来创建不同样式的按钮 首先 我们还是创建一个最基本的组件结构 Entry Component struct Index {build() {Row() {Column() {}.width(100%)}.height(100%)} }我们可以在 Column 组件中 加入一个button…

Jmeter相关知识介绍

Jmeter 是Apache 组织开发的基于JAVA 的压力测试工具,用于对软件做压力测试,特别适合于WEB 应用测试(包括压力,接口测试) 今天简单介绍Jemeter的入门相关概念的理解 一、在安装目录下有一个Bin\Jmeter.bat 双击打开 打开之后是一个这样的界面 二、测试计划 1、添加和删…

面试十分钟不到就被赶出来了,问的问题有点变态

从小厂出来&#xff0c;没想到在另一家公司又寄了。 到这家公司开始上班&#xff0c;加班是每天必不可少的&#xff0c;看在钱给的比较多的份上&#xff0c;就不太计较了。没想到12月一纸通知&#xff0c;所有人不准加班&#xff0c;加班费不仅没有了&#xff0c;薪资还要降40%…

【Xilinx FPGA】异步 FIFO 的复位

FIFO&#xff08;First-In-First_Out&#xff0c;先入先出&#xff09;是一种的存储器类型&#xff0c;在 FPGA 开发中通常用于数据缓存、位宽转换或者跨时钟域&#xff08;多 bit 数据流&#xff09;。在使用异步 FIFO 时&#xff0c;应注意复位信号是否遵循相关要求和规范&am…

Mysql show Profiles详解

1.简介 show profile 和 show profiles 命令用于展示SQL语句的资源使用情况&#xff0c;包括CPU的使用&#xff0c;CPU上下文切换&#xff0c;IO等待&#xff0c;内存使用等&#xff0c;这个命令对于分析某个SQL的性能瓶颈非常有帮助&#xff0c;借助于show profile的输出信息&…

啊哈c语言——逻辑挑战9:水仙花数

有一种三位数特别奇怪&#xff0c;这种数的“个位数的立方”加上“十位数的 立方”再加上“百位数的立方”恰好等于这个数。例如&#xff1a; 153111555333&#xff0c;我们为这种特殊的三位数起了一个很好听的名字——“水仙花数”&#xff0c;那么请你找出所有的“水仙花数”…

简易机器学习笔记(四)初识卷积神经网络

前言 第一次写卷积神经网络&#xff0c;也是照着paddlepaddle的官方文档抄&#xff0c;这里简单讲解一下心得。 首先我们要知道之前写的那些东西都是什么&#xff0c;之前写的我们称之为简单神经网络&#xff0c;也就是简单一层连接输出和输出&#xff0c;通过前向计算和逆向…

百度地图打点性能优化(海量点、mapv)

文章目录 百度地图打点性能优化&#xff08;海量点、mapv&#xff09;原因优化方法数据获取方面页面加载方面 参考资料 百度地图打点性能优化&#xff08;海量点、mapv&#xff09; 原因 在百度地图api中&#xff0c;默认的点是下图的红点 而这种点位比较多的时候&#xff0c…

【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 …

可变参数应用(C++并发编程中的joining_thread代码)

代码&#xff1a; #include "X:\Work\Share\CCode\CPlatform\Base\global_c_all.h" using namespace lf; using namespace std;class joining_thread {std::thread t; public:joining_thread() noexcept default;template<typename Callable, typename ... Args…

Python基础教程——90行Python代码让微信地球转起来!!(完整版,附源码)

今天我们来一个好玩儿的东西&#xff0c;使用90行Python代码就可以让【微信第地球】转起来&#xff0c;太有意思啦&#xff01; 1.微信地球 手机重启后打开微信的一瞬间&#xff0c;会看到一幅有名的图片&#xff0c;上面站着一个张小龙。 你可曾想过这样一个问题&#xff0c…

【QT】QStandardItemModel类的应用介绍

目录 1 概述 2 常用方法 3 QStandardItemModel的使用 3.1 界面设计与主窗口类定义 3.2 系统初始化 3.3 从文本文件导入数据 3.4 数据修改 3.5 单元格格式设置 3.6 数据另存为文件 1 概述 QStandardItemModel是标准的以项数据&#xff08;itemdata&#xff09;为基础的…

BMS电池管理系统带充放电控制过流过压保护

2.4G无线采集BMS开发板&#xff08;主从一体&#xff09; 全新升级 &#xff08;赠送上位机源码TTL 上位机&#xff0c;可以改成自己想要的界面&#xff09; 12串电池TTL上位机 CAN通信上位机源码有偿开源&#xff0c;供项目二次开发。 增加STM32平台 USB转TTL通信 CAN通信 增加…

Mac启动时候出现禁止符号

Mac启动时候出现禁止符号 启动时候出现禁止符号,意味着 选定的启动磁盘 包含 Mac 操作系统&#xff0c;但它不是 您的 Mac 可以使用的 macOS 。您应该在这个磁盘上 重新安装 macOS 。 可以尝试以下苹果提供的方法&#xff1a; Mac启动时候出现禁止符号 不要轻易抹除磁盘&am…

云原生学习系列之基础环境准备(单节点安装kubernetes)

一、环境要求 操作系统CentOS 7.x-86_x64 硬件配置&#xff1a;内存2GB或2G&#xff0c;CPU 2核或CPU 2核&#xff0c;需要在虚拟机中提前设置好&#xff0c;不然后续会报错 二、系统初始化 1、设置主机名 # 在master节点执行 hostnamectl set-hostname master01 2、配置主…

CRM如何精确控制RT设备和与UMD通信笔记

1 CRM工作职责 监控link req是否得到schedule监控link上各个device的req是否ready监控SOF或EOF的trigger信号&#xff0c;决定各个设备配置哪个req 2 CRM如何精确控制RT设备 分两步&#xff0c;一是crm知道某帧req的link dev准备好了&#xff0c;可以做apply setting。 二是…

解决vue3中watch 监听不到旧值的问题,亲测有效!

问题描述 这个问题是我在公司vue3项目的时候发现的一个问题&#xff0c;watch 在监听对象/数组变量的变化时&#xff0c;发现对象的数据变化时 旧数据 获取到的和新数据是一样的 类似于下面这样 const objref({a:我是原来的值,b:6, })obj.a改变值watch(obj,(nel,old)>{ c…

RTT打印时间戳

官方的RTT VIEWER没有打印接收时间戳的功能&#xff0c;经过查找后发现可以有以下三种打印时间戳的方法。 第三方的RTT上位机ExtraPutty自己打印 第三方的RTT上位机 码云上有一个RTT_T2的仓库&#xff0c;基于python qt包写的画面&#xff0c;通过pylink来jlink通信。 优点…

docker - 常用容器部署命令大全(MySQL、Redis、RabbitMQ、ES、Kibana、Nacos、Sentinel)

目录 一、常用容器运行指令 MySQL Redis RabbitMQ ElasticSearch & kibana Nacos Sentinel 一、常用容器运行指令 MySQL docker run -d --name mysql -p 3306:3306 -e TZAsia/Shanghai -e MYSQL_ROOT_PASSWORD1111 mysql:5.7 -e TZAsia/Shanghai&#xff1a;指定…

商智C店H5性能优化实战

前言 商智C店&#xff0c;是依托移动低码能力搭建的一个应用&#xff0c;产品面向B端商家。随着应用体量持续增大&#xff0c;考虑产品定位及用户体验&#xff0c;我们针对性能较差页面做了一次优化&#xff0c;并取得了不错的效果&#xff0c;用户体验值&#xff08;UEI&…