我曾经也有过你所描述的那种让人抓狂的经历,最终我编写了一个自定义认证提供程序,对Active Directory服务器进行LDAP查询。
因此,我的与安全相关的bean如下:
<beans:bean id="contextSource"
class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
<beans:constructor-arg value="ldap://hostname.queso.com:389/" />
</beans:bean>
<beans:bean id="ldapAuthenticationProvider"
class="org.queso.ad.service.authentication.LdapAuthenticationProvider">
<beans:property name="authenticator" ref="ldapAuthenticator" />
<custom-authentication-provider />
</beans:bean>
<beans:bean id="ldapAuthenticator"
class="org.queso.ad.service.authentication.LdapAuthenticatorImpl">
<beans:property name="contextFactory" ref="contextSource" />
<beans:property name="principalPrefix" value="QUESO\" />
</beans:bean>
然后是 LdapAuthenticationProvider 类:
public class LdapAuthenticationProvider implements AuthenticationProvider {
private LdapAuthenticator authenticator;
public Authentication authenticate(Authentication auth) throws AuthenticationException {
DirContextOperations authAdapter = authenticator.authenticate(auth);
LdapAuthenticationToken ldapAuth = new LdapAuthenticationToken(auth, "ROLE_USER");
InitialLdapContext ldapContext = (InitialLdapContext) authAdapter
.getObjectAttribute("ldapContext");
if (ldapContext != null) {
ldapAuth.setContext(ldapContext);
}
return ldapAuth;
}
public boolean supports(Class clazz) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(clazz));
}
public LdapAuthenticator getAuthenticator() {
return authenticator;
}
public void setAuthenticator(LdapAuthenticator authenticator) {
this.authenticator = authenticator;
}
}
然后是 LdapAuthenticatorImpl 类:
public class LdapAuthenticatorImpl implements LdapAuthenticator {
private DefaultSpringSecurityContextSource contextFactory;
private String principalPrefix = "";
public DirContextOperations authenticate(Authentication authentication) {
String principal = principalPrefix + authentication.getName();
String password = "";
if (authentication.getCredentials() != null) {
password = authentication.getCredentials().toString();
}
if (!("".equals(principal.trim())) && !("".equals(password.trim()))) {
InitialLdapContext ldapContext = (InitialLdapContext) contextFactory
.getReadWriteContext(principal, password);
DirContextOperations authAdapter = new DirContextAdapter();
authAdapter.addAttributeValue("ldapContext", ldapContext);
return authAdapter;
} else {
throw new BadCredentialsException("Blank username and/or password!");
}
}
static public InitialLdapContext recreateLdapContext(LdapAuthenticator authenticator,
LdapAuthenticationToken auth) {
DirContextOperations authAdapter = authenticator.authenticate(auth);
InitialLdapContext context = (InitialLdapContext) authAdapter
.getObjectAttribute("ldapContext");
auth.setContext(context);
return context;
}
public DefaultSpringSecurityContextSource getContextFactory() {
return contextFactory;
}
public void setContextFactory(DefaultSpringSecurityContextSource contextFactory) {
this.contextFactory = contextFactory;
}
public String getPrincipalPrefix() {
return principalPrefix;
}
public void setPrincipalPrefix(String principalPrefix) {
if (principalPrefix != null) {
this.principalPrefix = principalPrefix;
} else {
this.principalPrefix = "";
}
}
}
最后,是LdapAuthenticationToken类:
public class LdapAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = -5040340622950665401L;
private Authentication auth;
transient private InitialLdapContext context;
private List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
public LdapAuthenticationToken(Authentication auth, GrantedAuthority defaultAuthority) {
this.auth = auth;
if (auth.getAuthorities() != null) {
this.authorities.addAll(Arrays.asList(auth.getAuthorities()));
}
if (defaultAuthority != null) {
this.authorities.add(defaultAuthority);
}
super.setAuthenticated(true);
}
public LdapAuthenticationToken(Authentication auth, String defaultAuthority) {
this(auth, new GrantedAuthorityImpl(defaultAuthority));
}
public GrantedAuthority[] getAuthorities() {
GrantedAuthority[] authoritiesArray = this.authorities.toArray(new GrantedAuthority[0]);
return authoritiesArray;
}
public void addAuthority(GrantedAuthority authority) {
this.authorities.add(authority);
}
public Object getCredentials() {
return auth.getCredentials();
}
public Object getPrincipal() {
return auth.getPrincipal();
}
public InitialLdapContext getContext() {
return context;
}
public void setContext(InitialLdapContext context) {
this.context = context;
}
}
您会注意到其中有一些可能不需要的部分。
例如,我的应用程序需要保留成功登录的LDAP上下文以供用户在登录后进一步使用 -- 应用程序的目的是让用户通过他们的AD凭据登录,然后执行更多的与AD相关的功能。因此,我有一个自定义的身份验证令牌LdapAuthenticationToken,在其中传递(而不是Spring的默认身份验证令牌),这使我能够附加LDAP上下文。在LdapAuthenticationProvider.authenticate()中,我创建该令牌并将其传回;在LdapAuthenticatorImpl.authenticate()中,我将已登录的上下文附加到返回对象中,以便可以将其添加到用户的Spring身份验证对象中。
同样,在LdapAuthenticationProvider.authenticate()中,我分配所有已登录用户ROLE_USER角色 -- 这就是让我在我的intercept-url元素中测试该角色的原因。您将希望将其与您要测试的任何角色匹配,甚至基于Active Directory组或其他内容分配角色。
最后,与此相关的是,我实现的LdapAuthenticationProvider.authenticate()方式为所有具有有效AD帐户的用户分配了相同的ROLE_USER角色。显然,在该方法中,您可以对用户执行进一步的测试(即,用户是否在特定的AD组中?)并以这种方式分配角色,甚至在授予用户访问权限之前测试某些条件。