spring security,UserDetailsService,authenticationProvider,密码编码器..我迷路了
Posted
技术标签:
【中文标题】spring security,UserDetailsService,authenticationProvider,密码编码器..我迷路了【英文标题】:spring security, UserDetailsService, authenticationProvider, pass word encoder.. i'm lost 【发布时间】:2016-12-19 20:01:56 【问题描述】:首先,我已经阅读/重读(重复 10 次),至少有 6 本关于春季和春季安全的书籍,并在谷歌上搜索了所有内容。
在使用 spring 工作 10 年后,我仍然发现有如此多的注解定义、注入、组件、配置注解魔法正在发生,我对自己理解我的应用程序的信心为 0。 在线示例要么是 xml-config,要么不完整,要么完成 n diff。方式,过于简单化,使用较旧的弹簧,相互冲突,而且根本不是为处理基本的现实用例而构建的。
例如,以下代码尝试处理简单的登录,使用密码编码器对 db 表进行身份验证。 表单帖子包括一个对其进行身份验证的“客户端”、一个持久的 IP 地址和一些用于深度链接帖子登录的 url 路径信息。 (对于当今的单页网络应用来说,所有这些都是非常基本的东西) 我最初使用 xml 配置来工作,但 javaConfig 让我卡住了。
我不知道 userDetailsService、AuthenticationManagerBuilder 和 PasswordEncoder 在 SecurityConfiguration 中是如何交互的。我获得了服务的登录数据,但不确定在何时何地应用了 spring authenticationProvider,或者我什至需要一个。
我的用户实现了 UserDetails 并保存了必填字段。 我在我的 CustomUserDetailsService 中填充这些并授予权限。 如果我在服务中使用登录名/密码检查数据库,我如何/何时/为什么需要 auth.authenticationProvider(authenticationProvider())?
我的 UserDetailsService 现在似乎执行了两次。
spring 如何获取提交的密码,对其进行编码并与存储在数据库中的密码进行比较? 它怎么知道使用与创建用户时创建/持久化 p/w 时使用的相同的盐?
当 authenticationProvider() 也设置了 userDetailsService 时,为什么 configureGlobal() 会同时定义 auth.userDetailsService 和 auth.authenticationProvider?
为什么我的大脑这么小,我无法理解这一点? :)
@Service
public class CustomUserDetailsService implements UserDetailsService
@Autowired
private ClientDAO clientDAO;
@Autowired
private UserDAO userDAO;
public UserDetails loadUserByUsername(String multipartLogon) throws UsernameNotFoundException, DataAccessException
Boolean canAccess = false;
Long clientId = null;
String userLogon = null;
String password = null;
String id = null;
String entryUrl = null;
String ipAddress = null;
String urlParam = null;
String[] strParts = multipartLogon.split(":");
try
userLogon = strParts[0];
password = strParts[1];
id = strParts[2];
entryUrl = strParts[3];
ipAddress = strParts[4];
urlParam = strParts[5];
catch(IndexOutOfBoundsException ioob)
Client client = new Client();
if (!"".equals(id))
clientId = IdUtil.toLong(id);
client = clientDAO.getClient(clientId);
//BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//String encodedPassword = passwordEncoder.encode(password);
//String encodedPassword = "$2a$22$6UiVlDEOv6IQWjKkLm.04uN1yZEtkepVqYQ00JxaqPCtjzwIkXDjy";
User user = userDAO.getUserByUserLogonPassword(userLogon, password); //encodedPassword?
user.isCredentialsNonExpired = false;
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
for (UserRole userRole : userDAO.getUserRolesForUser(user))
if (userRole.getRole().getActiveStatus())
authorities.add(new SimpleGrantedAuthority(userRole.getRole().getRoleName()));
user.isCredentialsNonExpired = true;
user.setAuthorities(authorities);
user.setPassword(password); //encodedPassword?
user.setUsername(user.getUserLogon());
user.isAccountNonExpired = false;
user.isAccountNonLocked = false;
List<ClientUser> clientUsers = clientDAO.getClientUsersForUser(user);
for (ClientUser clientUser : clientUsers)
if (clientUser.getClient().getClientId().equals(client.getClientId()))
canAccess = true;
break;
user.isEnabled = false;
if (user.getActiveStatus() && canAccess)
user.isAccountNonExpired = true;
user.isAccountNonLocked = true;
user.isEnabled = true;
Session session = userDAO.getSessionForUser(user);
if (session == null) session = new Session();
session.setUser(user);
session.setDateLogon(Calendar.getInstance().getTime());
session.setClient(client);
session.setEntryUrl(entryUrl);
session.setUrlParam(urlParam);
session.setIPAddress(ipAddress);
session.setActive(true);
userDAO.persistOrMergeSession(session);
return user;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
@Autowired
CustomUserDetailsService customUserDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
auth.userDetailsService(customUserDetailsService);
auth.authenticationProvider(authenticationProvider());
@Bean
public BCryptPasswordEncoder passwordEncoder()
return new BCryptPasswordEncoder();
@Bean
public DaoAuthenticationProvider authenticationProvider()
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(customUserDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/conv/a/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_COURT_ADMIN')")
.antMatchers("/conv/u/**").access("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN') or hasRole('ROLE_COURT_ADMIN')")
.antMatchers("/**").permitAll()
.and()
.formLogin()
.loginPage("/conv/common/logon")
.usernameParameter("multipartLogon")
.loginProcessingUrl("/conv/common/logon")
.defaultSuccessUrl("/conv/")
.failureUrl("/conv/common/logon?error=1")
.and()
.logout()
.logoutUrl("/conv/common/logout")
.logoutSuccessUrl("/conv/")
.permitAll()
.and()
.rememberMe()
.key("conv_key")
.rememberMeServices(rememberMeServices())
.useSecureCookie(true);
@Override
public void configure(WebSecurity web) throws Exception
web.ignoring()
.antMatchers("/common/**")
.antMatchers("/favicon.ico");
@Bean
public RememberMeServices rememberMeServices()
TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("conv_key", customUserDetailsService);
rememberMeServices.setCookieName("remember_me_cookie");
rememberMeServices.setParameter("remember_me_checkbox");
rememberMeServices.setTokenValiditySeconds(2678400); //1month
return rememberMeServices;
【问题讨论】:
【参考方案1】:我的用户实现了 UserDetails 并保存了必填字段。一世 在我的 CustomUserDetailsService 中填充这些并授予权限。 我如何/何时/为什么需要一个 auth.authenticationProvider(authenticationProvider()),如果我检查数据库 在我的服务中使用登录名/密码?
我想你想要的是:
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
userDetailsService 方法是创建DaoAuthenticationProvider
bean 的快捷方式!你不应该两者都需要,它只是配置同一事物的两种不同方式。 authenticationProvider 方法用于更多的自定义设置。
spring 如何获取提交的密码,对其进行编码并进行比较 存储在数据库中?它怎么知道使用与那相同的盐 在创建用户时创建/持久化 p/w 时使用?
如果您使用 BCrypt,salt 存储在编码的密码值中。盐是第三个 $(美元)符号后的前 22 个字符。 matches 方法负责检查密码。
为什么 configureGlobal() 定义了 auth.userDetailsService 和 auth.authenticationProvider 当 authenticationProvider() 也设置 userDetailsService?
见上文。这可能是用户详细信息被加载两次的原因。
更新:奇怪的是,您将密码和其他详细信息输入到 UserDetailsService 中。这应该只根据用户名加载用户,例如:
User user = userDAO.getUserByUserLogonPassword(userLogon);
返回的用户对象应该包含编码(存储)的密码,而不是输入的密码。 Spring Security 会为您进行密码检查。您不应修改 UserDetailsService 中的 User 对象。
【讨论】:
哈! thx,盐的答案非常有帮助。只有 UserDetailsService,是的,这就是我原来的方式。我也将 passwordEncoder 定义为与您的完全一样。通过在线示例,人们倾向于以 9 种方式将这些东西放在一起到星期天,希望能奏效。 我的问题是我的 CustomUserDetailsService.loadUserByUsername() 采用冒号分隔的字符串,其中包含的不仅仅是用户名。它包含登录:密码:clientId:entryUrl:等。然而之后,当 DaoAuthProvider.retrieveUser(String username..) 被调用时,使用原始的多部分登录到 loadUserByUsername 并且它失败了 auth 实际上 DaoAuthenticationProvider 有正确的loadedUser,但是AbstractDetailsAuthenticationProvider authenticate() 使用原来的冒号分隔的用户名,这就是它验证失败的时候。 用户名,p/w 查找是我自定义的。我更新为只检查用户名。问题是,CustomUserDetailsService 工作正常,并且 p/w 已正确自动加密.. 但是随后 DaoAuthenticationProvider.retrieveUser() 被调用并且该方法使用最初提交的 multipartLogon(并且包含:[userName]:[clientId]:[requestedUrl ];[IPAddress]: 等等。这是因为我需要的不仅仅是用户名,来自登录的 p/w 和 userDetails.loadUserByUsername() 只接受单个字符串 arg 并自动传递给 DaoAutehnticationProvider。 @oldMan 再读一遍。不要乱用 UserDetailsService 中的密码之类的东西!【参考方案2】:哇,好的,这是很多问题。我会和这个人说话:
“我不知道 userDetailsService、AuthenticationManagerBuilder 和 PasswordEncoder 是如何实现的”
UserDetailsService 设置您可以从 Spring 访问的 User。如果您希望在用户的上下文中存储更多用户信息,则需要实现自己的用户并使用自定义用户详细信息服务进行设置。例如
public class CustomUser extends User implements UserDetails, CredentialsContainer
private Long id;
private String firstName;
private String lastName;
private String emailAddress;
....
然后,在您的自定义 UserDetailsService 中,设置属性:
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
DatabaseEntity databaseUser = this.userRepository.findByUsernameIgnoreCase(username);
customUser customUser = databaseUser.getUserDetails();
customUser.setId(databaseUser.getId());
customUser.setFirstName(databaseUser.getFirstname());
.....
密码编码器是 Spring 用来将纯文本密码与数据库中的加密哈希进行比较的机制。您可以使用 BCryptPasswordEncoder:
@Bean
public PasswordEncoder passwordEncoder()
return new BCryptPasswordEncoder();
除了将其传递给您的身份验证提供者之外,您还需要做更多的事情。
最后,configureGlobal 是您进行连接的地方。您定义了 Spring 要使用的用户详细信息服务和身份验证提供程序。
就我而言,我使用自定义身份验证提供程序来限制失败的登录尝试:
@Component("authenticationProvider")
public class LimitLoginAuthenticationProvider extends DaoAuthenticationProvider
然后我把所有东西都连接起来:
@Autowired
@Qualifier("authenticationProvider")
AuthenticationProvider authenticationProvider;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
LimitLoginAuthenticationProvider provider = (LimitLoginAuthenticationProvider)authenticationProvider;
provider.setPasswordEncoder(passwordEncoder);
auth.userDetailsService(customUserDetailsService()).passwordEncoder(passwordEncoder);
auth.authenticationProvider(authenticationProvider);
【讨论】:
感谢 Brian 的回复。是的,我知道我的帖子会如何让人们认为我对 userService、javaConfig 等的基本机制一无所知。 感谢 Brian 的回复。是的,我看到我的帖子如何让人们假设我对 userService、javaConfig 等的基本机制一无所知。但正如我包含的代码所证明的那样,我拥有你提到的所有设置。我实际上调用了一个使用用户名和密码来获取用户(实现用户详细信息)的 dao。当仅通过用户名加载用户时,我看不到安全性如何工作。是否使用密码在 UserDetails 上再次查找?它是如何/何时加密的(因为它与 db 中的加密值相比)?弹簧自动处理这个吗?春天怎么知道使用相同的盐? 断点显示检索和填充的用户在服务中。我什至尝试手动将encodedPassword 设置为db-stored 值。我得到了一个用户,但仍然没有通过 spring 身份验证。我不得不认为编码器没有被应用。我无法想象我手动添加:new BCryptPasswordEncoder() in service 以加密提交的 p/w,因为盐可能是 diff。 在 DaoAuthenticationProvider 中,会进行实际的密码检查。请参阅附加的AuthenticationChecks()。在我的情况下,使用我的自定义身份验证提供程序,我调用 super:@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException try Authentication auth = super.authenticate(authentication); 就我而言,在实施新的 UserDetailService 后,我没有更新 auth.userDetailsService(XXX) 下的类名,因此我一直收到Authentication failed: password does not match stored value
的错误以上是关于spring security,UserDetailsService,authenticationProvider,密码编码器..我迷路了的主要内容,如果未能解决你的问题,请参考以下文章
Spring mvc / security:从spring security中排除登录页面
Spring Security:2.4 Getting Spring Security
没有 JSP 的 Spring Security /j_spring_security_check