赞
踩
由于Spring-Security-Oauth2停止维护,官方推荐采用 spring-security-oauth2-authorization-server,而后者默认不支持密码授权模式,本篇实战中采用的版本如下:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.3.1</version>
</dependency>
尝试使用密码模式结果如下:
但是可能业务场景中需要使用到密码授权模式,所以参照spring oauth2-server源码自己实现。先上一张总图:需要编写的类:
编写它们的依据来源于spring源码中对authorization_code
以及client_credentials
的实现。
先简单介绍下上述4个类:
1.工具类:大部分代码来源于spring源码片断,复制而来
2.AuthenticationConverter
实现类:官方描述如下:A strategy used for converting from a HttpServletRequest to an Authentication of particular type. Used to authenticate with appropriate AuthenticationManager.(一种策略:把HttpServletRequest转换为特定类型的Authentication)
3.AuthenticationProvider
实现类:官方描述:Indicates a class can process a specific Authentication implementation.(可处理特定Authentication的实现)
4.Authentication
实现类:官方描述如下:Represents the token for an authentication request or for an authenticated principal once the request has been processed by the AuthenticationManager.authenticate(Authentication) method
编写好后,最后在我们的配置类中改造代码,本篇后面部分说明,先说上述4个类实现。
一.参照spring支持授权码以及client_credentials实现源码:
可以从上图中确认spring本身确实没有对密码模式的支持。我们先看spring对授权码和client_credentials
两种授权模式的实现:
它们代码都不多,而且都继承自OAuth2AuthorizationGrantAuthenticationToken
类。所以咱们要支持密码模式的Authentication实现类,同样继承OAuth2AuthorizationGrantAuthenticationToken
实现,代码如下:
package com.example.security; import org.springframework.lang.Nullable; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * @author: jelex.xu * @Date: 2024/1/5 20:39 * @desc: 由于Spring-Security-Oauth2停止维护,官方推荐采用 spring-security-oauth2-authorization-server * <dependency> * <groupId>org.springframework.security</groupId> * <artifactId>spring-security-oauth2-authorization-server</artifactId> * <version>0.3.1</version> * </dependency> * 因为 spring-security-oauth2-authorization-server不支持 password模式的oauth2认证,所以需要自己手工编写代码添加支持。 * 可参照 {@see OAuth2ClientCredentialsAuthenticationToken} and OAuth2AuthorizationCodeAuthenticationToken写, * 它们共同继承同一个父类,咱们也这样做: **/ public class OAuth2PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { private final Set<String> scopes; /** * Sub-class constructor. * * @param clientPrincipal the authenticated client principal * @param additionalParameters the additional parameters 比client_credentials 多出来的username+password参数在这里 */ public OAuth2PasswordAuthenticationToken(Authentication clientPrincipal, @Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) { super(AuthorizationGrantType.PASSWORD, clientPrincipal, additionalParameters); this.scopes = Collections.unmodifiableSet( scopes != null ? new HashSet<>(scopes) : Collections.emptySet()); } /** * Returns the requested scope(s). * * @return the requested scope(s), or an empty {@code Set} if not available */ public Set<String> getScopes() { return this.scopes; } }
思路很清晰,完成。
二.编写AuthenticationProvider
类:
如果你注意了上面类截图的话,可以注意到:
所以同样,参照OAuth2AuthorizationCodeAuthenticationProvider
和OAuth2ClientCredentialsAuthenticationProvider
两个类来编写咱们的密码模式的provider类:它们都直接implements AuthenticationProvider
.
下面是要实现的方法:
@Slf4j public class OAuth2PasswordAuthenticationProvider implements AuthenticationProvider { // 这部分代码和OAuth2ClientCredentialsAuthenticationProvider类似,只是添加了AuthenticationManager private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN); // 密码模式需要 AuthenticationManager private final AuthenticationManager authenticationManager; private final OAuth2AuthorizationService authorizationService; private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator; // 构造方法和OAuth2AuthorizationCodeAuthenticationProvider类似,只是多了authenticationManager的初始化 /** * Constructs an {@code OAuth2PasswordAuthenticationProvider} using the provided parameters. * * @param authorizationService the authorization service * @param tokenGenerator the token generator * @since 0.2.3 */ public OAuth2PasswordAuthenticationProvider(AuthenticationManager authenticationManager, OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) { Assert.notNull(authorizationService, "authorizationService cannot be null"); Assert.notNull(tokenGenerator, "tokenGenerator cannot be null"); this.authenticationManager = authenticationManager; this.authorizationService = authorizationService; this.tokenGenerator = tokenGenerator; } // 此方法实现和OAuth2AuthorizationCodeAuthenticationProvider类似,照猫画虎而已。 @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { OAuth2PasswordAuthenticationToken passwordAuthentication = (OAuth2PasswordAuthenticationToken) authentication; OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationUtils.getAuthenticatedClientElseThrowInvalidClient(passwordAuthentication); RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); } Authentication usernamePasswordAuthentication = OAuth2AuthenticationUtils .getUsernamePasswordAuthentication(authenticationManager, passwordAuthentication); Set<String> authorizedScopes = registeredClient.getScopes(); // Default to configured scopes Set<String> scopes = passwordAuthentication.getScopes(); if (!CollectionUtils.isEmpty(scopes)) { // 因为数据量不大,双重for循环先不优化(源码中也是这样做的) for (String requestedScope : scopes) { if (!registeredClient.getScopes().contains(requestedScope)) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE); } } authorizedScopes = new LinkedHashSet<>(scopes); } // @formatter:off DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() .registeredClient(registeredClient) .principal(usernamePasswordAuthentication) .providerContext(ProviderContextHolder.getProviderContext()) .authorizedScopes(authorizedScopes) .tokenType(OAuth2TokenType.ACCESS_TOKEN) .authorizationGrantType(AuthorizationGrantType.PASSWORD) .authorizationGrant(passwordAuthentication); // @formatter:on // ----- Access token ----- OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build(); OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext); if (generatedAccessToken == null) { OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the access token.", ERROR_URI); throw new OAuth2AuthenticationException(error); } if (log.isInfoEnabled()) { log.info("OAuth2PasswordAuthenticationProvider::start to generate token."); } OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); // @formatter:off OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) .principalName(usernamePasswordAuthentication.getName()) .authorizationGrantType(AuthorizationGrantType.PASSWORD) .attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes) .attribute(Principal.class.getName(), usernamePasswordAuthentication); // @formatter:on if (generatedAccessToken instanceof ClaimAccessor) { authorizationBuilder.token(accessToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims())); } else { authorizationBuilder.accessToken(accessToken); } // ----- Refresh token ----- OAuth2RefreshToken refreshToken = null; if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) && // Do not issue refresh token to public client !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) { tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build(); OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext); if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) { OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the refresh token.", ERROR_URI); throw new OAuth2AuthenticationException(error); } refreshToken = (OAuth2RefreshToken) generatedRefreshToken; if (log.isInfoEnabled()) { log.info("OAuth2PasswordAuthenticationProvider:: set refresh token."); } authorizationBuilder.refreshToken(refreshToken); } // ----- ID token ----- OidcIdToken idToken; if (scopes.contains(OidcScopes.OPENID)) { // @formatter:off tokenContext = tokenContextBuilder .tokenType(ID_TOKEN_TOKEN_TYPE) .authorization(authorizationBuilder.build()) // ID token customizer may need access to the access token and/or refresh token .build(); // @formatter:on OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext); if (!(generatedIdToken instanceof Jwt)) { OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the ID token.", ERROR_URI); throw new OAuth2AuthenticationException(error); } if (log.isInfoEnabled()) { log.info("OAuth2PasswordAuthenticationProvider:: generate id token."); } idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(), generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims()); authorizationBuilder.token(idToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims())); } else { idToken = null; } OAuth2Authorization authorization = authorizationBuilder.build(); this.authorizationService.save(authorization); if (log.isInfoEnabled()) { log.info("OAuth2PasswordAuthenticationProvider:: saved authorization."); } Map<String, Object> additionalParameters = Collections.emptyMap(); if (idToken != null) { additionalParameters = new HashMap<>(); additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue()); } return new OAuth2AccessTokenAuthenticationToken( registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters); } // 简单的方法:不解释 @Override public boolean supports(Class<?> authentication) { return 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模式也支持不受影响:
演示用户名或密码错误:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。