Java Spring Security LDAP 和记住我
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/24745528/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Spring Security LDAP and Remember Me
提问by Maksim
I'm building an app with Spring Boot that has integration with LDAP. I was able to connect successfully to LDAP server and authenticate user. Now I have a requirement to add remember-me functionality. I tried to look through different posts (this) but was not able to find an answer to my problem. Official Spring Security documentstates that
我正在使用 Spring Boot 构建一个与 LDAP 集成的应用程序。我能够成功连接到 LDAP 服务器并验证用户身份。现在我需要添加记住我的功能。我试图查看不同的帖子(this),但无法找到我的问题的答案。官方 Spring Security文件指出
If you are using an authentication provider which doesn't use a UserDetailsService (for example, the LDAP provider) then it won't work unless you also have a UserDetailsService bean in your application context
如果您使用的身份验证提供程序不使用 UserDetailsService(例如,LDAP 提供程序),则除非您的应用程序上下文中还有 UserDetailsService bean,否则它将无法工作
Here the my working code with some initial thoughts to add remember-me functionality:
这是我的工作代码以及一些添加记住我功能的初步想法:
WebSecurityConfig
网络安全配置
import com.ui.security.CustomUserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.event.LoggerListener;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
String DOMAIN = "ldap-server.com";
String URL = "ldap://ds.ldap-server.com:389";
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/ui/**").authenticated()
.antMatchers("/", "/home", "/UIDL/**", "/ui/**").permitAll()
.anyRequest().authenticated()
;
http
.formLogin()
.loginPage("/login").failureUrl("/login?error=true").permitAll()
.and().logout().permitAll()
;
// Not sure how to implement this
http.rememberMe().rememberMeServices(rememberMeServices()).key("password");
}
@Override
protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception {
authManagerBuilder
.authenticationProvider(activeDirectoryLdapAuthenticationProvider())
.userDetailsService(userDetailsService())
;
}
@Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(DOMAIN, URL);
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
provider.setUserDetailsContextMapper(userDetailsContextMapper());
return provider;
}
@Bean
public UserDetailsContextMapper userDetailsContextMapper() {
UserDetailsContextMapper contextMapper = new CustomUserDetailsServiceImpl();
return contextMapper;
}
/**
* Impl of remember me service
* @return
*/
@Bean
public RememberMeServices rememberMeServices() {
// TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("password", userService);
// rememberMeServices.setCookieName("cookieName");
// rememberMeServices.setParameter("rememberMe");
return rememberMeServices;
}
@Bean
public LoggerListener loggerListener() {
return new LoggerListener();
}
}
CustomUserDetailsServiceImpl
自定义用户详细信息ServiceImpl
public class CustomUserDetailsServiceImpl implements UserDetailsContextMapper {
@Autowired
SecurityHelper securityHelper;
Log ___log = LogFactory.getLog(this.getClass());
@Override
public LoggedInUserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> grantedAuthorities) {
LoggedInUserDetails userDetails = null;
try {
userDetails = securityHelper.authenticateUser(ctx, username, grantedAuthorities);
} catch (NamingException e) {
e.printStackTrace();
}
return userDetails;
}
@Override
public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
}
}
I know that I need to implement UserService somehow, but not sure how that can be achieved.
我知道我需要以某种方式实现 UserService,但不确定如何实现。
采纳答案by Vladimír Sch?fer
There are two issues to configuration of the RememberMe features with LDAP:
使用 LDAP 配置 RememberMe 功能有两个问题:
- selection of the correct RememberMe implementation (Tokens vs. PersistentTokens)
- its configuration using Spring's Java Configuration
- 选择正确的 RememberMe 实现(Tokens vs. PersistentTokens)
- 它的配置使用 Spring 的 Java 配置
I'll take these step by step.
我会一步一步来。
The Token-based remember me feature (TokenBasedRememberMeServices
) works in the following way during authentication:
基于令牌的记住我功能 ( TokenBasedRememberMeServices
) 在身份验证期间按以下方式工作:
- user gets authenticated (agaisnt AD) and we currently know user's ID and password
- we construct value username + expirationTime + password + staticKey and create an MD5 hash of it
- we create a cookie which contains username + expiration + the calculated hash
- 用户通过身份验证(agaisnt AD),我们目前知道用户的 ID 和密码
- 我们构造值 username + expireTime + password + staticKey 并创建它的 MD5 哈希
- 我们创建一个包含用户名 + 过期时间 + 计算出的哈希值的 cookie
When user wants to come back to the service and be authenticated using the remember me functionality we:
当用户想要返回服务并使用“记住我”功能进行身份验证时,我们:
- check whether the cookie exists and isn't expired
- populate the user ID from the cookie and call the provided UserDetailsService which is expected to return information related to the user's ID, including the password
- we then calculate the hash from the returned data and verify that the hash in the cookie matches with the value we calculated
- if it matches we return the user's Authentication object
- 检查cookie是否存在且未过期
- 从 cookie 中填充用户 ID 并调用提供的 UserDetailsService,该服务应返回与用户 ID 相关的信息,包括密码
- 然后我们根据返回的数据计算哈希值,并验证 cookie 中的哈希值是否与我们计算的值匹配
- 如果匹配,我们返回用户的 Authentication 对象
The hash checking process is required in order to make sure that nobody can create a "fake" remember me cookie, which would let them impersonate another user. The problem is that this process relies on possibility of loading password from our repository - but this is impossible with Active Directory - we cannot load plaintext password based on username.
哈希检查过程是必需的,以确保没有人可以创建“假的”记住我的 cookie,这会让他们冒充另一个用户。问题是这个过程依赖于从我们的存储库加载密码的可能性——但是这对于 Active Directory 是不可能的——我们无法根据用户名加载纯文本密码。
This makes the Token-based implementation unsuitable for usage with AD (unless we start creating some local user store which contains the password or some other secret user-based credential and I'm not suggesting this approach as I don't know other details of your application, although it might be a good way to go).
这使得基于令牌的实现不适合与 AD 一起使用(除非我们开始创建一些包含密码或其他一些基于用户的秘密凭据的本地用户存储,我不建议这种方法,因为我不知道其他细节您的应用程序,尽管它可能是一个不错的方法)。
The other remember me implementation is based on persistent tokens (PersistentTokenBasedRememberMeServices
) and it works like this (in a bit simplified way):
另一个记住我的实现基于持久令牌 ( PersistentTokenBasedRememberMeServices
),它的工作方式是这样的(以稍微简化的方式):
- when user authenticates we generate a random token
- we store the token in storage together with information about user's ID associated with it
- we create a cookie which includes the token ID
- 当用户进行身份验证时,我们会生成一个随机令牌
- 我们将令牌与有关用户 ID 的信息一起存储在存储中
- 我们创建一个包含令牌 ID 的 cookie
When user wants to authenticate we:
当用户想要进行身份验证时,我们:
- check whether we have the cookie with token ID available
- verify whether the token ID exists in database
- load user's data based on information in the database
- 检查我们是否有带有令牌 ID 的 cookie 可用
- 验证数据库中是否存在令牌 ID
- 根据数据库中的信息加载用户的数据
As you can see, the password is no longer required, although we now need a token storage (typically database, we can use in-memory for testing) which is used instead of the password verification.
如您所见,不再需要密码,尽管我们现在需要一个令牌存储(通常是数据库,我们可以使用内存进行测试)来代替密码验证。
And that gets us to the configuration part. The basic configuration for persistent-token-based remember me looks like this:
这让我们进入配置部分。基于持久令牌的记住我的基本配置如下所示:
@Override
protected void configure(HttpSecurity http) throws Exception {
....
String internalSecretKey = "internalSecretKey";
http.rememberMe().rememberMeServices(rememberMeServices(internalSecretKey)).key(internalSecretKey);
}
@Bean
public RememberMeServices rememberMeServices(String internalSecretKey) {
BasicRememberMeUserDetailsService rememberMeUserDetailsService = new BasicRememberMeUserDetailsService();
InMemoryTokenRepositoryImpl rememberMeTokenRepository = new InMemoryTokenRepositoryImpl();
PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices(staticKey, rememberMeUserDetailsService, rememberMeTokenRepository);
services.setAlwaysRemember(true);
return services;
}
This implementation will use in-memory token storage which should be replaced with JdbcTokenRepositoryImpl
for production. The provided UserDetailsService
is responsible for loading of additional data for the user identified by the user ID loaded from the remember me cookie. The simpliest implementation can look like this:
此实现将使用内存令牌存储,应将其替换JdbcTokenRepositoryImpl
为生产。提供UserDetailsService
者负责为从记住我 cookie 加载的用户 ID 标识的用户加载附加数据。最简单的实现如下所示:
public class BasicRememberMeUserDetailsService implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User(username, "", Collections.<GrantedAuthority>emptyList());
}
}
You could also supply another UserDetailsService
implementation which loads additional attributes or group memberships from your AD or internal database, depending on your needs. It could look like this:
您还可以提供另一个UserDetailsService
实现,根据您的需要从您的 AD 或内部数据库加载附加属性或组成员资格。它可能看起来像这样:
@Bean
public RememberMeServices rememberMeServices(String internalSecretKey) {
LdapContextSource ldapContext = getLdapContext();
String searchBase = "OU=Users,DC=test,DC=company,DC=com";
String searchFilter = "(&(objectClass=user)(sAMAccountName={0}))";
FilterBasedLdapUserSearch search = new FilterBasedLdapUserSearch(searchBase, searchFilter, ldapContext);
search.setSearchSubtree(true);
LdapUserDetailsService rememberMeUserDetailsService = new LdapUserDetailsService(search);
rememberMeUserDetailsService.setUserDetailsMapper(new CustomUserDetailsServiceImpl());
InMemoryTokenRepositoryImpl rememberMeTokenRepository = new InMemoryTokenRepositoryImpl();
PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices(internalSecretKey, rememberMeUserDetailsService, rememberMeTokenRepository);
services.setAlwaysRemember(true);
return services;
}
@Bean
public LdapContextSource getLdapContext() {
LdapContextSource source = new LdapContextSource();
source.setUserDn("user@"+DOMAIN);
source.setPassword("password");
source.setUrl(URL);
return source;
}
This will get you remember me functionality which works with LDAP and provides the loaded data inside RememberMeAuthenticationToken
which will be available in the SecurityContextHolder.getContext().getAuthentication()
. It will also be able to re-use your existing logic for parsing of LDAP data into an User object (CustomUserDetailsServiceImpl
).
这将使您记住我的功能,该功能可与 LDAP 一起使用并提供加载的数据,RememberMeAuthenticationToken
这些数据将在SecurityContextHolder.getContext().getAuthentication()
. 它还可以重用您现有的逻辑来将 LDAP 数据解析为用户对象 ( CustomUserDetailsServiceImpl
)。
As a separate subject, there's also one problem with the code posted in the question, you should replace the:
作为一个单独的主题,问题中发布的代码也存在一个问题,您应该替换:
authManagerBuilder
.authenticationProvider(activeDirectoryLdapAuthenticationProvider())
.userDetailsService(userDetailsService())
;
with:
和:
authManagerBuilder
.authenticationProvider(activeDirectoryLdapAuthenticationProvider())
;
The call to userDetailsService should only be made in order to add DAO-based authentication (e.g. against database) and should be called with a real implementation of the user details service. Your current configuration can lead to infinite loops.
对 userDetailsService 的调用应该仅用于添加基于 DAO 的身份验证(例如针对数据库),并且应该使用用户详细信息服务的真实实现来调用。您当前的配置可能会导致无限循环。
回答by SergeyB
It sounds like you are missing an instance of UserService
that your RememberMeService
needs a reference to. Since you are using LDAP, you'd need an LDAP version of UserService
. I'm only familiar with JDBC/JPA implementations, but looks like org.springframework.security.ldap.userdetails.LdapUserDetailsManager
is what you are looking for. Then your config would look something like this:
这听起来像你缺少的一个实例UserService
,你RememberMeService
需要一个参考。由于您使用的是 LDAP,因此您需要 LDAP 版本的UserService
. 我只熟悉 JDBC/JPA 实现,但看起来org.springframework.security.ldap.userdetails.LdapUserDetailsManager
正是您正在寻找的。然后你的配置看起来像这样:
@Bean
public UserDetailsService getUserDetailsService() {
return new LdapUserDetailsManager(); // TODO give it whatever constructor params it needs
}
@Bean
public RememberMeServices rememberMeServices() {
TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("password", getUserDetailsService());
rememberMeServices.setCookieName("cookieName");
rememberMeServices.setParameter("rememberMe");
return rememberMeServices;
}