我正在尝试在 Spring 身份验证服务器 (Spring Security < 5.0) 中配置多个身份验证提供程序(主要和次要)。我知道将在主要提供商或第二个提供商上找到用户,永远不会同时出现。因此,如果主要提供商的身份验证失败,我想给出正确的消息。
根据“认证”方法documentation:
<块引用>返回:一个完全经过身份验证的对象,包括凭据。可能 如果 AuthenticationProvider 无法支持,则返回 null 传递的 Authentication 对象的身份验证。在这种情况下, 下一个 AuthenticationProvider 支持所呈现的 将尝试身份验证类。
<块引用>抛出:AuthenticationException - 如果身份验证失败。
基于此,我在主提供程序上实现了身份验证方法,如下所示(我将省略 SecondaryAuthProvider 实现):
//PrimaryAuthProvider.class
public Authentication authenticate(Authentication authentication) {
var user = authServices.getLdapUser(authentication.getName());
//log and let the next provider handle it
if (user == null) {
logServices.userNotFound(new LogServices.AuthFailure(authentication.getName()));
return null;
}
if (passwordMatches(authentication.getCredentials(), user.getStringPassword())) {
return authenticatedToken(user);
} else {
logServices.authFailure(new LogServices.AuthFailure(authentication.getName()));
throw new BadCredentialsException("Invalid password");
}
}
在 WebSecurity 内部,我还注入了我的提供者:
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(primaryAuthProvider);
auth.authenticationProvider(secondaryAuthProvider);
}
如果出现以下情况,这将正确处理:
如果在主提供者上找到用户并且他的密码错误,将抛出 BadCredentialsException 但服务器仍将委托给次要提供者,最终消息将是“未找到用户”,这是误导性的。
>我认为 BadCredentialsException 会完成身份验证链并向客户端/用户报告,但似乎并非如此。
我错过了什么吗?
答案 0 :(得分:0)
好的,刚刚想通了。
Provider Manager 是我的服务器上使用的默认身份验证管理器。如果发生 AuthenticationException,它的身份验证方法确实委托给下一个提供者:
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
//(...)
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
我找到了两种方法。
第一个:如果身份验证失败,请在主要提供者上提供一个未经身份验证的令牌,并且不抛出任何异常:
//PrimaryAuthProvider.class
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
var user = authServices.getLdapUser(authentication.getName());
if (user == null) return null;
if (passwordMatches(authentication.getCredentials(), user.getStringPassword())) {
return authenticatedToken(user);
} else {
return unauthenticatedToken(user);
}
}
private UsernamePasswordAuthenticationToken unauthenticatedToken(LdapUser user) {
//Using 2 parameter constructor => authenticated = false
return new UsernamePasswordAuthenticationToken(
user.getLogin(),
user.getStringPassword(),
);
}
这有一个缺点,即以英语显示默认消息。我需要在其他地方拦截异常并用葡萄牙语抛出一个新异常。
第二个(希望我用过):实现我自己的 AuthorizationManager 作为 ProviderManager 的一个较小版本。这个不会尝试捕获提供者发起的异常:
public class CustomProviderManager implements AuthenticationManager {
private final List<AuthenticationProvider> providers;
public CustomProviderManager(AuthenticationProvider... providers) {
this.providers = List.of(providers);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
for (var provider : providers) {
if (!provider.supports(authentication.getClass())) continue;
//let exceptions go through
var result = provider.authenticate(authentication);
if (result != null) {
return result;
}
}
throw new ProviderNotFoundException(
"No provider for " + authentication.getName()
);
}
}
然后,在 WebSecurityConfig:
@Bean(BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return authenticationManager();
}
@Override
protected AuthenticationManager authenticationManager() {
return new CustomProviderManager(primaryAuthProvider, secondaryAuthProvider);
}
// Don't need it anymore
// @Override
// protected void configure(AuthenticationManagerBuilder auth) {
// auth.authenticationProvider(authenticationProvider);
// auth.authenticationProvider(secondaryAuthProvider);
// }
第二个需要更多的编码,但给了我更多的控制权。