目标是在
/oauth2/token
端点下进行身份验证和令牌分配。例如,当应用程序收到正确的凭据时:
POST http://localhost:9000/oauth2/token
Authorization: Basic bWVzc2FnaW5nLWNsaWVudDpzZWNyZXQ=
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials &
scope=access &
username=billy &
password=billypassword
应用程序分配一个令牌:
{"access_token":"1ZlxLr5-bDPUdK0x21e92GJQD6A","token_type":"bearer","expires_in":43199,"scope":"access"}
当凭据不正确时,应用程序不应分配令牌并响应:
{"error":"invalid_grant","error_description":"Bad credentials"}
额外要求:
经过一番分析,我认为,这有可能通过以下任一方式实现:
OAuth2ClientCredentialsAuthenticationProvider#setAuthenticationValidator
设置我自己的验证器。验证器可能类似于 OAuth2ClientCredentialsAuthenticationValidator
,但在 validateScope
方法的开头会有额外的代码负责身份验证。org.springframework.security.web.authentication.OAuth2ClientCredentialsAuthenticationValidator
public final class OAuth2ClientCredentialsAuthenticationValidator
implements Consumer<OAuth2ClientCredentialsAuthenticationContext> {
private static final Log LOGGER = LogFactory.getLog(OAuth2ClientCredentialsAuthenticationValidator.class);
/**
* The default validator for
* {@link OAuth2ClientCredentialsAuthenticationToken#getScopes()}.
*/
public static final Consumer<OAuth2ClientCredentialsAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OAuth2ClientCredentialsAuthenticationValidator::validateScope;
private final Consumer<OAuth2ClientCredentialsAuthenticationContext> authenticationValidator = DEFAULT_SCOPE_VALIDATOR;
@Override
public void accept(OAuth2ClientCredentialsAuthenticationContext authenticationContext) {
this.authenticationValidator.accept(authenticationContext);
}
private static void validateScope(OAuth2ClientCredentialsAuthenticationContext authenticationContext) {
OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthentication = authenticationContext
.getAuthentication();
RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
Set<String> requestedScopes = clientCredentialsAuthentication.getScopes();
Set<String> allowedScopes = registeredClient.getScopes();
if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(LogMessage.format(
"Invalid request: requested scope is not allowed" + " for registered client '%s'",
registeredClient.getId()));
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
}
}
}
UsernamePasswordAuthenticationFilter
中的过滤器处理 url 更改为 /oauth2/token
并将此过滤器放入与 /oauth2/token
相关的链中。org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
"POST");
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* Enables subclasses to override the composition of the password, such as by
* including additional values and a separator.
* <p>
* This might be used for example if a postcode/zipcode was required in addition to
* the password. A delimiter such as a pipe (|) should be used to separate the
* password and extended value(s). The <code>AuthenticationDao</code> will need to
* generate the expected password in a corresponding manner.
* </p>
* @param request so that request attributes can be retrieved
* @return the password that will be presented in the <code>Authentication</code>
* request token to the <code>AuthenticationManager</code>
*/
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
/**
* Enables subclasses to override the composition of the username, such as by
* including additional values and a separator.
* @param request so that request attributes can be retrieved
* @return the username that will be presented in the <code>Authentication</code>
* request token to the <code>AuthenticationManager</code>
*/
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
/**
* Provided so that subclasses may configure what is put into the authentication
* request's details property.
* @param request that an authentication request is being created for
* @param authRequest the authentication request object that should have its details
* set
*/
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
/**
* Sets the parameter name which will be used to obtain the username from the login
* request.
* @param usernameParameter the parameter name. Defaults to "username".
*/
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
/**
* Sets the parameter name which will be used to obtain the password from the login
* request..
* @param passwordParameter the parameter name. Defaults to "password".
*/
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
/**
* Defines whether only HTTP POST requests will be allowed by this filter. If set to
* true, and an authentication request is received which is not a POST request, an
* exception will be raised immediately and authentication will not be attempted. The
* <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed
* authentication.
* <p>
* Defaults to <tt>true</tt> but may be overridden by subclasses.
*/
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return this.usernameParameter;
}
public final String getPasswordParameter() {
return this.passwordParameter;
}
}
目前,我有以下配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http.csrf(AbstractHttpConfigurer::disable)
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc(Customizer.withDefaults())
)
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
.oauth2ResourceServer((oauth2ResourceServer) ->
oauth2ResourceServer
.jwt((jwt) ->
jwt
.decoder(jwtDecoder(jwkSource()))
)
)
.authenticationManager(authenticationManager(http));
SecurityFilterChain o = http.build();
return o;
}
@Bean
@Order(3)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
AuthenticationManager o = authenticationManagerBuilder.build();
return o;
}
@Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider =
new ActiveDirectoryLdapAuthenticationProvider("", "LDAP://localhost" + ":" + "389", "DC=example,dc=org");
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
provider.setSearchFilter("(uid=billy)");
return provider;
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient messagingClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("messaging-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:9000/login/oauth2/code/messaging-client")
.postLogoutRedirectUri("http://127.0.0.1:9000/")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("message:read")
.scope("message:write")
.scope("access")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
.build();
return new InMemoryRegisteredClientRepository(messagingClient);
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(myClientRegistration());
}
private ClientRegistration myClientRegistration() {
return ClientRegistration.withRegistrationId(UUID.randomUUID().toString())
.clientId("messaging-client")
.clientSecret("{noop}secret")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationUri("http://127.0.0.1:9000/login")
.tokenUri("http://127.0.0.1:9000/oauth2/token")
.userInfoUri("https://127.0.0.1:9000/oauth2/v3/userinfo")
.scope("message:read")
.scope("access")
.build();
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
问题是在没有检查密码的情况下分配令牌。
您能告诉我我提出的解决方案是否可以完成工作,提供问题的解决方案吗?
编辑
这个问题与 Spring Authorization Server with AuthorizationGrantType.PASSWORD 不同,因为使用已弃用的
AuthorizationGrantType.PASSWORD
不是一个选项(前面提到过)。
OAuth 2.0 规范允许任何合适的 HTTP 身份验证方案 对令牌端点的请求进行身份验证。 但是,您提议的功能在功能上等同于资源所有者密码凭据授予,并且具有导致资源所有者密码凭据授予不受支持的相同缺点。
如果考虑到此警告您仍想继续,请定义此自定义身份验证提供程序类:
public class CustomClientCredentialsAuthenticationProvider implements AuthenticationProvider {
private final AuthenticationManager authenticationManager;
private final OAuth2ClientCredentialsAuthenticationProvider delegate;
public CustomClientCredentialsAuthenticationProvider(
AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator
) {
this.authenticationManager = authenticationManager;
this.delegate =
new OAuth2ClientCredentialsAuthenticationProvider(authorizationService, tokenGenerator);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
var clientAuthentication = (OAuth2ClientCredentialsAuthenticationToken) authentication;
String username = (String) clientAuthentication.getAdditionalParameters().get("username");
String password = (String) clientAuthentication.getAdditionalParameters().get("password");
var usernamePassword = new UsernamePasswordAuthenticationToken(username, password);
try {
authenticationManager.authenticate(usernamePassword);
} catch (AuthenticationException e) {
// Throw this exception type to prevent ProviderManager trying subsequent providers.
throw new InternalAuthenticationServiceException(e.getMessage(), e);
}
return delegate.authenticate(clientAuthentication);
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2ClientCredentialsAuthenticationToken.class.isAssignableFrom(authentication);
}
}
定义自定义身份验证提供程序:
@Bean
CustomClientCredentialsAuthenticationProvider customClientCredentialsAuthenticationProvider(
AuthenticationConfiguration authenticationConfiguration,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<?> tokenGenerator
) throws Exception {
return new CustomClientCredentialsAuthenticationProvider(
authenticationConfiguration.getAuthenticationManager(),
authorizationService,
tokenGenerator);
}
在授权服务器安全过滤器链中,将身份验证提供程序添加到令牌端点:
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.tokenEndpoint(endpoint -> endpoint
.authenticationProvider(customClientCredentialsAuthenticationProvider)